Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ app.*.symbols
/mobile-app/android/.kotlin
/mobile-app/android/.idea
mobile-app/.env
mobile-app/.env.test
/rust-transaction-parser/target
/.cursor
mobile-app/android/app/google-services.json
mobile-app/patrol_test/test_bundle.dart
4 changes: 4 additions & 0 deletions mobile-app/ios/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ target 'Runner' do
target 'RunnerTests' do
inherit! :search_paths
end

target 'RunnerUITests' do
inherit! :complete
end
end

post_install do |installer|
Expand Down
337 changes: 337 additions & 0 deletions mobile-app/ios/Runner.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
Expand Down Expand Up @@ -67,6 +67,16 @@
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "15F373622FF2774200DB2DB0"
BuildableName = "RunnerUITests.xctest"
BlueprintName = "RunnerUITests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
Expand Down
30 changes: 0 additions & 30 deletions mobile-app/ios/Runner/GoogleService-Info.plist

This file was deleted.

9 changes: 9 additions & 0 deletions mobile-app/ios/RunnerUITests/RunnerUITests.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@import XCTest;
@import patrol;
@import ObjectiveC.runtime;

#if !defined(PATROL_INTEGRATION_TEST_IOS_RUNNER)
#import "PatrolIntegrationTestIosRunner.h"
#endif

PATROL_INTEGRATION_TEST_IOS_RUNNER(RunnerUITests)
5 changes: 5 additions & 0 deletions mobile-app/ios/TestPlan.xctestplan
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"defaultOptions" : {
"diagnosticCollectionPolicy" : "Never"
}
}
44 changes: 44 additions & 0 deletions mobile-app/lib/bootstrap/app_bootstrap.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:quantus_sdk/quantus_sdk.dart';
import 'package:resonance_network_wallet/app.dart';
import 'package:resonance_network_wallet/app_initializer.dart';
import 'package:resonance_network_wallet/app_lifecycle_manager.dart';
import 'package:resonance_network_wallet/shared/utils/env_utils.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:telemetrydecksdk/telemetrydecksdk.dart';

bool _initialized = false;

/// Initializes everything the app needs before [buildApp] can run.
///
/// Safe to call more than once: the heavy, one-shot initializers (Supabase,
/// the Rust SDK, Telemetry) run only on the first invocation. This lets E2E
/// tests reuse the exact production startup path while running several tests in
/// a single app process.
Future<void> bootstrap() async {
WidgetsFlutterBinding.ensureInitialized();
if (_initialized) return;

SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);

await dotenv.load();

await Supabase.initialize(url: EnvUtils.supabaseUrl, anonKey: EnvUtils.supabaseKey);
await QuantusSdk.init();

Telemetrydecksdk.start(
const TelemetryManagerConfiguration(appID: '098B4397-8426-4054-B379-0E4C53D2CA63', salt: 'QDay'),
);

_initialized = true;
}

/// The root widget tree shared by production and tests.
Widget buildApp() {
return const ProviderScope(
child: AppInitializer(child: AppLifecycleManager(child: ResonanceWalletApp())),
);
}
37 changes: 5 additions & 32 deletions mobile-app/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,36 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:quantus_sdk/quantus_sdk.dart';
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/shared/utils/env_utils.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:telemetrydecksdk/telemetrydecksdk.dart';
import 'package:resonance_network_wallet/bootstrap/app_bootstrap.dart';

void main() async {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);

await dotenv.load();

// Initialize Supabase
await Supabase.initialize(url: EnvUtils.supabaseUrl, anonKey: EnvUtils.supabaseKey);
await QuantusSdk.init();

Telemetrydecksdk.start(
const TelemetryManagerConfiguration(
appID: '098B4397-8426-4054-B379-0E4C53D2CA63',
salt: 'QDay',
// debug: true,
),
);

