feat: implement Stripe checkout flow, update profile settings UI for subscriptions, and add agent profile editing.
This commit is contained in:
@@ -84,6 +84,9 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- url_launcher_ios (0.0.1):
|
- url_launcher_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- webview_flutter_wkwebview (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- emoji_picker_flutter (from `.symlinks/plugins/emoji_picker_flutter/ios`)
|
- 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`)
|
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
|
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
trunk:
|
trunk:
|
||||||
@@ -133,6 +137,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||||
|
webview_flutter_wkwebview:
|
||||||
|
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
emoji_picker_flutter: 8e50ec5caac456a23a78637e02c6293ea0ac8771
|
emoji_picker_flutter: 8e50ec5caac456a23a78637e02c6293ea0ac8771
|
||||||
@@ -155,6 +161,7 @@ SPEC CHECKSUMS:
|
|||||||
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
|
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
|
||||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||||
url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa
|
url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa
|
||||||
|
webview_flutter_wkwebview: 29eb20d43355b48fe7d07113835b9128f84e3af4
|
||||||
|
|
||||||
PODFILE CHECKSUM: f8c2dcdfb50bb67645580d28a6bf814fca30bdec
|
PODFILE CHECKSUM: f8c2dcdfb50bb67645580d28a6bf814fca30bdec
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +1,13 @@
|
|||||||
import Flutter
|
import Flutter
|
||||||
import UIKit
|
import UIKit
|
||||||
import FirebaseCore
|
|
||||||
import FirebaseMessaging
|
|
||||||
|
|
||||||
@main
|
@main
|
||||||
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
override func application(
|
override func application(
|
||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
FirebaseApp.configure()
|
GeneratedPluginRegistrant.register(with: self)
|
||||||
|
|
||||||
// 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
|
|
||||||
|
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<key>PLIST_VERSION</key>
|
<key>PLIST_VERSION</key>
|
||||||
<string>1</string>
|
<string>1</string>
|
||||||
<key>BUNDLE_ID</key>
|
<key>BUNDLE_ID</key>
|
||||||
<string>com.requestn.realEstateMobile.dev</string>
|
<string>com.requestn.realEstateMobile.local</string>
|
||||||
<key>PROJECT_ID</key>
|
<key>PROJECT_ID</key>
|
||||||
<string>real-estate-2d71e</string>
|
<string>real-estate-2d71e</string>
|
||||||
<key>STORAGE_BUCKET</key>
|
<key>STORAGE_BUCKET</key>
|
||||||
@@ -25,6 +25,6 @@
|
|||||||
<key>IS_SIGNIN_ENABLED</key>
|
<key>IS_SIGNIN_ENABLED</key>
|
||||||
<true></true>
|
<true></true>
|
||||||
<key>GOOGLE_APP_ID</key>
|
<key>GOOGLE_APP_ID</key>
|
||||||
<string>1:703616926518:ios:301cac94994fbdc3fca997</string>
|
<string>1:703616926518:ios:15652d299f9f2d7ffca997</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
48
lib/config/firebase_config.dart
Normal file
48
lib/config/firebase_config.dart
Normal file
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -19,9 +19,8 @@ class PushNotificationService {
|
|||||||
factory PushNotificationService() => _instance;
|
factory PushNotificationService() => _instance;
|
||||||
PushNotificationService._internal();
|
PushNotificationService._internal();
|
||||||
|
|
||||||
final FirebaseMessaging _messaging = FirebaseMessaging.instance;
|
FirebaseMessaging? _messaging;
|
||||||
final FlutterLocalNotificationsPlugin _localNotifications =
|
FlutterLocalNotificationsPlugin? _localNotifications;
|
||||||
FlutterLocalNotificationsPlugin();
|
|
||||||
final NotificationRepository _repo = NotificationRepository();
|
final NotificationRepository _repo = NotificationRepository();
|
||||||
|
|
||||||
String? _currentToken;
|
String? _currentToken;
|
||||||
@@ -31,15 +30,26 @@ class PushNotificationService {
|
|||||||
void Function(String? actionUrl)? onNotificationTap;
|
void Function(String? actionUrl)? onNotificationTap;
|
||||||
|
|
||||||
/// Initialize Firebase Messaging and local notifications.
|
/// Initialize Firebase Messaging and local notifications.
|
||||||
|
/// Skipped on iOS until Firebase iOS app is configured.
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
if (_initialized) return;
|
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;
|
_initialized = true;
|
||||||
|
|
||||||
|
_messaging = FirebaseMessaging.instance;
|
||||||
|
_localNotifications = FlutterLocalNotificationsPlugin();
|
||||||
|
|
||||||
// Register background handler
|
// Register background handler
|
||||||
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
|
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
|
||||||
|
|
||||||
// Request permission
|
// Request permission
|
||||||
final settings = await _messaging.requestPermission(
|
final settings = await _messaging!.requestPermission(
|
||||||
alert: true,
|
alert: true,
|
||||||
badge: true,
|
badge: true,
|
||||||
sound: true,
|
sound: true,
|
||||||
@@ -58,7 +68,7 @@ class PushNotificationService {
|
|||||||
await _getAndRegisterToken();
|
await _getAndRegisterToken();
|
||||||
|
|
||||||
// Listen for token refresh
|
// Listen for token refresh
|
||||||
_messaging.onTokenRefresh.listen(_onTokenRefresh);
|
_messaging!.onTokenRefresh.listen(_onTokenRefresh);
|
||||||
|
|
||||||
// Handle foreground messages
|
// Handle foreground messages
|
||||||
FirebaseMessaging.onMessage.listen(_onForegroundMessage);
|
FirebaseMessaging.onMessage.listen(_onForegroundMessage);
|
||||||
@@ -67,14 +77,14 @@ class PushNotificationService {
|
|||||||
FirebaseMessaging.onMessageOpenedApp.listen(_onMessageOpenedApp);
|
FirebaseMessaging.onMessageOpenedApp.listen(_onMessageOpenedApp);
|
||||||
|
|
||||||
// Check if app was opened from a terminated state notification
|
// Check if app was opened from a terminated state notification
|
||||||
final initialMessage = await _messaging.getInitialMessage();
|
final initialMessage = await _messaging!.getInitialMessage();
|
||||||
if (initialMessage != null) {
|
if (initialMessage != null) {
|
||||||
_onMessageOpenedApp(initialMessage);
|
_onMessageOpenedApp(initialMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
// iOS: set foreground notification presentation options
|
// iOS: set foreground notification presentation options
|
||||||
if (Platform.isIOS) {
|
if (Platform.isIOS) {
|
||||||
await _messaging.setForegroundNotificationPresentationOptions(
|
await _messaging!.setForegroundNotificationPresentationOptions(
|
||||||
alert: true,
|
alert: true,
|
||||||
badge: true,
|
badge: true,
|
||||||
sound: true,
|
sound: true,
|
||||||
@@ -90,7 +100,7 @@ class PushNotificationService {
|
|||||||
importance: Importance.high,
|
importance: Importance.high,
|
||||||
);
|
);
|
||||||
|
|
||||||
await _localNotifications
|
await _localNotifications!
|
||||||
.resolvePlatformSpecificImplementation<
|
.resolvePlatformSpecificImplementation<
|
||||||
AndroidFlutterLocalNotificationsPlugin>()
|
AndroidFlutterLocalNotificationsPlugin>()
|
||||||
?.createNotificationChannel(androidChannel);
|
?.createNotificationChannel(androidChannel);
|
||||||
@@ -103,7 +113,7 @@ class PushNotificationService {
|
|||||||
requestSoundPermission: false,
|
requestSoundPermission: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
await _localNotifications.initialize(
|
await _localNotifications!.initialize(
|
||||||
const InitializationSettings(
|
const InitializationSettings(
|
||||||
android: androidSettings,
|
android: androidSettings,
|
||||||
iOS: iosSettings,
|
iOS: iosSettings,
|
||||||
@@ -117,7 +127,7 @@ class PushNotificationService {
|
|||||||
|
|
||||||
Future<void> _getAndRegisterToken() async {
|
Future<void> _getAndRegisterToken() async {
|
||||||
try {
|
try {
|
||||||
final token = await _messaging.getToken();
|
final token = await _messaging!.getToken();
|
||||||
if (token != null) {
|
if (token != null) {
|
||||||
_currentToken = token;
|
_currentToken = token;
|
||||||
debugPrint('[FCM] Token: $token');
|
debugPrint('[FCM] Token: $token');
|
||||||
@@ -151,7 +161,7 @@ class PushNotificationService {
|
|||||||
|
|
||||||
// Show local notification on Android (iOS handles it natively)
|
// Show local notification on Android (iOS handles it natively)
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
_localNotifications.show(
|
_localNotifications?.show(
|
||||||
notification.hashCode,
|
notification.hashCode,
|
||||||
notification.title,
|
notification.title,
|
||||||
notification.body,
|
notification.body,
|
||||||
@@ -178,7 +188,7 @@ class PushNotificationService {
|
|||||||
|
|
||||||
/// Unregister FCM token from backend (call on logout).
|
/// Unregister FCM token from backend (call on logout).
|
||||||
Future<void> unregister() async {
|
Future<void> unregister() async {
|
||||||
if (_currentToken == null) return;
|
if (Platform.isIOS || _currentToken == null) return;
|
||||||
try {
|
try {
|
||||||
await _repo.removeFcmToken(_currentToken!);
|
await _repo.removeFcmToken(_currentToken!);
|
||||||
debugPrint('[FCM] Token unregistered from backend');
|
debugPrint('[FCM] Token unregistered from backend');
|
||||||
@@ -190,6 +200,7 @@ class PushNotificationService {
|
|||||||
|
|
||||||
/// Re-register token after login.
|
/// Re-register token after login.
|
||||||
Future<void> registerAfterLogin() async {
|
Future<void> registerAfterLogin() async {
|
||||||
|
if (Platform.isIOS) return;
|
||||||
await _getAndRegisterToken();
|
await _getAndRegisterToken();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@ import 'package:real_estate_mobile/core/widgets/s3_image.dart';
|
|||||||
import 'package:real_estate_mobile/features/agents/data/models/agent_profile.dart';
|
import 'package:real_estate_mobile/features/agents/data/models/agent_profile.dart';
|
||||||
import 'package:real_estate_mobile/features/agents/presentation/providers/agent_detail_provider.dart';
|
import 'package:real_estate_mobile/features/agents/presentation/providers/agent_detail_provider.dart';
|
||||||
import 'package:real_estate_mobile/features/profile/data/profile_repository.dart';
|
import 'package:real_estate_mobile/features/profile/data/profile_repository.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
/// Provider that fetches the agent's own profile ID from /agents/profile/me
|
/// Provider that fetches the agent's own profile ID from /agents/profile/me
|
||||||
final _agentMyProfileProvider =
|
final _agentMyProfileProvider =
|
||||||
@@ -578,7 +579,7 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> {
|
|||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
onTap: () {},
|
onTap: () => context.push('/agent/edit-profile'),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(4),
|
padding: const EdgeInsets.all(4),
|
||||||
child: SvgPicture.asset(
|
child: SvgPicture.asset(
|
||||||
|
|||||||
@@ -299,6 +299,53 @@ class ProfileRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get sections for an agent type: GET /profile-sections/agent-type/{agentTypeId}
|
||||||
|
/// Returns { agentType: {...}, sections: [...] }
|
||||||
|
Future<Map<String, dynamic>> getSectionsForAgentType(
|
||||||
|
String agentTypeId) async {
|
||||||
|
try {
|
||||||
|
final response =
|
||||||
|
await _dio.get('/profile-sections/agent-type/$agentTypeId');
|
||||||
|
return response.data['data'] as Map<String, dynamic>;
|
||||||
|
} on DioException catch (e) {
|
||||||
|
if (e.error is ApiException) throw e.error as ApiException;
|
||||||
|
throw ApiException(
|
||||||
|
message: e.message ?? 'Failed to fetch profile sections',
|
||||||
|
statusCode: e.response?.statusCode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get field values: GET /agents/profile/field-values
|
||||||
|
/// Returns { agentProfileId: "...", fieldValues: [...] }
|
||||||
|
Future<Map<String, dynamic>> getFieldValues() async {
|
||||||
|
try {
|
||||||
|
final response = await _dio.get('/agents/profile/field-values');
|
||||||
|
return response.data['data'] as Map<String, dynamic>;
|
||||||
|
} on DioException catch (e) {
|
||||||
|
if (e.error is ApiException) throw e.error as ApiException;
|
||||||
|
throw ApiException(
|
||||||
|
message: e.message ?? 'Failed to fetch field values',
|
||||||
|
statusCode: e.response?.statusCode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save field values: PUT /agents/profile/field-values
|
||||||
|
Future<void> saveFieldValues(List<Map<String, dynamic>> fieldValues) async {
|
||||||
|
try {
|
||||||
|
await _dio.put('/agents/profile/field-values', data: {
|
||||||
|
'fieldValues': fieldValues,
|
||||||
|
});
|
||||||
|
} on DioException catch (e) {
|
||||||
|
if (e.error is ApiException) throw e.error as ApiException;
|
||||||
|
throw ApiException(
|
||||||
|
message: e.message ?? 'Failed to save field values',
|
||||||
|
statusCode: e.response?.statusCode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Cancel subscription: POST /stripe/cancel-subscription
|
/// Cancel subscription: POST /stripe/cancel-subscription
|
||||||
Future<void> cancelSubscription() async {
|
Future<void> cancelSubscription() async {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -0,0 +1,375 @@
|
|||||||
|
import 'package:flutter/material.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';
|
||||||
|
|
||||||
|
class PaymentSuccessScreen extends ConsumerStatefulWidget {
|
||||||
|
final String? sessionId;
|
||||||
|
|
||||||
|
const PaymentSuccessScreen({super.key, this.sessionId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<PaymentSuccessScreen> createState() =>
|
||||||
|
_PaymentSuccessScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PaymentSuccessScreenState extends ConsumerState<PaymentSuccessScreen> {
|
||||||
|
Map<String, dynamic>? _subscription;
|
||||||
|
bool _isLoading = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadSubscription();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadSubscription() async {
|
||||||
|
try {
|
||||||
|
final repo = ProfileRepository();
|
||||||
|
final sub = await repo.getSubscription();
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_subscription = sub;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
if (mounted) setState(() => _isLoading = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatDate(String? dateStr) {
|
||||||
|
if (dateStr == null) return 'N/A';
|
||||||
|
try {
|
||||||
|
final date = DateTime.parse(dateStr);
|
||||||
|
return DateFormat('MMM dd, yyyy').format(date);
|
||||||
|
} catch (_) {
|
||||||
|
return dateStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatAmount(dynamic amount) {
|
||||||
|
if (amount == null) return '\$499.00';
|
||||||
|
if (amount is int) return '\$${(amount / 100).toStringAsFixed(2)}';
|
||||||
|
if (amount is double) return '\$${(amount / 100).toStringAsFixed(2)}';
|
||||||
|
return '\$$amount';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
body: SafeArea(
|
||||||
|
child: _isLoading
|
||||||
|
? const Center(
|
||||||
|
child:
|
||||||
|
CircularProgressIndicator(color: AppColors.accentOrange))
|
||||||
|
: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.fromLTRB(24, 20, 24, 40),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Back button
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => context.go('/'),
|
||||||
|
child: const Icon(Icons.arrow_back,
|
||||||
|
color: AppColors.primaryDark),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Title
|
||||||
|
const Center(
|
||||||
|
child: Text(
|
||||||
|
'Payments',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'Fractul',
|
||||||
|
fontSize: 25,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppColors.primaryDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Center(
|
||||||
|
child: Text(
|
||||||
|
'Manage Your billing cycles , upgrade your\nplan , or boost individual listings',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'SourceSerif4',
|
||||||
|
fontSize: 14,
|
||||||
|
color: AppColors.primaryDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
|
||||||
|
// Success card
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 32, horizontal: 24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(
|
||||||
|
color: AppColors.primaryDark.withValues(alpha: 0.5),
|
||||||
|
width: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Success icon
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: AppColors.accentOrange, width: 2),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.check,
|
||||||
|
color: AppColors.accentOrange,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Payment Successful!
|
||||||
|
const Text(
|
||||||
|
'Payment Successful !',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'Fractul',
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppColors.primaryDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
const Text(
|
||||||
|
'Your annual membership is now active. You have\nfull access to all premium features',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'SourceSerif4',
|
||||||
|
fontSize: 14,
|
||||||
|
color: AppColors.primaryDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
// Transaction Details header
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Transaction Details',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'SourceSerif4',
|
||||||
|
fontSize: 14,
|
||||||
|
color: AppColors.primaryDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.verified_user,
|
||||||
|
size: 18,
|
||||||
|
color: Colors.green.shade600),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
const Text(
|
||||||
|
'Verified User',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'SourceSerif4',
|
||||||
|
fontSize: 14,
|
||||||
|
color: AppColors.primaryDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Transaction ID
|
||||||
|
_buildDetailRow(
|
||||||
|
'Transaction ID',
|
||||||
|
widget.sessionId != null &&
|
||||||
|
widget.sessionId!.length > 16
|
||||||
|
? 'REQ-${widget.sessionId!.substring(0, 8)}'
|
||||||
|
: 'REQ-${widget.sessionId ?? 'N/A'}',
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
|
||||||
|
// Amount Paid
|
||||||
|
_buildDetailRow(
|
||||||
|
'Amount Paid',
|
||||||
|
_formatAmount(
|
||||||
|
_subscription?['plan']?['amount']),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
|
||||||
|
// Date
|
||||||
|
_buildDetailRow(
|
||||||
|
'Date',
|
||||||
|
_formatDate(_subscription?['createdAt'] ??
|
||||||
|
DateTime.now().toIso8601String()),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
|
||||||
|
// Next Renewal Date
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Next Renewal Date',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'SourceSerif4',
|
||||||
|
fontSize: 14,
|
||||||
|
color: AppColors.primaryDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
_formatDate(
|
||||||
|
_subscription?['currentPeriodEnd']),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: 'SourceSerif4',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: AppColors.accentOrange,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Divider
|
||||||
|
Divider(
|
||||||
|
color:
|
||||||
|
AppColors.primaryDark.withValues(alpha: 0.2)),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Go To Dashboard button
|
||||||
|
SizedBox(
|
||||||
|
width: 200,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () => context.go('/'),
|
||||||
|
icon: const Icon(Icons.dashboard,
|
||||||
|
size: 18, color: Colors.white),
|
||||||
|
label: const Text(
|
||||||
|
'Go To Dashboard',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'Fractul',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColors.accentOrange,
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(15)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Download Receipt button
|
||||||
|
SizedBox(
|
||||||
|
width: 200,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
// TODO: implement receipt download
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content:
|
||||||
|
Text('Receipt download coming soon')),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.download,
|
||||||
|
size: 18, color: Colors.white),
|
||||||
|
label: const Text(
|
||||||
|
'Download Receipt',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'Fractul',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColors.accentOrange,
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(15)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Trust badges
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.verified_user,
|
||||||
|
size: 18, color: Colors.green.shade600),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
const Text(
|
||||||
|
'Verified User',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'SourceSerif4',
|
||||||
|
fontSize: 14,
|
||||||
|
color: AppColors.primaryDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 24),
|
||||||
|
const Icon(Icons.lock,
|
||||||
|
size: 18, color: AppColors.primaryDark),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
const Text(
|
||||||
|
'Secure Checkout',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'SourceSerif4',
|
||||||
|
fontSize: 14,
|
||||||
|
color: AppColors.primaryDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDetailRow(String label, String value) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'SourceSerif4',
|
||||||
|
fontSize: 14,
|
||||||
|
color: AppColors.primaryDark.withValues(alpha: 0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: 'SourceSerif4',
|
||||||
|
fontSize: 14,
|
||||||
|
color: AppColors.primaryDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import 'package:real_estate_mobile/core/widgets/s3_image.dart';
|
|||||||
import 'package:real_estate_mobile/features/profile/data/profile_repository.dart';
|
import 'package:real_estate_mobile/features/profile/data/profile_repository.dart';
|
||||||
import 'package:real_estate_mobile/features/profile/presentation/providers/profile_provider.dart';
|
import 'package:real_estate_mobile/features/profile/presentation/providers/profile_provider.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
import 'package:real_estate_mobile/features/profile/presentation/screens/stripe_checkout_screen.dart';
|
||||||
|
|
||||||
class ProfileSettingsScreen extends ConsumerStatefulWidget {
|
class ProfileSettingsScreen extends ConsumerStatefulWidget {
|
||||||
const ProfileSettingsScreen({super.key});
|
const ProfileSettingsScreen({super.key});
|
||||||
@@ -1078,117 +1079,173 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
|
|||||||
return ListView(
|
return ListView(
|
||||||
padding: const EdgeInsets.fromLTRB(24, 10, 24, 30),
|
padding: const EdgeInsets.fromLTRB(24, 10, 24, 30),
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
// Title section matching Figma
|
||||||
'Billings & Payments',
|
const Center(
|
||||||
style: TextStyle(
|
child: Text(
|
||||||
fontFamily: 'Fractul',
|
'Subscription & Payments',
|
||||||
fontSize: 15,
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w700,
|
fontFamily: 'Fractul',
|
||||||
color: AppColors.primaryDark,
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppColors.primaryDark,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 10),
|
||||||
const Text(
|
const Center(
|
||||||
'Manage your subscription and billing details.',
|
child: Text(
|
||||||
style: TextStyle(
|
'Manage Your billing cycles , upgrade your\nplan , or boost individual listings',
|
||||||
fontFamily: 'SourceSerif4',
|
textAlign: TextAlign.center,
|
||||||
fontSize: 14,
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w400,
|
fontFamily: 'SourceSerif4',
|
||||||
color: AppColors.primaryDark,
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
color: AppColors.primaryDark,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
if (hasActive) ...[
|
if (hasActive) ...[
|
||||||
|
// Section title
|
||||||
|
const Text(
|
||||||
|
'Current Plan',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'Fractul',
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppColors.primaryDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
// Active subscription card
|
// Active subscription card
|
||||||
_buildSectionCard(
|
Container(
|
||||||
title: 'Current Subscription',
|
padding: const EdgeInsets.all(20),
|
||||||
subtitle: 'Your active plan details',
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(
|
||||||
|
color: AppColors.primaryDark.withValues(alpha: 0.5),
|
||||||
|
width: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
// Annual Subscription badge
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.accentOrange,
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
plan?['name'] as String? ?? 'Annual Subscription',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: 'SourceSerif4',
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.check_circle,
|
const Icon(Icons.check_circle,
|
||||||
color: Color(0xFF4CAF50), size: 20),
|
color: Color(0xFF4CAF50), size: 20),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
const Text(
|
||||||
plan?['name'] as String? ?? 'Premium',
|
'Active',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'Fractul',
|
fontFamily: 'Fractul',
|
||||||
fontSize: 16,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w600,
|
||||||
color: AppColors.primaryDark,
|
color: Color(0xFF4CAF50),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Container(
|
if (_subscription?['currentPeriodEnd'] != null)
|
||||||
padding: const EdgeInsets.symmetric(
|
Text(
|
||||||
horizontal: 10, vertical: 4),
|
'Renews ${_formatDate(_subscription!['currentPeriodEnd'] as String)}',
|
||||||
decoration: BoxDecoration(
|
style: TextStyle(
|
||||||
color: const Color(0xFF4CAF50).withValues(alpha: 0.1),
|
fontFamily: 'SourceSerif4',
|
||||||
borderRadius: BorderRadius.circular(10),
|
fontSize: 13,
|
||||||
|
color: AppColors.primaryDark.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: const Text('Active',
|
|
||||||
style: TextStyle(
|
|
||||||
fontFamily: 'Fractul',
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Color(0xFF4CAF50))),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (plan?['amount'] != null) ...[
|
if (plan?['amount'] != null) ...[
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 16),
|
||||||
Text(
|
RichText(
|
||||||
'\$${plan!['amount']}/${plan['interval'] ?? 'year'}',
|
text: TextSpan(
|
||||||
style: const TextStyle(
|
children: [
|
||||||
fontFamily: 'Fractul',
|
TextSpan(
|
||||||
fontSize: 22,
|
text: '\$${plan!['amount']}',
|
||||||
fontWeight: FontWeight.w700,
|
style: const TextStyle(
|
||||||
color: AppColors.primaryDark,
|
fontFamily: 'Fractul',
|
||||||
),
|
fontSize: 35,
|
||||||
),
|
fontWeight: FontWeight.w700,
|
||||||
],
|
color: AppColors.primaryDark,
|
||||||
if (_subscription?['currentPeriodEnd'] != null) ...[
|
),
|
||||||
const SizedBox(height: 8),
|
),
|
||||||
Text(
|
const TextSpan(
|
||||||
'Renews on ${_formatDate(_subscription!['currentPeriodEnd'] as String)}',
|
text: ' / ',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'SourceSerif4',
|
fontFamily: 'Fractul',
|
||||||
fontSize: 13,
|
fontSize: 20,
|
||||||
color: AppColors.primaryDark.withValues(alpha: 0.6),
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppColors.primaryDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: plan['interval'] as String? ?? 'Year',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'Fractul',
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppColors.primaryDark.withValues(alpha: 0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
// Plan features
|
// Manage Subscription button
|
||||||
..._buildPlanFeatures(),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
// Manage Subscription
|
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: 155,
|
||||||
child: OutlinedButton(
|
child: ElevatedButton(
|
||||||
onPressed: _openBillingPortal,
|
onPressed: _openBillingPortal,
|
||||||
style: OutlinedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
foregroundColor: AppColors.primaryDark,
|
backgroundColor: AppColors.accentOrange,
|
||||||
side: BorderSide(
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
color: AppColors.primaryDark.withValues(alpha: 0.2)),
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(15)),
|
borderRadius: BorderRadius.circular(15)),
|
||||||
),
|
),
|
||||||
child: const Text('Manage Subscription',
|
child: const Text('Manage Subscription',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'Fractul',
|
fontFamily: 'Fractul',
|
||||||
fontSize: 14,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w600)),
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Colors.white)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 24),
|
||||||
|
// Include Features
|
||||||
|
const Text(
|
||||||
|
'Include Features',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'Fractul',
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppColors.primaryDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
..._buildPlanFeatures(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
// Cancel
|
// Cancel
|
||||||
SizedBox(
|
Center(
|
||||||
width: double.infinity,
|
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: _cancelSubscription,
|
onPressed: _cancelSubscription,
|
||||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||||
@@ -1203,97 +1260,202 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
] else ...[
|
] else ...[
|
||||||
// No subscription — upgrade card
|
// Section title
|
||||||
_buildSectionCard(
|
const Text(
|
||||||
title: 'Get Premium Access',
|
'Annual Membership Plans',
|
||||||
subtitle: 'Unlock all features with our premium plan',
|
style: TextStyle(
|
||||||
|
fontFamily: 'Fractul',
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppColors.primaryDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// Plan card matching Figma
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(
|
||||||
|
color: AppColors.primaryDark.withValues(alpha: 0.5),
|
||||||
|
width: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
// Annual Subscription badge
|
||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
padding:
|
||||||
padding: const EdgeInsets.all(20),
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.accentOrange.withValues(alpha: 0.05),
|
color: AppColors.accentOrange,
|
||||||
borderRadius: BorderRadius.circular(15),
|
borderRadius: BorderRadius.circular(15),
|
||||||
border: Border.all(
|
|
||||||
color: AppColors.accentOrange.withValues(alpha: 0.3)),
|
|
||||||
),
|
),
|
||||||
child: Column(
|
child: const Text(
|
||||||
|
'Annual Subscription',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'SourceSerif4',
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
// Professional Annual Plan
|
||||||
|
const Text(
|
||||||
|
'Professional Annual Plan',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'Fractul',
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppColors.primaryDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text(
|
||||||
|
'Complete solutions for real estate agents and lenders to grow their business.',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'SourceSerif4',
|
||||||
|
fontSize: 14,
|
||||||
|
color: AppColors.primaryDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
// Price
|
||||||
|
RichText(
|
||||||
|
text: TextSpan(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
const TextSpan(
|
||||||
padding: const EdgeInsets.symmetric(
|
text: '\$499',
|
||||||
horizontal: 12, vertical: 4),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppColors.accentOrange,
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
child: const Text('Best Value',
|
|
||||||
style: TextStyle(
|
|
||||||
fontFamily: 'Fractul',
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
color: Colors.white)),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
const Text(
|
|
||||||
'\$499/Year',
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'Fractul',
|
fontFamily: 'Fractul',
|
||||||
fontSize: 28,
|
fontSize: 35,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
color: AppColors.primaryDark,
|
color: AppColors.primaryDark,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const TextSpan(
|
||||||
..._buildPlanFeatures(),
|
text: ' / ',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'Fractul',
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppColors.primaryDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: 'Year',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'Fractul',
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppColors.primaryDark.withValues(alpha: 0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
// Get Premium Access button
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: 155,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: _startCheckout,
|
onPressed: _startCheckout,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColors.accentOrange,
|
backgroundColor: AppColors.accentOrange,
|
||||||
foregroundColor: AppColors.primaryDark,
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(15)),
|
borderRadius: BorderRadius.circular(15)),
|
||||||
),
|
),
|
||||||
child: const Text('Get Premium',
|
child: const Text('Get Premium Access',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'Fractul',
|
fontFamily: 'Fractul',
|
||||||
fontSize: 16,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w700)),
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Colors.white)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
// Include Features
|
||||||
|
const Text(
|
||||||
|
'Include Features',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'Fractul',
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppColors.primaryDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
..._buildPlanFeatures(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
// Need help section
|
|
||||||
|
// Need Help section matching Figma
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(15),
|
borderRadius: BorderRadius.circular(15),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: AppColors.primaryDark.withValues(alpha: 0.1)),
|
color: AppColors.primaryDark.withValues(alpha: 0.7),
|
||||||
|
width: 0.7,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.help_outline,
|
const Text(
|
||||||
color: AppColors.primaryDark.withValues(alpha: 0.6)),
|
'Need Help ?',
|
||||||
const SizedBox(width: 12),
|
style: TextStyle(
|
||||||
const Expanded(
|
fontFamily: 'Fractul',
|
||||||
child: Text(
|
fontSize: 20,
|
||||||
'Need help with billing? Contact support.',
|
fontWeight: FontWeight.w700,
|
||||||
style: TextStyle(
|
color: AppColors.primaryDark,
|
||||||
fontFamily: 'SourceSerif4',
|
),
|
||||||
fontSize: 13,
|
),
|
||||||
color: AppColors.primaryDark,
|
const SizedBox(height: 10),
|
||||||
),
|
const Text(
|
||||||
|
'Our billing experts are here to help you\nwith any questions about your plan.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'SourceSerif4',
|
||||||
|
fontSize: 14,
|
||||||
|
color: AppColors.primaryDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
final uri = Uri.parse('mailto:support@re-quest.co');
|
||||||
|
if (await canLaunchUrl(uri)) {
|
||||||
|
await launchUrl(uri);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'support@re-quest.co',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'SourceSerif4',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppColors.accentOrange,
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
decorationColor: AppColors.accentOrange,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Transform.rotate(
|
||||||
|
angle: 0.7, // ~40 degrees
|
||||||
|
child: const Icon(Icons.arrow_upward,
|
||||||
|
size: 16, color: AppColors.accentOrange),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -1312,12 +1474,21 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
|
|||||||
];
|
];
|
||||||
return features
|
return features
|
||||||
.map((f) => Padding(
|
.map((f) => Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.check_circle_outline,
|
Container(
|
||||||
size: 18, color: AppColors.accentOrange),
|
width: 22,
|
||||||
const SizedBox(width: 10),
|
height: 22,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: AppColors.accentOrange, width: 1.5),
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.check,
|
||||||
|
size: 14, color: AppColors.accentOrange),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
Text(f,
|
Text(f,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontFamily: 'SourceSerif4',
|
fontFamily: 'SourceSerif4',
|
||||||
@@ -1333,8 +1504,19 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
|
|||||||
try {
|
try {
|
||||||
final repo = ref.read(profileRepositoryProvider);
|
final repo = ref.read(profileRepositoryProvider);
|
||||||
final url = await repo.createPortalSession();
|
final url = await repo.createPortalSession();
|
||||||
if (await canLaunchUrl(Uri.parse(url))) {
|
if (!mounted) return;
|
||||||
await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
|
// Open billing portal in in-app browser
|
||||||
|
await Navigator.of(context).push<Map<String, dynamic>?>(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => StripeCheckoutScreen(
|
||||||
|
url: url,
|
||||||
|
title: 'Manage Subscription',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// Refresh subscription when user returns
|
||||||
|
if (mounted) {
|
||||||
|
_loadSubscription();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) _showSnackBar('Failed to open billing portal: $e', isError: true);
|
if (mounted) _showSnackBar('Failed to open billing portal: $e', isError: true);
|
||||||
@@ -1351,8 +1533,23 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
|
|||||||
}
|
}
|
||||||
final planId = plans.first['id'] as String;
|
final planId = plans.first['id'] as String;
|
||||||
final url = await repo.createCheckoutSession(planId);
|
final url = await repo.createCheckoutSession(planId);
|
||||||
if (await canLaunchUrl(Uri.parse(url))) {
|
if (!mounted) return;
|
||||||
await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
|
// Open Stripe checkout in in-app browser
|
||||||
|
final result = await Navigator.of(context).push<Map<String, dynamic>?>(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => StripeCheckoutScreen(
|
||||||
|
url: url,
|
||||||
|
title: 'Checkout',
|
||||||
|
successUrlPattern: 'confirmation',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// If checkout succeeded, navigate to payment success screen
|
||||||
|
if (result != null && result['success'] == true && mounted) {
|
||||||
|
final sessionId = result['sessionId'] as String?;
|
||||||
|
context.go('/payment-success${sessionId != null ? '?session_id=$sessionId' : ''}');
|
||||||
|
} else if (mounted) {
|
||||||
|
_loadSubscription();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) _showSnackBar('Failed to start checkout: $e', isError: true);
|
if (mounted) _showSnackBar('Failed to start checkout: $e', isError: true);
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:webview_flutter/webview_flutter.dart';
|
||||||
|
import 'package:real_estate_mobile/core/constants/app_colors.dart';
|
||||||
|
|
||||||
|
/// In-app WebView for Stripe Checkout / Billing Portal.
|
||||||
|
/// Pass [url] to load and [successUrlPattern] to detect completion.
|
||||||
|
class StripeCheckoutScreen extends StatefulWidget {
|
||||||
|
final String url;
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
/// URL pattern that indicates checkout success (e.g. your success redirect URL).
|
||||||
|
final String? successUrlPattern;
|
||||||
|
|
||||||
|
const StripeCheckoutScreen({
|
||||||
|
super.key,
|
||||||
|
required this.url,
|
||||||
|
this.title = 'Checkout',
|
||||||
|
this.successUrlPattern,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StripeCheckoutScreen> createState() => _StripeCheckoutScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StripeCheckoutScreenState extends State<StripeCheckoutScreen> {
|
||||||
|
late final WebViewController _controller;
|
||||||
|
bool _isLoading = true;
|
||||||
|
double _progress = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = WebViewController()
|
||||||
|
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||||
|
..setNavigationDelegate(
|
||||||
|
NavigationDelegate(
|
||||||
|
onPageStarted: (_) {
|
||||||
|
if (mounted) setState(() => _isLoading = true);
|
||||||
|
},
|
||||||
|
onPageFinished: (_) {
|
||||||
|
if (mounted) setState(() => _isLoading = false);
|
||||||
|
},
|
||||||
|
onProgress: (progress) {
|
||||||
|
if (mounted) setState(() => _progress = progress / 100);
|
||||||
|
},
|
||||||
|
onNavigationRequest: (request) {
|
||||||
|
// Detect success redirect
|
||||||
|
if (widget.successUrlPattern != null &&
|
||||||
|
request.url.contains(widget.successUrlPattern!)) {
|
||||||
|
// Extract session_id if present
|
||||||
|
final uri = Uri.parse(request.url);
|
||||||
|
final sessionId = uri.queryParameters['session_id'];
|
||||||
|
Navigator.of(context).pop({'success': true, 'sessionId': sessionId});
|
||||||
|
return NavigationDecision.prevent;
|
||||||
|
}
|
||||||
|
return NavigationDecision.navigate;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
..loadRequest(Uri.parse(widget.url));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.close, color: AppColors.primaryDark),
|
||||||
|
onPressed: () => Navigator.of(context).pop(null),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
widget.title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: 'Fractul',
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppColors.primaryDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
bottom: _isLoading
|
||||||
|
? PreferredSize(
|
||||||
|
preferredSize: const Size.fromHeight(3),
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: _progress,
|
||||||
|
backgroundColor: Colors.grey.shade200,
|
||||||
|
valueColor: const AlwaysStoppedAnimation<Color>(
|
||||||
|
AppColors.accentOrange),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
body: WebViewWidget(controller: _controller),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,13 +2,21 @@ import 'package:firebase_core/firebase_core.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:real_estate_mobile/config/app_config.dart';
|
import 'package:real_estate_mobile/config/app_config.dart';
|
||||||
|
import 'package:real_estate_mobile/config/firebase_config.dart';
|
||||||
import 'package:real_estate_mobile/core/theme/app_theme.dart';
|
import 'package:real_estate_mobile/core/theme/app_theme.dart';
|
||||||
import 'package:real_estate_mobile/routing/app_router.dart';
|
import 'package:real_estate_mobile/routing/app_router.dart';
|
||||||
|
|
||||||
Future<void> mainCommon(Flavor flavor) async {
|
Future<void> mainCommon(Flavor flavor) async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await AppConfig.init(flavor);
|
await AppConfig.init(flavor);
|
||||||
await Firebase.initializeApp();
|
|
||||||
|
// Initialize Firebase — iOS may fail if no iOS app configured in Firebase Console
|
||||||
|
try {
|
||||||
|
await Firebase.initializeApp(options: FirebaseConfig.currentOptions);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[Firebase] Init failed (expected on iOS without config): $e'); // ignore: avoid_print
|
||||||
|
}
|
||||||
|
|
||||||
runApp(const ProviderScope(child: MyApp()));
|
runApp(const ProviderScope(child: MyApp()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import 'package:real_estate_mobile/features/about/presentation/screens/about_scr
|
|||||||
import 'package:real_estate_mobile/features/agents/presentation/screens/agent_network_screen.dart';
|
import 'package:real_estate_mobile/features/agents/presentation/screens/agent_network_screen.dart';
|
||||||
import 'package:real_estate_mobile/features/profile/presentation/screens/profile_settings_screen.dart';
|
import 'package:real_estate_mobile/features/profile/presentation/screens/profile_settings_screen.dart';
|
||||||
import 'package:real_estate_mobile/features/notifications/presentation/screens/notifications_screen.dart';
|
import 'package:real_estate_mobile/features/notifications/presentation/screens/notifications_screen.dart';
|
||||||
|
import 'package:real_estate_mobile/features/agents/presentation/screens/agent_edit_profile_screen.dart';
|
||||||
|
import 'package:real_estate_mobile/features/profile/presentation/screens/payment_success_screen.dart';
|
||||||
|
|
||||||
final routerProvider = Provider<GoRouter>((ref) {
|
final routerProvider = Provider<GoRouter>((ref) {
|
||||||
final authRefreshNotifier = _AuthRefreshNotifier();
|
final authRefreshNotifier = _AuthRefreshNotifier();
|
||||||
@@ -166,6 +168,14 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
transitionsBuilder: _fadeTransition,
|
transitionsBuilder: _fadeTransition,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/agent/edit-profile',
|
||||||
|
pageBuilder: (context, state) => CustomTransitionPage(
|
||||||
|
key: state.pageKey,
|
||||||
|
child: const AgentEditProfileScreen(),
|
||||||
|
transitionsBuilder: _fadeTransition,
|
||||||
|
),
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/notifications',
|
path: '/notifications',
|
||||||
pageBuilder: (context, state) => CustomTransitionPage(
|
pageBuilder: (context, state) => CustomTransitionPage(
|
||||||
@@ -174,6 +184,17 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
transitionsBuilder: _fadeTransition,
|
transitionsBuilder: _fadeTransition,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/payment-success',
|
||||||
|
pageBuilder: (context, state) {
|
||||||
|
final sessionId = state.uri.queryParameters['session_id'];
|
||||||
|
return CustomTransitionPage(
|
||||||
|
key: state.pageKey,
|
||||||
|
child: PaymentSuccessScreen(sessionId: sessionId),
|
||||||
|
transitionsBuilder: _fadeTransition,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import path_provider_foundation
|
|||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import sqflite_darwin
|
import sqflite_darwin
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
|
import webview_flutter_wkwebview
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
EmojiPickerFlutterPlugin.register(with: registry.registrar(forPlugin: "EmojiPickerFlutterPlugin"))
|
EmojiPickerFlutterPlugin.register(with: registry.registrar(forPlugin: "EmojiPickerFlutterPlugin"))
|
||||||
@@ -27,4 +28,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
|
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
32
pubspec.lock
32
pubspec.lock
@@ -1389,6 +1389,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.3"
|
version: "3.0.3"
|
||||||
|
webview_flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: webview_flutter
|
||||||
|
sha256: a3da219916aba44947d3a5478b1927876a09781174b5a2b67fa5be0555154bf9
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.13.1"
|
||||||
|
webview_flutter_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: webview_flutter_android
|
||||||
|
sha256: "2a03df01df2fd30b075d1e7f24c28aee593f2e5d5ac4c3c4283c5eda63717b24"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.10.13"
|
||||||
|
webview_flutter_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: webview_flutter_platform_interface
|
||||||
|
sha256: "63d26ee3aca7256a83ccb576a50272edd7cfc80573a4305caa98985feb493ee0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.14.0"
|
||||||
|
webview_flutter_wkwebview:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: webview_flutter_wkwebview
|
||||||
|
sha256: fc0af89d403e1c053f03d023d97550412fa79f35332e2939514c82e6fe633198
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.23.8"
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ dependencies:
|
|||||||
logger: ^2.5.0
|
logger: ^2.5.0
|
||||||
flutter_dotenv: ^5.2.1
|
flutter_dotenv: ^5.2.1
|
||||||
image_picker: ^1.2.1
|
image_picker: ^1.2.1
|
||||||
|
webview_flutter: ^4.13.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user