Files
2025-10-23 20:40:17 +07:00

1660 lines
44 KiB
Plaintext

# RevoLand SuperApp - Runtime Modularization Architecture with Security
## 1. Project Overview
**RevoLand** is a real estate SuperApp built on Flutter (iOS & Android) that dynamically loads and runs SubApps remotely through WebView at runtime. Each SubApp resides in its own repository, deployed as Flutter Web, and hosted on a server or CDN. This allows updates without republishing the main SuperApp, with robust security mechanisms to prevent unauthorized access.
## 2. Architecture Components
### 2.1 SuperApp (Flutter Native)
The main RevoLand app contains the core interface, navigation, and shared services.
**Key Responsibilities:**
- User authentication
- Fetch available SubApps from Metadata API
- Display dynamic menu based on user permissions
- Launch SubApps in WebView with secure context
- Establish bidirectional communication bridge
**Example Implementation:**
```dart
// lib/models/sub_app.dart
class SubApp {
final String id;
final String displayName;
final String description;
final String iconUrl;
final String launchUrl;
final String signedToken;
final List<String> permissions;
SubApp({
required this.id,
required this.displayName,
required this.description,
required this.iconUrl,
required this.launchUrl,
required this.signedToken,
required this.permissions,
});
factory SubApp.fromJson(Map<String, dynamic> json) {
return SubApp(
id: json['id'] as String,
displayName: json['displayName'] as String,
description: json['description'] as String,
iconUrl: json['iconUrl'] as String,
launchUrl: json['launchUrl'] as String,
signedToken: json['signedToken'] as String,
permissions: List<String>.from(json['permissions'] ?? []),
);
}
}
```
```dart
// lib/services/metadata_service.dart
import 'package:dio/dio.dart';
import '../models/sub_app.dart';
class MetadataService {
final Dio _dio;
final String _baseUrl;
MetadataService({
required String baseUrl,
required String authToken,
}) : _baseUrl = baseUrl,
_dio = Dio(BaseOptions(
baseUrl: baseUrl,
headers: {
'Authorization': 'Bearer $authToken',
'Content-Type': 'application/json',
},
));
Future<List<SubApp>> fetchAvailableSubApps() async {
try {
final response = await _dio.get('/api/v1/subapps/available');
if (response.statusCode == 200) {
final List<dynamic> data = response.data['subApps'];
return data.map((json) => SubApp.fromJson(json)).toList();
} else {
throw Exception('Failed to fetch SubApps');
}
} catch (e) {
throw Exception('Error fetching SubApps: $e');
}
}
Future<String> refreshSubAppToken(String subAppId) async {
try {
final response = await _dio.post(
'/api/v1/subapps/$subAppId/token/refresh',
);
if (response.statusCode == 200) {
return response.data['signedToken'] as String;
} else {
throw Exception('Failed to refresh token');
}
} catch (e) {
throw Exception('Error refreshing token: $e');
}
}
}
```
```dart
// lib/screens/sub_app_launcher.dart
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import '../models/sub_app.dart';
class SubAppLauncher extends StatefulWidget {
final SubApp subApp;
final String userId;
final String userRole;
const SubAppLauncher({
Key? key,
required this.subApp,
required this.userId,
required this.userRole,
}) : super(key: key);
@override
State<SubAppLauncher> createState() => _SubAppLauncherState();
}
class _SubAppLauncherState extends State<SubAppLauncher> {
late WebViewController _controller;
bool _isLoading = true;
@override
void initState() {
super.initState();
_initializeWebView();
}
void _initializeWebView() {
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(Colors.white)
..addJavaScriptChannel(
'RevoLandNativeBridge',
onMessageReceived: _handleNativeMessage,
)
..setNavigationDelegate(
NavigationDelegate(
onPageStarted: (url) {
setState(() => _isLoading = true);
},
onPageFinished: (url) {
_injectSuperAppContext();
setState(() => _isLoading = false);
},
onWebResourceError: (error) {
debugPrint('WebView error: ${error.description}');
},
),
)
..loadRequest(Uri.parse(
'${widget.subApp.launchUrl}?token=${widget.subApp.signedToken}&hostAppId=revoland-mobile',
));
}
void _injectSuperAppContext() {
final context = '''
window.__REVOLAND_CONTEXT__ = {
token: "${widget.subApp.signedToken}",
hostAppId: "revoland-mobile",
platform: "${Theme.of(context).platform.name}",
userId: "${widget.userId}",
userRole: "${widget.userRole}",
subAppId: "${widget.subApp.id}",
theme: {
primaryColor: "#2563eb",
isDarkMode: ${Theme.of(context).brightness == Brightness.dark}
},
locale: "${Localizations.localeOf(context).languageCode}"
};
if (window.onRevoLandContextReady) {
window.onRevoLandContextReady(window.__REVOLAND_CONTEXT__);
}
console.log("RevoLand context injected:", window.__REVOLAND_CONTEXT__);
''';
_controller.runJavaScript(context);
}
void _handleNativeMessage(JavaScriptMessage message) {
debugPrint('Message from SubApp: ${message.message}');
try {
final data = jsonDecode(message.message);
final action = data['action'] as String?;
switch (action) {
case 'subappReady':
debugPrint('SubApp ${widget.subApp.id} is ready');
break;
case 'requestLocation':
_handleLocationRequest(data);
break;
case 'shareProperty':
_handleShareProperty(data);
break;
case 'openMap':
_handleOpenMap(data);
break;
case 'navigateBack':
Navigator.of(context).pop();
break;
default:
debugPrint('Unknown action: $action');
}
} catch (e) {
debugPrint('Error handling message: $e');
}
}
void _handleLocationRequest(Map<String, dynamic> data) {
// Request device location and send back to SubApp
// Implementation depends on location service
}
void _handleShareProperty(Map<String, dynamic> data) {
final propertyId = data['propertyId'] as String?;
final propertyUrl = data['propertyUrl'] as String?;
// Trigger native share dialog
}
void _handleOpenMap(Map<String, dynamic> data) {
final lat = data['latitude'] as double?;
final lng = data['longitude'] as double?;
// Open native maps app
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.subApp.displayName),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () => _controller.reload(),
),
],
),
body: Stack(
children: [
WebViewWidget(controller: _controller),
if (_isLoading)
const Center(
child: CircularProgressIndicator(),
),
],
),
);
}
}
```
```dart
// lib/screens/home_screen.dart
import 'package:flutter/material.dart';
import '../models/sub_app.dart';
import '../services/metadata_service.dart';
import 'sub_app_launcher.dart';
class HomeScreen extends StatefulWidget {
final String userId;
final String userRole;
final String authToken;
const HomeScreen({
Key? key,
required this.userId,
required this.userRole,
required this.authToken,
}) : super(key: key);
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
late MetadataService _metadataService;
List<SubApp> _subApps = [];
bool _isLoading = true;
String? _error;
@override
void initState() {
super.initState();
_metadataService = MetadataService(
baseUrl: 'https://api.revoland.com',
authToken: widget.authToken,
);
_loadSubApps();
}
Future<void> _loadSubApps() async {
setState(() {
_isLoading = true;
_error = null;
});
try {
final subApps = await _metadataService.fetchAvailableSubApps();
setState(() {
_subApps = subApps;
_isLoading = false;
});
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
void _launchSubApp(SubApp subApp) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SubAppLauncher(
subApp: subApp,
userId: widget.userId,
userRole: widget.userRole,
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('RevoLand'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loadSubApps,
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _error != null
? Center(child: Text('Error: $_error'))
: GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 1.2,
),
itemCount: _subApps.length,
itemBuilder: (context, index) {
final subApp = _subApps[index];
return _SubAppCard(
subApp: subApp,
onTap: () => _launchSubApp(subApp),
);
},
),
);
}
}
class _SubAppCard extends StatelessWidget {
final SubApp subApp;
final VoidCallback onTap;
const _SubAppCard({
required this.subApp,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return Card(
elevation: 2,
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.network(
subApp.iconUrl,
width: 48,
height: 48,
errorBuilder: (context, error, stackTrace) =>
const Icon(Icons.apps, size: 48),
),
const SizedBox(height: 12),
Text(
subApp.displayName,
style: Theme.of(context).textTheme.titleMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 4),
Text(
subApp.description,
style: Theme.of(context).textTheme.bodySmall,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
),
);
}
}
```
### 2.2 Metadata API (Backend)
Backend service that manages SubApp metadata and authentication.
**Example Implementation (Node.js + Express):**
```typescript
// src/types/subapp.types.ts
export interface SubAppMetadata {
id: string;
displayName: string;
description: string;
iconUrl: string;
launchUrl: string;
signedToken: string;
permissions: string[];
version: string;
minHostVersion: string;
}
export interface JWTPayload {
userId: string;
hostAppId: string;
subAppId: string;
userRole: string;
permissions: string[];
iat: number;
exp: number;
}
```
```typescript
// src/middleware/auth.middleware.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
export interface AuthRequest extends Request {
user?: {
userId: string;
role: string;
email: string;
};
}
export const authenticateUser = (
req: AuthRequest,
res: Response,
next: NextFunction
): void => {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
res.status(401).json({ error: 'No token provided' });
return;
}
const token = authHeader.substring(7);
const decoded = jwt.verify(token, JWT_SECRET) as {
userId: string;
role: string;
email: string;
};
req.user = decoded;
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
};
```
```typescript
// src/services/subapp.service.ts
import jwt from 'jsonwebtoken';
import { SubAppMetadata, JWTPayload } from '../types/subapp.types';
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
const TOKEN_EXPIRY = '5m'; // 5 minutes
export class SubAppService {
private subAppRegistry: Map<string, Omit<SubAppMetadata, 'signedToken'>> = new Map([
[
'property-listings',
{
id: 'property-listings',
displayName: 'Property Listings',
description: 'Browse available properties',
iconUrl: 'https://cdn.revoland.com/icons/listings.png',
launchUrl: 'https://cdn.revoland.com/subapps/property-listings/index.html',
permissions: ['read:properties', 'search:properties'],
version: '1.2.0',
minHostVersion: '1.0.0',
},
],
[
'virtual-tours',
{
id: 'virtual-tours',
displayName: 'Virtual Tours',
description: '360° property tours',
iconUrl: 'https://cdn.revoland.com/icons/virtual-tour.png',
launchUrl: 'https://cdn.revoland.com/subapps/virtual-tours/index.html',
permissions: ['read:properties', 'view:tours'],
version: '2.0.1',
minHostVersion: '1.0.0',
},
],
[
'mortgage-calculator',
{
id: 'mortgage-calculator',
displayName: 'Mortgage Calculator',
description: 'Calculate mortgage payments',
iconUrl: 'https://cdn.revoland.com/icons/calculator.png',
launchUrl: 'https://cdn.revoland.com/subapps/mortgage-calculator/index.html',
permissions: ['calculate:mortgage'],
version: '1.5.2',
minHostVersion: '1.0.0',
},
],
[
'agent-dashboard',
{
id: 'agent-dashboard',
displayName: 'Agent Dashboard',
description: 'Manage your properties and leads',
iconUrl: 'https://cdn.revoland.com/icons/dashboard.png',
launchUrl: 'https://cdn.revoland.com/subapps/agent-dashboard/index.html',
permissions: ['manage:properties', 'manage:leads', 'view:analytics'],
version: '3.1.0',
minHostVersion: '1.0.0',
},
],
]);
private rolePermissions: Record<string, string[]> = {
buyer: ['read:properties', 'search:properties', 'view:tours', 'calculate:mortgage'],
seller: ['read:properties', 'search:properties', 'view:tours'],
agent: ['read:properties', 'search:properties', 'view:tours', 'calculate:mortgage', 'manage:properties', 'manage:leads', 'view:analytics'],
admin: ['*'],
};
generateSubAppToken(
userId: string,
subAppId: string,
userRole: string,
permissions: string[]
): string {
const payload: JWTPayload = {
userId,
hostAppId: 'revoland-mobile',
subAppId,
userRole,
permissions,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 300, // 5 minutes
};
return jwt.sign(payload, JWT_SECRET);
}
getUserPermissions(userRole: string): string[] {
return this.rolePermissions[userRole] || [];
}
hasPermission(userPermissions: string[], requiredPermission: string): boolean {
if (userPermissions.includes('*')) return true;
return userPermissions.includes(requiredPermission);
}
getAvailableSubApps(userId: string, userRole: string): SubAppMetadata[] {
const userPermissions = this.getUserPermissions(userRole);
const availableSubApps: SubAppMetadata[] = [];
for (const [id, subApp] of this.subAppRegistry) {
// Check if user has at least one required permission
const hasAccess = subApp.permissions.some((perm) =>
this.hasPermission(userPermissions, perm)
);
if (hasAccess) {
const signedToken = this.generateSubAppToken(
userId,
id,
userRole,
userPermissions.filter((p) => subApp.permissions.includes(p) || p === '*')
);
availableSubApps.push({
...subApp,
signedToken,
});
}
}
return availableSubApps;
}
refreshToken(userId: string, subAppId: string, userRole: string): string | null {
const subApp = this.subAppRegistry.get(subAppId);
if (!subApp) return null;
const userPermissions = this.getUserPermissions(userRole);
const hasAccess = subApp.permissions.some((perm) =>
this.hasPermission(userPermissions, perm)
);
if (!hasAccess) return null;
return this.generateSubAppToken(
userId,
subAppId,
userRole,
userPermissions.filter((p) => subApp.permissions.includes(p) || p === '*')
);
}
}
```
```typescript
// src/routes/subapp.routes.ts
import { Router } from 'express';
import { AuthRequest, authenticateUser } from '../middleware/auth.middleware';
import { SubAppService } from '../services/subapp.service';
const router = Router();
const subAppService = new SubAppService();
// Get available SubApps for current user
router.get('/available', authenticateUser, (req: AuthRequest, res) => {
try {
if (!req.user) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
const subApps = subAppService.getAvailableSubApps(
req.user.userId,
req.user.role
);
res.json({
success: true,
subApps,
timestamp: new Date().toISOString(),
});
} catch (error) {
console.error('Error fetching SubApps:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Refresh token for specific SubApp
router.post('/:subAppId/token/refresh', authenticateUser, (req: AuthRequest, res) => {
try {
if (!req.user) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
const { subAppId } = req.params;
const newToken = subAppService.refreshToken(
req.user.userId,
subAppId,
req.user.role
);
if (!newToken) {
res.status(403).json({ error: 'Access denied to this SubApp' });
return;
}
res.json({
success: true,
signedToken: newToken,
expiresIn: 300,
});
} catch (error) {
console.error('Error refreshing token:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
export default router;
```
```typescript
// src/app.ts
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import subAppRoutes from './routes/subapp.routes';
const app = express();
// Security middleware
app.use(helmet());
app.use(cors({
origin: ['https://revoland.com', 'https://app.revoland.com'],
credentials: true,
}));
app.use(express.json());
// Routes
app.use('/api/v1/subapps', subAppRoutes);
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Metadata API server running on port ${PORT}`);
});
export default app;
```
### 2.3 SubApp (Flutter Web)
Independent SubApp module that runs inside WebView.
**Example: Property Listings SubApp**
```dart
// property_listings/lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
import 'services/revoland_bridge.dart';
import 'services/api_service.dart';
import 'screens/listings_screen.dart';
void main() {
setUrlStrategy(PathUrlStrategy());
runApp(const PropertyListingsApp());
}
class PropertyListingsApp extends StatefulWidget {
const PropertyListingsApp({Key? key}) : super(key: key);
@override
State<PropertyListingsApp> createState() => _PropertyListingsAppState();
}
class _PropertyListingsAppState extends State<PropertyListingsApp> {
late RevoLandBridge _bridge;
late ApiService _apiService;
bool _isReady = false;
@override
void initState() {
super.initState();
_initializeSubApp();
}
Future<void> _initializeSubApp() async {
_bridge = RevoLandBridge();
await _bridge.initialize();
final context = _bridge.getSuperAppContext();
if (context == null) {
debugPrint('Error: SuperApp context not available');
return;
}
_apiService = ApiService(
baseUrl: 'https://api.revoland.com',
token: context.token,
);
setState(() => _isReady = true);
// Notify SuperApp that SubApp is ready
_bridge.sendMessage({
'action': 'subappReady',
'subAppId': context.subAppId,
'timestamp': DateTime.now().toIso8601String(),
});
}
@override
Widget build(BuildContext context) {
if (!_isReady) {
return const MaterialApp(
home: Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
),
);
}
return MaterialApp(
title: 'Property Listings',
theme: ThemeData(
primaryColor: Color(
int.parse(_bridge.getSuperAppContext()?.theme['primaryColor']
?.replaceFirst('#', '0xFF') ?? '0xFF2563eb'),
),
brightness: _bridge.getSuperAppContext()?.theme['isDarkMode'] == true
? Brightness.dark
: Brightness.light,
),
home: ListingsScreen(
apiService: _apiService,
bridge: _bridge,
),
);
}
}
```
```dart
// property_listings/lib/services/revoland_bridge.dart
import 'dart:convert';
import 'dart:html' as html;
import 'package:flutter/foundation.dart';
class SuperAppContext {
final String token;
final String hostAppId;
final String platform;
final String userId;
final String userRole;
final String subAppId;
final Map<String, dynamic> theme;
final String locale;
SuperAppContext({
required this.token,
required this.hostAppId,
required this.platform,
required this.userId,
required this.userRole,
required this.subAppId,
required this.theme,
required this.locale,
});
factory SuperAppContext.fromJson(Map<String, dynamic> json) {
return SuperAppContext(
token: json['token'] as String,
hostAppId: json['hostAppId'] as String,
platform: json['platform'] as String,
userId: json['userId'] as String,
userRole: json['userRole'] as String,
subAppId: json['subAppId'] as String,
theme: json['theme'] as Map<String, dynamic>,
locale: json['locale'] as String,
);
}
}
class RevoLandBridge {
SuperAppContext? _context;
final List<Function(Map<String, dynamic>)> _messageListeners = [];
Future<void> initialize() async {
// Try to get context from window object
_context = _getContextFromWindow();
if (_context == null) {
// Try to get from URL params
_context = _getContextFromUrl();
}
// Set up callback for when context is injected
_setupContextCallback();
debugPrint('RevoLand Bridge initialized with context: $_context');
}
SuperAppContext? _getContextFromWindow() {
try {
final contextObj = html.window['__REVOLAND_CONTEXT__'];
if (contextObj != null) {
final Map<String, dynamic> contextMap = {};
final keys = js_util.getProperty(contextObj, 'keys');
// Convert JS object to Dart Map
// This is a simplified version - actual implementation may vary
return SuperAppContext.fromJson(contextMap);
}
} catch (e) {
debugPrint('Error getting context from window: $e');
}
return null;
}
SuperAppContext? _getContextFromUrl() {
try {
final uri = Uri.parse(html.window.location.href);
final token = uri.queryParameters['token'];
final hostAppId = uri.queryParameters['hostAppId'];
if (token != null && hostAppId != null) {
// Create minimal context from URL params
return SuperAppContext(
token: token,
hostAppId: hostAppId,
platform: 'web',
userId: '',
userRole: '',
subAppId: '',
theme: {'primaryColor': '#2563eb', 'isDarkMode': false},
locale: 'en',
);
}
} catch (e) {
debugPrint('Error getting context from URL: $e');
}
return null;
}
void _setupContextCallback() {
html.window['onRevoLandContextReady'] = (dynamic context) {
try {
// Parse context and update
debugPrint('Context received from SuperApp');
} catch (e) {
debugPrint('Error in context callback: $e');
}
};
}
SuperAppContext? getSuperAppContext() => _context;
void sendMessage(Map<String, dynamic> message) {
try {
final nativeBridge = html.window['RevoLandNativeBridge'];
if (nativeBridge != null) {
final messageStr = jsonEncode(message);
// Call native bridge
// nativeBridge.postMessage(messageStr);
debugPrint('Sending message to SuperApp: $messageStr');
} else {
debugPrint('Native bridge not available');
}
} catch (e) {
debugPrint('Error sending message: $e');
}
}
void requestLocation() {
sendMessage({
'action': 'requestLocation',
'timestamp': DateTime.now().toIso8601String(),
});
}
void shareProperty(String propertyId, String propertyUrl) {
sendMessage({
'action': 'shareProperty',
'propertyId': propertyId,
'propertyUrl': propertyUrl,
'timestamp': DateTime.now().toIso8601String(),
});
}
void openMap(double latitude, double longitude) {
sendMessage({
'action': 'openMap',
'latitude': latitude,
'longitude': longitude,
'timestamp': DateTime.now().toIso8601String(),
});
}
void navigateBack() {
sendMessage({
'action': 'navigateBack',
'timestamp': DateTime.now().toIso8601String(),
});
}
}
```
```dart
// property_listings/lib/services/api_service.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
class Property {
final String id;
final String title;
final String address;
final double price;
final int bedrooms;
final int bathrooms;
final double sqft;
final String imageUrl;
final double latitude;
final double longitude;
Property({
required this.id,
required this.title,
required this.address,
required this.price,
required this.bedrooms,
required this.bathrooms,
required this.sqft,
required this.imageUrl,
required this.latitude,
required this.longitude,
});
factory Property.fromJson(Map<String, dynamic> json) {
return Property(
id: json['id'] as String,
title: json['title'] as String,
address: json['address'] as String,
price: (json['price'] as num).toDouble(),
bedrooms: json['bedrooms'] as int,
bathrooms: json['bathrooms'] as int,
sqft: (json['sqft'] as num).toDouble(),
imageUrl: json['imageUrl'] as String,
latitude: (json['latitude'] as num).toDouble(),
longitude: (json['longitude'] as num).toDouble(),
);
}
}
class ApiService {
final String baseUrl;
final String token;
ApiService({
required this.baseUrl,
required this.token,
});
Map<String, String> get _headers => {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
};
Future<List<Property>> fetchProperties({
String? city,
double? minPrice,
double? maxPrice,
int? minBedrooms,
}) async {
final queryParams = <String, String>{};
if (city != null) queryParams['city'] = city;
if (minPrice != null) queryParams['minPrice'] = minPrice.toString();
if (maxPrice != null) queryParams['maxPrice'] = maxPrice.toString();
if (minBedrooms != null) queryParams['minBedrooms'] = minBedrooms.toString();
final uri = Uri.parse('$baseUrl/api/v1/properties').replace(
queryParameters: queryParams.isEmpty ? null : queryParams,
);
final response = await http.get(uri, headers: _headers);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final List<dynamic> propertiesJson = data['properties'];
return propertiesJson.map((json) => Property.fromJson(json)).toList();
} else if (response.statusCode == 401) {
throw Exception('Unauthorized - token expired');
} else {
throw Exception('Failed to load properties');
}
}
Future<Property> getPropertyDetails(String propertyId) async {
final uri = Uri.parse('$baseUrl/api/v1/properties/$propertyId');
final response = await http.get(uri, headers: _headers);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return Property.fromJson(data['property']);
} else if (response.statusCode == 401) {
throw Exception('Unauthorized - token expired');
} else {
throw Exception('Failed to load property details');
}
}
}
```
```dart
// property_listings/lib/screens/listings_screen.dart
import 'package:flutter/material.dart';
import '../services/api_service.dart';
import '../services/revoland_bridge.dart';
class ListingsScreen extends StatefulWidget {
final ApiService apiService;
final RevoLandBridge bridge;
const ListingsScreen({
Key? key,
required this.apiService,
required this.bridge,
}) : super(key: key);
@override
State<ListingsScreen> createState() => _ListingsScreenState();
}
class _ListingsScreenState extends State<ListingsScreen> {
List<Property> _properties = [];
bool _isLoading = true;
String? _error;
@override
void initState() {
super.initState();
_loadProperties();
}
Future<void> _loadProperties() async {
setState(() {
_isLoading = true;
_error = null;
});
try {
final properties = await widget.apiService.fetchProperties();
setState(() {
_properties = properties;
_isLoading = false;
});
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Property Listings'),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => widget.bridge.navigateBack(),
),
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _error != null
? Center(child: Text('Error: $_error'))
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _properties.length,
itemBuilder: (context, index) {
final property = _properties[index];
return _PropertyCard(
property: property,
onTap: () => _showPropertyDetails(property),
onShare: () => widget.bridge.shareProperty(
property.id,
'https://revoland.com/properties/${property.id}',
),
onShowMap: () => widget.bridge.openMap(
property.latitude,
property.longitude,
),
);
},
),
);
}
void _showPropertyDetails(Property property) {
// Navigate to property details screen
}
}
class _PropertyCard extends StatelessWidget {
final Property property;
final VoidCallback onTap;
final VoidCallback onShare;
final VoidCallback onShowMap;
const _PropertyCard({
required this.property,
required this.onTap,
required this.onShare,
required this.onShowMap,
});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 16),
child: InkWell(
onTap: onTap,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
property.imageUrl,
height: 200,
width: double.infinity,
fit: BoxFit.cover,
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
property.title,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
property.address,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 8),
Text(
'\$${property.price.toStringAsFixed(0)}',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Row(
children: [
_PropertyFeature(
icon: Icons.bed,
label: '${property.bedrooms} beds',
),
const SizedBox(width: 16),
_PropertyFeature(
icon: Icons.bathroom,
label: '${property.bathrooms} baths',
),
const SizedBox(width: 16),
_PropertyFeature(
icon: Icons.square_foot,
label: '${property.sqft.toStringAsFixed(0)} sqft',
),
],
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
icon: const Icon(Icons.map),
onPressed: onShowMap,
),
IconButton(
icon: const Icon(Icons.share),
onPressed: onShare,
),
],
),
],
),
),
],
),
),
);
}
}
class _PropertyFeature extends StatelessWidget {
final IconData icon;
final String label;
const _PropertyFeature({
required this.icon,
required this.label,
});
@override
Widget build(BuildContext context) {
return Row(
children: [
Icon(icon, size: 16),
const SizedBox(width: 4),
Text(label),
],
);
}
}
```
### 2.4 SubApp Backend API
Backend service for SubApp data.
```typescript
// src/middleware/subapp-auth.middleware.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { JWTPayload } from '../types/subapp.types';
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
export interface SubAppAuthRequest extends Request {
subAppAuth?: JWTPayload;
}
export const authenticateSubApp = (
req: SubAppAuthRequest,
res: Response,
next: NextFunction
): void => {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
res.status(401).json({ error: 'No token provided' });
return;
}
const token = authHeader.substring(7);
const decoded = jwt.verify(token, JWT_SECRET) as JWTPayload;
// Verify token hasn't expired
const now = Math.floor(Date.now() / 1000);
if (decoded.exp < now) {
res.status(401).json({ error: 'Token expired' });
return;
}
// Verify hostAppId
if (decoded.hostAppId !== 'revoland-mobile') {
res.status(403).json({ error: 'Invalid host app' });
return;
}
req.subAppAuth = decoded;
next();
} catch (error) {
console.error('Token verification error:', error);
res.status(401).json({ error: 'Invalid token' });
}
};
export const requirePermission = (permission: string) => {
return (req: SubAppAuthRequest, res: Response, next: NextFunction): void => {
if (!req.subAppAuth) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
const { permissions } = req.subAppAuth;
if (!permissions.includes(permission) && !permissions.includes('*')) {
res.status(403).json({ error: `Missing required permission: ${permission}` });
return;
}
next();
};
};
```
```typescript
// src/routes/properties.routes.ts
import { Router } from 'express';
import { SubAppAuthRequest, authenticateSubApp, requirePermission } from '../middleware/subapp-auth.middleware';
const router = Router();
// Mock property data
const properties = [
{
id: 'prop-001',
title: 'Modern Downtown Condo',
address: '123 Main St, Downtown',
price: 450000,
bedrooms: 2,
bathrooms: 2,
sqft: 1200,
imageUrl: 'https://cdn.revoland.com/properties/prop-001.jpg',
latitude: 40.7128,
longitude: -74.0060,
description: 'Beautiful modern condo in the heart of downtown',
},
{
id: 'prop-002',
title: 'Spacious Family Home',
address: '456 Oak Ave, Suburbia',
price: 650000,
bedrooms: 4,
bathrooms: 3,
sqft: 2500,
imageUrl: 'https://cdn.revoland.com/properties/prop-002.jpg',
latitude: 40.7589,
longitude: -73.9851,
description: 'Perfect family home with large backyard',
},
];
// Get all properties
router.get(
'/',
authenticateSubApp,
requirePermission('read:properties'),
(req: SubAppAuthRequest, res) => {
try {
const { city, minPrice, maxPrice, minBedrooms } = req.query;
let filtered = [...properties];
if (city) {
filtered = filtered.filter((p) =>
p.address.toLowerCase().includes((city as string).toLowerCase())
);
}
if (minPrice) {
filtered = filtered.filter((p) => p.price >= Number(minPrice));
}
if (maxPrice) {
filtered = filtered.filter((p) => p.price <= Number(maxPrice));
}
if (minBedrooms) {
filtered = filtered.filter((p) => p.bedrooms >= Number(minBedrooms));
}
res.json({
success: true,
properties: filtered,
count: filtered.length,
userId: req.subAppAuth?.userId,
});
} catch (error) {
console.error('Error fetching properties:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
);
// Get property by ID
router.get(
'/:propertyId',
authenticateSubApp,
requirePermission('read:properties'),
(req: SubAppAuthRequest, res) => {
try {
const { propertyId } = req.params;
const property = properties.find((p) => p.id === propertyId);
if (!property) {
res.status(404).json({ error: 'Property not found' });
return;
}
res.json({
success: true,
property,
});
} catch (error) {
console.error('Error fetching property:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
);
export default router;
```
## 3. Workflow
1. User logs into RevoLand SuperApp
2. SuperApp calls Metadata API to get available SubApps and launch tokens
3. User selects a SubApp (e.g., "Property Listings")
4. SuperApp initializes WebView with SubApp URL and injects context with short-lived JWT
5. SubApp reads token and calls backend API to fetch property data
6. Backend validates JWT and returns data if valid
7. SubApp and SuperApp exchange messages via JS Bridge for native features
## 4. Security Mechanisms
### 4.1 Short-lived JWT Tokens
- Each SubApp launch generates a new JWT with 5-minute expiration
- Token contains: userId, hostAppId, subAppId, userRole, permissions, exp
- Tokens can be refreshed through Metadata API
### 4.2 Backend Validation
- All SubApp API calls require valid JWT
- Backend verifies signature, expiration, and hostAppId
- Rejects requests with expired or invalid tokens
### 4.3 Access Control
- CORS restricted to CDN domains only
- User-agent validation to confirm WebView origin
- Permission-based access to SubApps and features
### 4.4 Secure Communication
- HTTPS enforced for all communications
- No sensitive data embedded in SubApp source code
- Minimal data exposure through JS Bridge
## 5. Benefits
- **Fast Updates**: Deploy SubApp updates without republishing SuperApp
- **Independent Development**: Teams work on SubApps in isolation
- **Security**: Dynamic tokens and server-side validation
- **Flexibility**: Add/remove SubApps via metadata without code changes
- **Scalability**: CDN hosting for SubApps ensures global performance
## 6. Architecture Diagram
```
[User]
↓ Login
[RevoLand SuperApp - Flutter Native]
↓ HTTPS + Bearer Token
[Metadata API] → Returns SubApps + JWT tokens
Display SubApp Menu (Property Listings, Virtual Tours, etc.)
↓ User selects SubApp
[WebView] ↔ JS Bridge ↔ [SubApp - Flutter Web]
↓ API calls with Bearer JWT
[SubApp Backend API]
↓ Validate JWT → Return data
[User sees property listings, tours, calculators, etc.]
```
## 7. Example API Response
**Metadata API Response:**
```json
{
"success": true,
"subApps": [
{
"id": "property-listings",
"displayName": "Property Listings",
"description": "Browse available properties",
"iconUrl": "https://cdn.revoland.com/icons/listings.png",
"launchUrl": "https://cdn.revoland.com/subapps/property-listings/index.html",
"signedToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"permissions": ["read:properties", "search:properties"],
"version": "1.2.0",
"minHostVersion": "1.0.0"
},
{
"id": "virtual-tours",
"displayName": "Virtual Tours",
"description": "360° property tours",
"iconUrl": "https://cdn.revoland.com/icons/virtual-tour.png",
"launchUrl": "https://cdn.revoland.com/subapps/virtual-tours/index.html",
"signedToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"permissions": ["read:properties", "view:tours"],
"version": "2.0.1",
"minHostVersion": "1.0.0"
},
{
"id": "mortgage-calculator",
"displayName": "Mortgage Calculator",
"description": "Calculate mortgage payments",
"iconUrl": "https://cdn.revoland.com/icons/calculator.png",
"launchUrl": "https://cdn.revoland.com/subapps/mortgage-calculator/index.html",
"signedToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"permissions": ["calculate:mortgage"],
"version": "1.5.2",
"minHostVersion": "1.0.0"
},
{
"id": "agent-dashboard",
"displayName": "Agent Dashboard",
"description": "Manage your properties and leads",
"iconUrl": "https://cdn.revoland.com/icons/dashboard.png",
"launchUrl": "https://cdn.revoland.com/subapps/agent-dashboard/index.html",
"signedToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"permissions": ["manage:properties", "manage:leads", "view:analytics"],
"version": "3.1.0",
"minHostVersion": "1.0.0"
}
],
"timestamp": "2025-10-23T10:30:00Z"
}
```
**SubApp API Response (Properties):**
```json
{
"success": true,
"properties": [
{
"id": "prop-001",
"title": "Modern Downtown Condo",
"address": "123 Main St, Downtown",
"price": 450000,
"bedrooms": 2,
"bathrooms": 2,
"sqft": 1200,
"imageUrl": "https://cdn.revoland.com/properties/prop-001.jpg",
"latitude": 40.7128,
"longitude": -74.0060,
"description": "Beautiful modern condo in the heart of downtown"
},
{
"id": "prop-002",
"title": "Spacious Family Home",
"address": "456 Oak Ave, Suburbia",
"price": 650000,
"bedrooms": 4,
"bathrooms": 3,
"sqft": 2500,
"imageUrl": "https://cdn.revoland.com/properties/prop-002.jpg",
"latitude": 40.7589,
"longitude": -73.9851,
"description": "Perfect family home with large backyard"
}
],
"count": 2,
"userId": "user-123"
}
```
---
**Document Version:** 1.0
**Last Updated:** October 23, 2025
**Project:** RevoLand - Real Estate SuperApp Platform