runApp(
const ProviderScope(
child: AppInitializer(child: AppLifecycleManager(child: ResonanceWalletApp())),
),
);
await bootstrap();
// buildApp() wraps the tree in a ProviderScope; the lint can't see through it.
// ignore: missing_provider_scope
runApp(buildApp());
}
30 changes: 30 additions & 0 deletions mobile-app/lib/services/firebase_messaging_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import 'package:quantus_sdk/quantus_sdk.dart';
import 'package:resonance_network_wallet/models/notification_models.dart';
import 'package:resonance_network_wallet/providers/account_providers.dart';
import 'package:resonance_network_wallet/providers/notification_provider.dart';
import 'package:resonance_network_wallet/providers/remote_config_provider.dart';
import 'package:resonance_network_wallet/services/history_polling_manager.dart';
import 'package:resonance_network_wallet/services/telemetry_service.dart';
import 'package:resonance_network_wallet/services/transaction_service.dart';
import 'package:resonance_network_wallet/shared/utils/print.dart';

Expand Down Expand Up @@ -210,3 +212,31 @@ class FirebaseMessagingService {
final firebaseMessagingServiceProvider = Provider<FirebaseMessagingService>((ref) {
return FirebaseMessagingService(ref);
});

/// Best-effort push-notification registration for onboarding entry points.
///
/// This must never block or abort wallet creation/import. Reading
/// [firebaseMessagingServiceProvider] constructs a [FirebaseMessagingService],
/// whose field initializer touches `FirebaseMessaging.instance` synchronously;
/// that throws when Firebase has not been initialized yet (it is initialized
/// lazily once remote notifications are enabled). Tapping immediately after
/// launch could therefore throw and skip navigation, so the provider read and
/// every subsequent call are wrapped here and all failures are swallowed.
///
/// When [insertAddress] is non-null, the address is registered for push
/// notifications on the existing device; otherwise the device itself is
/// registered for the first time.
Future<void> registerForRemoteNotificationsBestEffort(WidgetRef ref, {String? insertAddress}) async {
try {
if (!ref.read(remoteConfigProvider).enableRemoteNotifications) return;
final service = ref.read(firebaseMessagingServiceProvider);
if (insertAddress != null) {
await service.insertNewAddress(insertAddress);
} else {
await service.registerDeviceIfPossible();
}
} catch (e) {
quantusDebugPrint('Failed to register for remote notifications: $e');
TelemetryService().sendError('registerForRemoteNotifications', error: e, stackTrace: StackTrace.current);
}
}
24 changes: 24 additions & 0 deletions mobile-app/lib/shared/constants/e2e_keys.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/// Stable widget key identifiers shared between production widgets and E2E tests.
///
/// These live in `lib/` (not in the test folder) so that production screens and
/// the Patrol selectors in `patrol_test/support/selectors.dart` reference the
/// exact same strings. This keeps the two in lockstep and avoids the drift that
/// happens when each side hardcodes its own copy of a key.
///
/// Keep these as plain [String] constants (not [Key]s) so this file stays free
/// of any test-framework dependency and can be imported anywhere.
class E2EKeys {
E2EKeys._();

static const String welcomeScreen = 'welcome_screen';
static const String welcomeCreateWalletButton = 'welcome_create_wallet_button';
static const String welcomeImportWalletButton = 'welcome_import_wallet_button';

static const String accountReadyDoneButton = 'account_ready_done_button';

static const String importWalletScreen = 'import_wallet_screen';
static const String importWalletSeedPhraseField = 'import_wallet_seed_phrase_field';
static const String importWalletButton = 'import_wallet_button';

static const String homeScreen = 'home_screen';
}
2 changes: 2 additions & 0 deletions mobile-app/lib/v2/screens/accounts/account_ready_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:quantus_sdk/quantus_sdk.dart';
import 'package:resonance_network_wallet/l10n/app_localizations.dart';
import 'package:resonance_network_wallet/providers/l10n_provider.dart';
import 'package:resonance_network_wallet/providers/wallet_providers.dart';
import 'package:resonance_network_wallet/shared/constants/e2e_keys.dart';
import 'package:resonance_network_wallet/v2/components/loader.dart';
import 'package:resonance_network_wallet/v2/components/quantus_button.dart';
import 'package:resonance_network_wallet/v2/components/scaffold_base.dart';
Expand Down Expand Up @@ -136,6 +137,7 @@ class AccountReadyScreen extends ConsumerWidget {
),
bottomContent: ScaffoldBaseBottomContent(
child: QuantusButton.simple(
key: const Key(E2EKeys.accountReadyDoneButton),
label: l10n.accountReadyDone,
onTap: () => _goHome(context),
variant: ButtonVariant.primary,
Expand Down
2 changes: 2 additions & 0 deletions mobile-app/lib/v2/screens/home/home_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:resonance_network_wallet/providers/remote_config_provider.dart';
import 'package:resonance_network_wallet/routes.dart';
import 'package:resonance_network_wallet/services/global_history_polling_service.dart';
import 'package:resonance_network_wallet/services/telemetry_service.dart';
import 'package:resonance_network_wallet/shared/constants/e2e_keys.dart';
import 'package:resonance_network_wallet/shared/extensions/current_route_extensions.dart';
import 'package:resonance_network_wallet/shared/utils/print.dart';
import 'package:resonance_network_wallet/shared/utils/url_utils.dart';
Expand Down Expand Up @@ -166,6 +167,7 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
final text = context.themeText;

return GlobalToastListener(
key: const Key(E2EKeys.homeScreen),
child: accountAsync.when(
loading: () => const ScaffoldBase(mainContent: Center(child: Loader())),
error: (e, _) => ScaffoldBase(
Expand Down
29 changes: 17 additions & 12 deletions mobile-app/lib/v2/screens/import/import_wallet_screen.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
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/account_providers.dart';
import 'package:resonance_network_wallet/providers/l10n_provider.dart';
import 'package:resonance_network_wallet/providers/remote_config_provider.dart';
import 'package:resonance_network_wallet/providers/wallet_providers.dart';
import 'package:resonance_network_wallet/services/firebase_messaging_service.dart';
import 'package:resonance_network_wallet/services/telemetry_service.dart';
import 'package:resonance_network_wallet/shared/constants/e2e_keys.dart';
import 'package:resonance_network_wallet/shared/utils/print.dart';
import 'package:resonance_network_wallet/v2/components/quantus_button.dart';
import 'package:resonance_network_wallet/v2/components/scaffold_base.dart';
Expand Down Expand Up @@ -115,11 +117,9 @@ class _ImportWalletScreenV2State extends ConsumerState<ImportWalletScreenV2> {
_settingsService.setRecoveryPhraseViewed(widget.walletIndex);
ref.invalidate(recoveryPhraseViewedProvider(widget.walletIndex));

if (ref.read(remoteConfigProvider).enableRemoteNotifications && widget.walletIndex == 0) {
ref.read(firebaseMessagingServiceProvider).registerDeviceIfPossible();
} else if (ref.read(remoteConfigProvider).enableRemoteNotifications && widget.walletIndex > 0) {
ref.read(firebaseMessagingServiceProvider).insertNewAddress(key.ss58Address);
}
unawaited(
registerForRemoteNotificationsBestEffort(ref, insertAddress: widget.walletIndex > 0 ? key.ss58Address : null),
);

if (!mounted) return;
if (widget.openAccountsOnComplete) {
Expand Down Expand Up @@ -161,6 +161,7 @@ class _ImportWalletScreenV2State extends ConsumerState<ImportWalletScreenV2> {
final fieldTextStyle = text.smallTitle?.copyWith(color: colors.checksum, fontWeight: FontWeight.w400);

return ScaffoldBase(
key: const Key(E2EKeys.importWalletScreen),
appBar: V2AppBar(title: l10n.importWalletAppBarTitle),
mainContent: GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
Expand All @@ -183,6 +184,7 @@ class _ImportWalletScreenV2State extends ConsumerState<ImportWalletScreenV2> {
Padding(
padding: const EdgeInsets.only(right: 36),
child: TextField(
key: const Key(E2EKeys.importWalletSeedPhraseField),
controller: _controller,
focusNode: _focusNode,
onChanged: (_) => setState(() {}),
Expand Down Expand Up @@ -227,12 +229,15 @@ class _ImportWalletScreenV2State extends ConsumerState<ImportWalletScreenV2> {
),
),
bottomContent: ScaffoldBaseBottomContent(
child: QuantusButton.simple(
key: _buttonKey,
label: l10n.importWalletButton,
onTap: _import,
isLoading: _isLoading,
isDisabled: !_hasInput,
child: KeyedSubtree(
key: const Key(E2EKeys.importWalletButton),
child: QuantusButton.simple(
key: _buttonKey,
label: l10n.importWalletButton,
onTap: _import,
isLoading: _isLoading,
isDisabled: !_hasInput,
),
),
),
);
Expand Down
Loading
Loading