Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ 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
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
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());
}
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
18 changes: 12 additions & 6 deletions mobile-app/lib/v2/screens/import/import_wallet_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ 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 @@ -161,6 +162,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 +185,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 +230,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
31 changes: 27 additions & 4 deletions mobile-app/lib/v2/screens/welcome/welcome_screen.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:quantus_sdk/quantus_sdk.dart';
Expand All @@ -6,6 +8,7 @@ import 'package:resonance_network_wallet/providers/l10n_provider.dart';
import 'package:resonance_network_wallet/providers/remote_config_provider.dart';
import 'package:resonance_network_wallet/services/firebase_messaging_service.dart';
import 'package:resonance_network_wallet/services/wallet_creation_service.dart';
import 'package:resonance_network_wallet/shared/constants/e2e_keys.dart';
import 'package:resonance_network_wallet/shared/extensions/toaster_extensions.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 @@ -49,9 +52,7 @@ class _WelcomeScreenV2State extends ConsumerState<WelcomeScreenV2> {
ref.invalidate(accountsProvider);
ref.invalidate(activeAccountProvider);

if (ref.read(remoteConfigProvider).enableRemoteNotifications) {
ref.read(firebaseMessagingServiceProvider).registerDeviceIfPossible();
}
unawaited(_registerForRemoteNotifications());

if (!mounted) return;
Navigator.pushAndRemoveUntil(
Expand All @@ -75,11 +76,27 @@ class _WelcomeScreenV2State extends ConsumerState<WelcomeScreenV2> {
}
}

/// Best-effort push-notification registration.
///
/// This must never block or abort wallet creation. `FirebaseMessaging.instance`
/// throws when Firebase has not been initialized yet (it is initialized lazily
/// once remote notifications are enabled), so tapping immediately after launch
/// could otherwise throw here and skip navigation to the account-ready screen.
Future<void> _registerForRemoteNotifications() async {
try {
if (!ref.read(remoteConfigProvider).enableRemoteNotifications) return;
await ref.read(firebaseMessagingServiceProvider).registerDeviceIfPossible();
} catch (_) {
// Notifications are non-critical to wallet creation; ignore failures.
}
Comment thread
dewabisma marked this conversation as resolved.
Outdated
}

@override
Widget build(BuildContext context) {
final l10n = ref.watch(l10nProvider);

return ScaffoldBase(
key: const Key(E2EKeys.welcomeScreen),
backgroundWidget: const OnboardingBackground(),
mainContent: Column(
mainAxisAlignment: MainAxisAlignment.end,
Expand All @@ -91,9 +108,15 @@ class _WelcomeScreenV2State extends ConsumerState<WelcomeScreenV2> {
child: Text(l10n.welcomeTagline, textAlign: TextAlign.center, style: context.themeText.mediumTitle),
),
const SizedBox(height: 56),
QuantusButton.simple(label: l10n.welcomeCreateNewWallet, onTap: _createWallet, isLoading: _isCreating),
QuantusButton.simple(
key: const Key(E2EKeys.welcomeCreateWalletButton),
label: l10n.welcomeCreateNewWallet,
onTap: _createWallet,
isLoading: _isCreating,
),
const SizedBox(height: 24),
QuantusButton.simple(
key: const Key(E2EKeys.welcomeImportWalletButton),
label: l10n.welcomeImportWallet,
onTap: () => Navigator.push(
context,
Expand Down
23 changes: 23 additions & 0 deletions mobile-app/patrol_test/flows/create_wallet_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:patrol/patrol.dart';

import '../support/app_launcher.dart';
import '../support/patrol_timeouts.dart';
import '../support/selectors.dart';

void main() {
patrolTest('create new wallet from welcome lands on home', ($) async {
await AppLauncher.launchFresh($);

expect($(Selectors.welcomeScreen), findsOneWidget);

await $(Selectors.welcomeCreateWalletButton).tap();

await $(Selectors.accountReadyDoneButton).waitUntilVisible(timeout: PatrolTimeouts.network);
expect($('Account 1'), findsOneWidget);

await $(Selectors.accountReadyDoneButton).tap();

await $(Selectors.homeScreen).waitUntilVisible(timeout: PatrolTimeouts.network);
});
}
25 changes: 25 additions & 0 deletions mobile-app/patrol_test/flows/import_wallet_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:patrol/patrol.dart';

import '../support/app_launcher.dart';
import '../support/patrol_timeouts.dart';
import '../support/selectors.dart';
import '../support/test_env.dart';

void main() {
patrolTest('import existing wallet from welcome lands on home', ($) async {
await AppLauncher.launchFresh($);

expect($(Selectors.welcomeScreen), findsOneWidget);

await $(Selectors.welcomeImportWalletButton).tap();

await $(Selectors.importWalletScreen).waitUntilVisible(timeout: PatrolTimeouts.visible);

await $(Selectors.importWalletSeedPhraseField).enterText(TestEnv.importMnemonic);

await $(Selectors.importWalletButton).tap();

await $(Selectors.homeScreen).waitUntilVisible(timeout: PatrolTimeouts.network);
});
}
17 changes: 17 additions & 0 deletions mobile-app/patrol_test/smoke/hello_world_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:patrol/patrol.dart';

void main() {
patrolTest('hello world shows Quantus Patrol title', ($) async {
await $.pumpWidgetAndSettle(
MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Quantus Patrol')),
),
),
);

expect($('Quantus Patrol'), findsOneWidget);
});
}
Loading
Loading