diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 0112cdf..0acd3e5 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -84,6 +84,9 @@ PODS:
- FlutterMacOS
- url_launcher_ios (0.0.1):
- Flutter
+ - webview_flutter_wkwebview (0.0.1):
+ - Flutter
+ - FlutterMacOS
DEPENDENCIES:
- emoji_picker_flutter (from `.symlinks/plugins/emoji_picker_flutter/ios`)
@@ -97,6 +100,7 @@ DEPENDENCIES:
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
+ - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
SPEC REPOS:
trunk:
@@ -133,6 +137,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
+ webview_flutter_wkwebview:
+ :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
SPEC CHECKSUMS:
emoji_picker_flutter: 8e50ec5caac456a23a78637e02c6293ea0ac8771
@@ -155,6 +161,7 @@ SPEC CHECKSUMS:
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa
+ webview_flutter_wkwebview: 29eb20d43355b48fe7d07113835b9128f84e3af4
PODFILE CHECKSUM: f8c2dcdfb50bb67645580d28a6bf814fca30bdec
diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift
index 60775fd..6266644 100644
--- a/ios/Runner/AppDelegate.swift
+++ b/ios/Runner/AppDelegate.swift
@@ -1,47 +1,13 @@
import Flutter
import UIKit
-import FirebaseCore
-import FirebaseMessaging
@main
-@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
+@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
- FirebaseApp.configure()
-
- // Request notification permissions
- UNUserNotificationCenter.current().delegate = self
- let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
- UNUserNotificationCenter.current().requestAuthorization(options: authOptions) { _, _ in }
- application.registerForRemoteNotifications()
-
- Messaging.messaging().delegate = self
-
+ GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
-
- func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
- GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
- }
-
- override func application(
- _ application: UIApplication,
- didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
- ) {
- Messaging.messaging().apnsToken = deviceToken
- super.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
- }
-}
-
-extension AppDelegate: MessagingDelegate {
- func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
- let dataDict: [String: String] = ["token": fcmToken ?? ""]
- NotificationCenter.default.post(
- name: Notification.Name("FCMToken"),
- object: nil,
- userInfo: dataDict
- )
- }
}
diff --git a/ios/Runner/GoogleService-Info.plist b/ios/Runner/GoogleService-Info.plist
index fcf5ca2..4421dcb 100644
--- a/ios/Runner/GoogleService-Info.plist
+++ b/ios/Runner/GoogleService-Info.plist
@@ -9,7 +9,7 @@
PLIST_VERSION
1
BUNDLE_ID
- com.requestn.realEstateMobile.dev
+ com.requestn.realEstateMobile.local
PROJECT_ID
real-estate-2d71e
STORAGE_BUCKET
@@ -25,6 +25,6 @@
IS_SIGNIN_ENABLED
GOOGLE_APP_ID
- 1:703616926518:ios:301cac94994fbdc3fca997
+ 1:703616926518:ios:15652d299f9f2d7ffca997
\ No newline at end of file
diff --git a/lib/config/firebase_config.dart b/lib/config/firebase_config.dart
new file mode 100644
index 0000000..7f787a1
--- /dev/null
+++ b/lib/config/firebase_config.dart
@@ -0,0 +1,48 @@
+import 'package:firebase_core/firebase_core.dart';
+import 'package:real_estate_mobile/config/app_config.dart';
+
+class FirebaseConfig {
+ static FirebaseOptions get currentOptions {
+ switch (AppConfig.flavor) {
+ case Flavor.dev:
+ return _dev;
+ case Flavor.demo:
+ return _demo;
+ case Flavor.local:
+ return _local;
+ }
+ }
+
+ // ── Dev ──
+ static const _dev = FirebaseOptions(
+ apiKey: 'AIzaSyAwb_9fc7n9gbN_IQqWeYRkHSSykzVHCyc', // Android
+ appId: '1:703616926518:android:4342b796fbabb690fca997',
+ messagingSenderId: '703616926518',
+ projectId: 'real-estate-2d71e',
+ storageBucket: 'real-estate-2d71e.firebasestorage.app',
+ iosBundleId: 'com.requestn.realEstateMobile.dev',
+ iosClientId: null,
+ );
+
+ // ── Demo ──
+ static const _demo = FirebaseOptions(
+ apiKey: 'AIzaSyAwb_9fc7n9gbN_IQqWeYRkHSSykzVHCyc',
+ appId: '1:703616926518:android:270639d6887d2cb4fca997',
+ messagingSenderId: '703616926518',
+ projectId: 'real-estate-2d71e',
+ storageBucket: 'real-estate-2d71e.firebasestorage.app',
+ iosBundleId: 'com.requestn.realEstateMobile.demo',
+ iosClientId: null,
+ );
+
+ // ── Local ──
+ static const _local = FirebaseOptions(
+ apiKey: 'AIzaSyAwb_9fc7n9gbN_IQqWeYRkHSSykzVHCyc',
+ appId: '1:703616926518:android:2eb242f07b174c25fca997',
+ messagingSenderId: '703616926518',
+ projectId: 'real-estate-2d71e',
+ storageBucket: 'real-estate-2d71e.firebasestorage.app',
+ iosBundleId: 'com.requestn.realEstateMobile.local',
+ iosClientId: null,
+ );
+}
diff --git a/lib/core/services/push_notification_service.dart b/lib/core/services/push_notification_service.dart
index 5cb25f3..be2280a 100644
--- a/lib/core/services/push_notification_service.dart
+++ b/lib/core/services/push_notification_service.dart
@@ -19,9 +19,8 @@ class PushNotificationService {
factory PushNotificationService() => _instance;
PushNotificationService._internal();
- final FirebaseMessaging _messaging = FirebaseMessaging.instance;
- final FlutterLocalNotificationsPlugin _localNotifications =
- FlutterLocalNotificationsPlugin();
+ FirebaseMessaging? _messaging;
+ FlutterLocalNotificationsPlugin? _localNotifications;
final NotificationRepository _repo = NotificationRepository();
String? _currentToken;
@@ -31,15 +30,26 @@ class PushNotificationService {
void Function(String? actionUrl)? onNotificationTap;
/// Initialize Firebase Messaging and local notifications.
+ /// Skipped on iOS until Firebase iOS app is configured.
Future initialize() async {
if (_initialized) return;
+
+ // Skip on iOS — no Firebase iOS app configured yet
+ if (Platform.isIOS) {
+ debugPrint('[FCM] Skipped on iOS — no Firebase iOS app configured');
+ return;
+ }
+
_initialized = true;
+ _messaging = FirebaseMessaging.instance;
+ _localNotifications = FlutterLocalNotificationsPlugin();
+
// Register background handler
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
// Request permission
- final settings = await _messaging.requestPermission(
+ final settings = await _messaging!.requestPermission(
alert: true,
badge: true,
sound: true,
@@ -58,7 +68,7 @@ class PushNotificationService {
await _getAndRegisterToken();
// Listen for token refresh
- _messaging.onTokenRefresh.listen(_onTokenRefresh);
+ _messaging!.onTokenRefresh.listen(_onTokenRefresh);
// Handle foreground messages
FirebaseMessaging.onMessage.listen(_onForegroundMessage);
@@ -67,14 +77,14 @@ class PushNotificationService {
FirebaseMessaging.onMessageOpenedApp.listen(_onMessageOpenedApp);
// Check if app was opened from a terminated state notification
- final initialMessage = await _messaging.getInitialMessage();
+ final initialMessage = await _messaging!.getInitialMessage();
if (initialMessage != null) {
_onMessageOpenedApp(initialMessage);
}
// iOS: set foreground notification presentation options
if (Platform.isIOS) {
- await _messaging.setForegroundNotificationPresentationOptions(
+ await _messaging!.setForegroundNotificationPresentationOptions(
alert: true,
badge: true,
sound: true,
@@ -90,7 +100,7 @@ class PushNotificationService {
importance: Importance.high,
);
- await _localNotifications
+ await _localNotifications!
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(androidChannel);
@@ -103,7 +113,7 @@ class PushNotificationService {
requestSoundPermission: false,
);
- await _localNotifications.initialize(
+ await _localNotifications!.initialize(
const InitializationSettings(
android: androidSettings,
iOS: iosSettings,
@@ -117,7 +127,7 @@ class PushNotificationService {
Future _getAndRegisterToken() async {
try {
- final token = await _messaging.getToken();
+ final token = await _messaging!.getToken();
if (token != null) {
_currentToken = token;
debugPrint('[FCM] Token: $token');
@@ -151,7 +161,7 @@ class PushNotificationService {
// Show local notification on Android (iOS handles it natively)
if (Platform.isAndroid) {
- _localNotifications.show(
+ _localNotifications?.show(
notification.hashCode,
notification.title,
notification.body,
@@ -178,7 +188,7 @@ class PushNotificationService {
/// Unregister FCM token from backend (call on logout).
Future unregister() async {
- if (_currentToken == null) return;
+ if (Platform.isIOS || _currentToken == null) return;
try {
await _repo.removeFcmToken(_currentToken!);
debugPrint('[FCM] Token unregistered from backend');
@@ -190,6 +200,7 @@ class PushNotificationService {
/// Re-register token after login.
Future registerAfterLogin() async {
+ if (Platform.isIOS) return;
await _getAndRegisterToken();
}
}
diff --git a/lib/features/agents/presentation/screens/agent_edit_profile_screen.dart b/lib/features/agents/presentation/screens/agent_edit_profile_screen.dart
new file mode 100644
index 0000000..6229aff
--- /dev/null
+++ b/lib/features/agents/presentation/screens/agent_edit_profile_screen.dart
@@ -0,0 +1,1599 @@
+import 'dart:convert';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:go_router/go_router.dart';
+import 'package:intl/intl.dart';
+import 'package:real_estate_mobile/core/constants/app_colors.dart';
+import 'package:real_estate_mobile/features/profile/data/profile_repository.dart';
+
+// ---------------------------------------------------------------------------
+// Data loading provider
+// ---------------------------------------------------------------------------
+
+/// Bundles all three API calls needed to render the edit-profile form.
+final _editProfileDataProvider =
+ FutureProvider.autoDispose<_EditProfileData>((ref) async {
+ final repo = ProfileRepository();
+
+ // 1. Fetch agent profile (need agentTypeId + pre-fill values)
+ final profile = await repo.getMyProfile('AGENT');
+
+ // 2. Resolve agentTypeId
+ final agentTypeId = profile['agentTypeId'] as String? ??
+ (profile['agentType'] as Map?)?['id'] as String?;
+ if (agentTypeId == null || agentTypeId.isEmpty) {
+ throw Exception('Agent type not found on profile');
+ }
+
+ // 3 + 4. Fetch sections & field-values in parallel
+ final results = await Future.wait([
+ repo.getSectionsForAgentType(agentTypeId),
+ repo.getFieldValues(),
+ ]);
+
+ // getSectionsForAgentType returns { agentType: {...}, sections: [...] }
+ final sectionsResponse = results[0];
+ final sections = (sectionsResponse['sections'] as List?) ?? [];
+
+ // getFieldValues returns { agentProfileId: "...", fieldValues: [...] }
+ final fieldValuesResponse = results[1];
+ final fieldValues =
+ (fieldValuesResponse['fieldValues'] as List?) ?? [];
+
+ return _EditProfileData(
+ profile: profile,
+ sections: sections,
+ fieldValues: fieldValues,
+ );
+});
+
+class _EditProfileData {
+ final Map profile;
+ final List sections;
+ final List fieldValues;
+ const _EditProfileData({
+ required this.profile,
+ required this.sections,
+ required this.fieldValues,
+ });
+}
+
+// ---------------------------------------------------------------------------
+// Screen
+// ---------------------------------------------------------------------------
+
+class AgentEditProfileScreen extends ConsumerStatefulWidget {
+ const AgentEditProfileScreen({super.key});
+
+ @override
+ ConsumerState createState() =>
+ _AgentEditProfileScreenState();
+}
+
+class _AgentEditProfileScreenState
+ extends ConsumerState {
+ /// Master form data keyed by field slug.
+ final Map _formData = {};
+
+ /// Repeatable section entries keyed by section slug.
+ final Map>> _repeatableEntries = {};
+
+ /// Tag-input controllers keyed by field slug.
+ final Map _tagControllers = {};
+
+ /// All text controllers for cleanup.
+ final List _controllers = [];
+
+ bool _saving = false;
+ bool _initialized = false;
+
+ final _formKey = GlobalKey();
+
+ // Pre-fill slug mapping to profile keys
+ static const _profileSlugMap = {
+ 'first-name': 'firstName',
+ 'first_name': 'firstName',
+ 'last-name': 'lastName',
+ 'last_name': 'lastName',
+ 'email': 'email',
+ 'bio': 'bio',
+ 'license-number': 'licenseNumber',
+ 'license_number': 'licenseNumber',
+ 'experience': 'experience',
+ 'specializations': 'specializations',
+ 'languages': 'languages',
+ 'service-areas': 'serviceAreas',
+ 'service_areas': 'serviceAreas',
+ };
+
+ @override
+ void dispose() {
+ for (final c in _controllers) {
+ c.dispose();
+ }
+ for (final c in _tagControllers.values) {
+ c.dispose();
+ }
+ super.dispose();
+ }
+
+ // ---------------------------------------------------------------------------
+ // Initialization
+ // ---------------------------------------------------------------------------
+
+ void _initializeFormData(_EditProfileData data) {
+ if (_initialized) return;
+ _initialized = true;
+
+ final profile = data.profile;
+
+ // Build field-value lookup by slug
+ final fvMap = {};
+ for (final fv in data.fieldValues) {
+ if (fv is Map) {
+ final slug = fv['fieldSlug'] as String? ?? '';
+ if (slug.isNotEmpty) {
+ fvMap[slug] = fv['value'];
+ }
+ }
+ }
+
+ // Walk sections & fields
+ for (final section in data.sections) {
+ if (section is! Map) continue;
+ final isRepeatable = section['isRepeatable'] == true;
+ final sectionSlug = section['slug'] as String? ?? '';
+ final fields = section['fields'] as List? ?? [];
+
+ if (isRepeatable) {
+ // Load existing repeatable entries from field values
+ final entriesKey = '${sectionSlug}_entries';
+ final existingEntries = fvMap[entriesKey];
+ if (existingEntries is List && existingEntries.isNotEmpty) {
+ _repeatableEntries[sectionSlug] = existingEntries
+ .map((e) => e is Map
+ ? Map.from(e)
+ : {})
+ .toList();
+ } else {
+ _repeatableEntries[sectionSlug] = [{}];
+ }
+ }
+
+ for (final field in fields) {
+ if (field is! Map) continue;
+ final slug = field['slug'] as String? ?? '';
+ final fieldType = field['fieldType'] as String? ?? 'TEXT';
+ if (slug.isEmpty) continue;
+
+ // Priority: fieldValues API > profile pre-fill > defaultValue
+ dynamic value;
+
+ // 1. Field values from API
+ if (fvMap.containsKey(slug)) {
+ value = fvMap[slug];
+ }
+ // 2. Profile pre-fill
+ else if (_profileSlugMap.containsKey(slug)) {
+ value = profile[_profileSlugMap[slug]!];
+ }
+ // 3. Default value
+ else {
+ value = field['defaultValue'];
+ }
+
+ // Normalize value based on field type
+ if (!isRepeatable) {
+ _formData[slug] = _normalizeValue(value, fieldType, field);
+ }
+ }
+ }
+ }
+
+ dynamic _normalizeValue(
+ dynamic value, String fieldType, Map field) {
+ switch (fieldType) {
+ case 'CHECKBOX':
+ if (value is bool) return value;
+ if (value is String) return value.toLowerCase() == 'true';
+ return false;
+ case 'CHECKBOX_GROUP':
+ case 'MULTI_SELECT':
+ case 'TAG_INPUT':
+ if (value is List) return List.from(value.map((e) => '$e'));
+ if (value is String && value.isNotEmpty) {
+ try {
+ final decoded = jsonDecode(value);
+ if (decoded is List) {
+ return List.from(decoded.map((e) => '$e'));
+ }
+ } catch (_) {}
+ return [value];
+ }
+ return [];
+ case 'NUMBER':
+ if (value is num) return value;
+ if (value is String) return num.tryParse(value);
+ return null;
+ case 'RANGE':
+ if (value is Map) return value;
+ if (value is String && value.isNotEmpty) {
+ try {
+ return jsonDecode(value);
+ } catch (_) {}
+ }
+ final rangeConfig = field['rangeConfig'] as Map?;
+ final min = (rangeConfig?['min'] as num?)?.toDouble() ?? 0;
+ final max = (rangeConfig?['max'] as num?)?.toDouble() ?? 100;
+ return {'min': min, 'max': max};
+ default:
+ return value?.toString() ?? '';
+ }
+ }
+
+ // ---------------------------------------------------------------------------
+ // Save
+ // ---------------------------------------------------------------------------
+
+ Future _save(_EditProfileData data) async {
+ if (!(_formKey.currentState?.validate() ?? false)) return;
+
+ setState(() => _saving = true);
+
+ try {
+ final repo = ProfileRepository();
+ final fieldValuesToSave =