Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ jobs:
run: melos exec --concurrency=1 -- "flutter clean"
- name: Analyze
run: melos exec --concurrency=1 -- "flutter analyze ."
- name: Create mock .env file to not fail the tests
run: touch mobile-app/.env
- name: Test Mobile App
run: melos exec --concurrency=1 --scope="resonance_network_wallet" -- "flutter test"
- name: Test SDK Feature Flags API Contract
run: melos exec --concurrency=1 --scope="quantus_sdk" -- "flutter test test/contract"

# build_android:
# name: Build Android
Expand Down
10 changes: 1 addition & 9 deletions mobile-app/lib/app.dart
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:resonance_network_wallet/shared/global_navigator_key.dart';
import 'package:resonance_network_wallet/wallet_initializer.dart';
import 'package:resonance_network_wallet/utils/feature_flags.dart';
import 'package:resonance_network_wallet/v2/screens/auth/auth_wrapper.dart';
import 'package:resonance_network_wallet/v2/theme/app_theme.dart';
import 'package:resonance_network_wallet/services/firebase_messaging_service.dart';
import 'package:resonance_network_wallet/services/local_notifications_service.dart';
import 'package:resonance_network_wallet/services/notification_integration_service.dart';
import 'package:resonance_network_wallet/services/referral_service.dart';
import 'package:resonance_network_wallet/services/telemetry_navigator_observer.dart';
import 'package:resonance_network_wallet/services/deep_link_service.dart';
import 'dart:io' show Platform;

// This ensures it's a single, persistent key for the entire app lifecycle.
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

class ResonanceWalletApp extends ConsumerStatefulWidget {
const ResonanceWalletApp({super.key});

Expand All @@ -35,10 +31,6 @@ class _ResonanceWalletAppState extends ConsumerState<ResonanceWalletApp> {
ref.read(localNotificationsServiceProvider).setupNotificationsClickListener(navigatorKey);
ref.read(localNotificationsServiceProvider).handleLaunchByNotification(navigatorKey);

if (FeatureFlags.enableRemoteNotifications) {
ref.read(firebaseMessagingServiceProvider).setupNotificationTapHandlers(navigatorKey);
}

if (Platform.isAndroid) _referralService.checkPlayStoreReferralCode();
});
}
Expand Down
13 changes: 6 additions & 7 deletions mobile-app/lib/app_initializer.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:resonance_network_wallet/services/firebase_messaging_service.dart';
import 'package:resonance_network_wallet/providers/feature_flags_provider.dart';
import 'package:resonance_network_wallet/services/history_polling_manager.dart';
import 'package:resonance_network_wallet/services/local_notifications_service.dart';
import 'package:resonance_network_wallet/utils/feature_flags.dart';

/// Widget that initializes the polling services for the entire app.
/// This should be placed high in the widget tree, typically in your main app
Expand All @@ -21,19 +22,17 @@ class _AppInitializerState extends ConsumerState<AppInitializer> {
@override
void initState() {
super.initState();

_initialize();
}

Future<void> _initialize() async {
try {
ref.read(featureFlagsProvider.notifier).registerRemoteRefreshListener(ref);

final notificationService = ref.read(localNotificationsServiceProvider);
await notificationService.init();

if (FeatureFlags.enableRemoteNotifications) {
final fcmService = ref.read(firebaseMessagingServiceProvider);
await fcmService.init();
}

ref.read(historyPollingManagerProvider);
} catch (e, stackTrace) {
debugPrint('Initialization error: $e\n$stackTrace');
Expand Down
6 changes: 6 additions & 0 deletions mobile-app/lib/app_lifecycle_manager.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:quantus_sdk/quantus_sdk.dart';
import 'package:resonance_network_wallet/providers/connectivity_provider.dart';
import 'package:resonance_network_wallet/providers/feature_flags_provider.dart';
import 'package:resonance_network_wallet/providers/local_auth_provider.dart';
import 'package:resonance_network_wallet/services/history_polling_manager.dart';

Expand Down Expand Up @@ -99,6 +102,9 @@ class _AppLifecycleManagerState extends ConsumerState<AppLifecycleManager> with

// Initialize Taskmaster login if wallet exists
_initializeTaskmasterLogin();

// Sync feature flags on background resume
unawaited(ref.read(featureFlagsProvider.notifier).syncFlags());
}
} else {
// Handle background states (inactive, paused, hidden, detached)
Expand Down
6 changes: 0 additions & 6 deletions mobile-app/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
Expand All @@ -8,10 +7,8 @@ import 'package:resonance_network_wallet/app_initializer.dart';
import 'package:resonance_network_wallet/app_lifecycle_manager.dart';
import 'package:resonance_network_wallet/app.dart';
import 'package:resonance_network_wallet/utils/env_utils.dart';
import 'package:resonance_network_wallet/utils/feature_flags.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:telemetrydecksdk/telemetrydecksdk.dart';
import 'package:resonance_network_wallet/firebase_options.dart';

void main() async {
WidgetsFlutterBinding.ensureInitialized();
Expand All @@ -22,9 +19,6 @@ void main() async {
// Initialize Supabase
await Supabase.initialize(url: EnvUtils.supabaseUrl, anonKey: EnvUtils.supabaseKey);
await QuantusSdk.init();
if (FeatureFlags.enableRemoteNotifications) {
await Firebase.initializeApp(options: DefaultFirebaseOptions.getOptionsForEnvironment());
}

Telemetrydecksdk.start(
const TelemetryManagerConfiguration(
Expand Down
80 changes: 80 additions & 0 deletions mobile-app/lib/providers/feature_flags_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'dart:async';
import 'package:quantus_sdk/quantus_sdk.dart';
import 'package:resonance_network_wallet/firebase_options.dart';
import 'package:resonance_network_wallet/services/feature_flags_service.dart';
import 'package:resonance_network_wallet/services/firebase_messaging_service.dart';
import 'package:resonance_network_wallet/shared/global_navigator_key.dart';

final featureFlagsServiceProvider = Provider<FeatureFlagsService>((ref) {
return FeatureFlagsService();
});

final featureFlagsProvider = StateNotifierProvider<FeatureFlagsNotifier, FeatureFlagsModel>((ref) {
return FeatureFlagsNotifier(ref.read(featureFlagsServiceProvider));
});

class FeatureFlagsNotifier extends StateNotifier<FeatureFlagsModel> {
final FeatureFlagsService _service;
bool _isRefreshingRemote = false;
bool _isEnablingRemoteNotifications = false;

FeatureFlagsNotifier(this._service) : super(_service.readLocalFlags()) {
syncFlags();
}

Future<void> syncFlags() async {
// Fetch remote in the background. This should not block startup feel.
if (_isRefreshingRemote) return;
_isRefreshingRemote = true;

unawaited(() async {
try {
final remote = await _service.readRemoteFlags();
if (remote == null) return;

if (remote != state) {
_service.cacheFlags(remote.toCacheJson());
state = remote;
}
} catch (e) {
// Keep using cached flags on failure.
print('Feature flags remote refresh failed: $e');
} finally {
_isRefreshingRemote = false;
}
}());
}

void registerRemoteRefreshListener(WidgetRef ref) {
// using `listenManual` allows
// setting up the side-effect listener from `initState`/async code.
ref.listenManual<FeatureFlagsModel>(featureFlagsProvider, (previous, next) {
if (!next.enableRemoteNotifications) return;
unawaited(_enableRemoteNotificationsIfNeeded(ref));
});
}

Future<void> _enableRemoteNotificationsIfNeeded(WidgetRef ref) async {
if (_isEnablingRemoteNotifications) return;
_isEnablingRemoteNotifications = true;

// If Firebase wasn't initialized at startup (because cached flags were false),
// do it now.
if (Firebase.apps.isEmpty) {
await Firebase.initializeApp(options: DefaultFirebaseOptions.getOptionsForEnvironment());
}

final fcmService = ref.read(firebaseMessagingServiceProvider);
await fcmService.init(); // This requests notification permission.

// Ensure navigatorKey.currentState is attached before handling any initial message.
WidgetsBinding.instance.addPostFrameCallback((_) {
fcmService.setupNotificationTapHandlers(navigatorKey);
});

_isEnablingRemoteNotifications = false;
}
}
40 changes: 40 additions & 0 deletions mobile-app/lib/services/feature_flags_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import 'dart:convert';

import 'package:quantus_sdk/quantus_sdk.dart';

const String featureFlagsCacheKey = 'feature_flags_cache_v1';

class FeatureFlagsService {
final TaskmasterService _taskmasterService = TaskmasterService();
final SettingsService _settingsService = SettingsService();

Future<FeatureFlagsModel?> readRemoteFlags() async {
try {
final remoteData = await _taskmasterService.getWalletFeatureFlags();
return remoteData;
} catch (error) {
print('Feature flags remote read failed: $error');
return null;
}
}

FeatureFlagsModel readLocalFlags() {
final jsonString = _settingsService.getString(featureFlagsCacheKey);

if (jsonString == null || jsonString.isEmpty) {
cacheFlags(FeatureFlagsModel.defaults.toCacheJson());
return FeatureFlagsModel.defaults;
}

final decoded = jsonDecode(jsonString);
return FeatureFlagsModel.fromJson(decoded);
}

Future<void> cacheFlags(Object json) async {
try {
await _settingsService.setString(featureFlagsCacheKey, jsonEncode(json));
} catch (error) {
print('Feature flags local save failed: $error');
}
}
}
4 changes: 4 additions & 0 deletions mobile-app/lib/services/firebase_messaging_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class FirebaseMessagingService {
final SenotiService _senotiService = SenotiService();

bool _isInitialized = false;
bool __hasRegisteredHandlers = false;
String? _cachedToken;

FirebaseMessagingService(this._ref);
Expand Down Expand Up @@ -147,6 +148,9 @@ class FirebaseMessagingService {
/// Handle the user tapping on an FCM notification that launched/resumed the app.
/// Call this after the navigator key is available.
void setupNotificationTapHandlers(GlobalKey<NavigatorState> navigatorKey) {
if (__hasRegisteredHandlers) return;
__hasRegisteredHandlers = true;

FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
debugPrint('FCM notification tapped (background): ${message.messageId}');
_handleNotificationTap(message, navigatorKey);
Expand Down
3 changes: 3 additions & 0 deletions mobile-app/lib/shared/global_navigator_key.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import 'package:flutter/material.dart';

final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
8 changes: 0 additions & 8 deletions mobile-app/lib/utils/feature_flags.dart

This file was deleted.

4 changes: 2 additions & 2 deletions mobile-app/lib/v2/screens/create/wallet_ready_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:quantus_sdk/quantus_sdk.dart';
import 'package:resonance_network_wallet/providers/account_providers.dart';
import 'package:resonance_network_wallet/providers/feature_flags_provider.dart';
import 'package:resonance_network_wallet/services/firebase_messaging_service.dart';
import 'package:resonance_network_wallet/services/referral_service.dart';
import 'package:resonance_network_wallet/shared/extensions/clipboard_extensions.dart';
import 'package:resonance_network_wallet/shared/extensions/toaster_extensions.dart';
import 'package:resonance_network_wallet/utils/feature_flags.dart';
import 'package:resonance_network_wallet/v2/components/glass_button.dart';
import 'package:resonance_network_wallet/v2/components/glass_container.dart';
import 'package:resonance_network_wallet/v2/components/scaffold_base.dart';
Expand Down Expand Up @@ -91,7 +91,7 @@ class _WalletReadyScreenV2State extends ConsumerState<WalletReadyScreenV2> {
ref.invalidate(accountsProvider);
ref.invalidate(activeAccountProvider);

if (FeatureFlags.enableRemoteNotifications) {
if (ref.read(featureFlagsProvider).enableRemoteNotifications) {
ref.read(firebaseMessagingServiceProvider).registerDeviceIfPossible();
}

Expand Down
6 changes: 4 additions & 2 deletions mobile-app/lib/v2/screens/home/home_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import 'package:quantus_sdk/quantus_sdk.dart';
import 'package:resonance_network_wallet/features/components/button.dart';
import 'package:resonance_network_wallet/features/components/shared_address_action_sheet.dart';
import 'package:resonance_network_wallet/features/components/skeleton.dart';
import 'package:resonance_network_wallet/providers/feature_flags_provider.dart';
import 'package:resonance_network_wallet/v2/components/glass_button.dart' hide ButtonVariant;
import 'package:resonance_network_wallet/v2/components/glass_icon_button.dart';
import 'package:resonance_network_wallet/v2/screens/accounts/accounts_sheet.dart';
import 'package:resonance_network_wallet/v2/screens/receive/receive_sheet.dart';
import 'package:resonance_network_wallet/v2/screens/send/send_sheet.dart';
import 'package:resonance_network_wallet/v2/screens/settings/settings_screen.dart';
import 'package:resonance_network_wallet/utils/feature_flags.dart';
import 'package:resonance_network_wallet/v2/screens/pos/pos_amount_screen.dart';
import 'package:resonance_network_wallet/v2/screens/swap/swap_screen.dart';
import 'package:resonance_network_wallet/providers/account_id_list_cache.dart';
Expand Down Expand Up @@ -226,6 +226,8 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
}

Widget _buildActionButtons() {
final enableSwap = ref.watch(featureFlagsProvider).enableSwap;

final receiveCard = _actionCard(
iconAsset: 'assets/v2/action_receive.svg',
label: 'Receive',
Expand All @@ -238,7 +240,7 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
onTap: () => showSendSheetV2(context),
);

if (!FeatureFlags.enableSwap) {
if (!enableSwap) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expand Down
4 changes: 2 additions & 2 deletions mobile-app/lib/v2/screens/import/import_wallet_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:quantus_sdk/quantus_sdk.dart';
import 'package:resonance_network_wallet/providers/account_providers.dart';
import 'package:resonance_network_wallet/providers/feature_flags_provider.dart';
import 'package:resonance_network_wallet/services/firebase_messaging_service.dart';
import 'package:resonance_network_wallet/utils/feature_flags.dart';
import 'package:resonance_network_wallet/v2/components/glass_button.dart';
import 'package:resonance_network_wallet/v2/components/scaffold_base.dart';
import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart';
Expand Down Expand Up @@ -55,7 +55,7 @@ class _ImportWalletScreenV2State extends ConsumerState<ImportWalletScreenV2> {
_settingsService.setReferralCheckCompleted();
_settingsService.setExistingUserSeenPromoVideo();

if (FeatureFlags.enableRemoteNotifications) {
if (ref.read(featureFlagsProvider).enableRemoteNotifications) {
ref.read(firebaseMessagingServiceProvider).registerDeviceIfPossible();
}

Expand Down
4 changes: 2 additions & 2 deletions mobile-app/lib/v2/screens/settings/settings_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:quantus_sdk/quantus_sdk.dart';
import 'package:resonance_network_wallet/providers/feature_flags_provider.dart';
import 'package:resonance_network_wallet/services/firebase_messaging_service.dart';
import 'package:resonance_network_wallet/utils/feature_flags.dart';
import 'package:resonance_network_wallet/v2/components/glass_button.dart';
import 'package:resonance_network_wallet/v2/screens/settings/recovery_phrase_screen.dart';
import 'package:resonance_network_wallet/v2/screens/settings/reset_confirmation_sheet.dart';
Expand Down Expand Up @@ -66,7 +66,7 @@ class _SettingsScreenV2State extends ConsumerState<SettingsScreenV2> {
}

Future<void> _resetAndClearData() async {
if (FeatureFlags.enableRemoteNotifications) {
if (ref.read(featureFlagsProvider).enableRemoteNotifications) {
ref.read(firebaseMessagingServiceProvider).unregisterDevice();
}

Expand Down
9 changes: 0 additions & 9 deletions mobile-app/test/unit/send_screen_logic_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,6 @@ void main() {
});

group('hasAmountError', () {
test('returns true for zero amount', () {
final result = SendScreenLogic.hasAmountError(
amount: BigInt.zero,
balance: BigInt.from(5000000000000),
networkFee: BigInt.from(100000000),
);
expect(result, isTrue);
});

test('returns true when amount + fee exceeds balance', () {
final result = SendScreenLogic.hasAmountError(
amount: BigInt.from(4999900000000),
Expand Down
1 change: 1 addition & 0 deletions quantus_sdk/lib/quantus_sdk.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export 'src/models/event_type.dart';
export 'src/models/extrinsic_data.dart';
export 'src/models/extrinsic_fee_data.dart';
export 'src/models/unsigned_transaction_data.dart';
export 'src/models/feature_flags_model.dart';
export 'src/models/miner_reward_event.dart';
export 'src/models/miner_stats.dart';
export 'src/models/opted_in_position.dart';
Expand Down
Loading
Loading