diff --git a/mobile-app/assets/high_security/security_icon_big.svg b/mobile-app/assets/high_security/security_icon_big.svg new file mode 100644 index 00000000..592ccaae --- /dev/null +++ b/mobile-app/assets/high_security/security_icon_big.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile-app/assets/high_security/security_icon_black.svg b/mobile-app/assets/high_security/security_icon_black.svg new file mode 100644 index 00000000..ee793ee2 --- /dev/null +++ b/mobile-app/assets/high_security/security_icon_black.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/mobile-app/assets/high_security/step_indicator_active_icon.svg b/mobile-app/assets/high_security/step_indicator_active_icon.svg new file mode 100644 index 00000000..1d151160 --- /dev/null +++ b/mobile-app/assets/high_security/step_indicator_active_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/mobile-app/assets/high_security/step_indicator_icon.svg b/mobile-app/assets/high_security/step_indicator_icon.svg new file mode 100644 index 00000000..7ea01eef --- /dev/null +++ b/mobile-app/assets/high_security/step_indicator_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/mobile-app/ios/Runner.xcodeproj/project.pbxproj b/mobile-app/ios/Runner.xcodeproj/project.pbxproj index f3a23933..eb4370d4 100644 --- a/mobile-app/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile-app/ios/Runner.xcodeproj/project.pbxproj @@ -8,14 +8,14 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 193D36486744F9CA896B5858 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ABCD771D31123455D6A0A380 /* Pods_RunnerTests.framework */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 86E110147A80CB996E2CA4AB /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97939A145D33F6801E304DF5 /* Pods_Runner.framework */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - A7A4D6A980D319AC470A9637 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 313CD1C8F586AE6AE7FC4933 /* Pods_Runner.framework */; }; + F2AFF251EE4997924A7D0F78 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0ABF2C48D8CF924735FEFF77 /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -42,30 +42,30 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 0E7E3C1546B23A5AFF5942FD /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 0ABF2C48D8CF924735FEFF77 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 313CD1C8F586AE6AE7FC4933 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 19F520CF0B8598B21F21E35D /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 1F5D52D89FF5F3FC074A093C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 4672F8772DB9DA61003B0FFF /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; - 4F6FA13E483D97F15F98587C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 91653C25A16732D6D1699E5A /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 823E822652161FDACC420786 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97939A145D33F6801E304DF5 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - ABCD771D31123455D6A0A380 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - BFE1153F1F57BEE110B11642 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; - C4D362161EB554F367F7E9E7 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; - E3E5F71FCCC8D2959D796E5E /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + CDD18E53B3D0BB011AFA8C44 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + CE44BF236E1BD0C36A568AA1 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + DD6CCDCA2672B6A4F381839C /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -73,7 +73,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 193D36486744F9CA896B5858 /* Pods_RunnerTests.framework in Frameworks */, + F2AFF251EE4997924A7D0F78 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -81,7 +81,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - A7A4D6A980D319AC470A9637 /* Pods_Runner.framework in Frameworks */, + 86E110147A80CB996E2CA4AB /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -96,11 +96,11 @@ path = RunnerTests; sourceTree = ""; }; - 58644EB040BB5BEDAFF2CD5F /* Frameworks */ = { + 568D30F7218BAC6CA6F26C90 /* Frameworks */ = { isa = PBXGroup; children = ( - 313CD1C8F586AE6AE7FC4933 /* Pods_Runner.framework */, - ABCD771D31123455D6A0A380 /* Pods_RunnerTests.framework */, + 97939A145D33F6801E304DF5 /* Pods_Runner.framework */, + 0ABF2C48D8CF924735FEFF77 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -108,12 +108,12 @@ 697901FF368C5DA3A4CA46A0 /* Pods */ = { isa = PBXGroup; children = ( - 0E7E3C1546B23A5AFF5942FD /* Pods-Runner.debug.xcconfig */, - 4F6FA13E483D97F15F98587C /* Pods-Runner.release.xcconfig */, - E3E5F71FCCC8D2959D796E5E /* Pods-Runner.profile.xcconfig */, - BFE1153F1F57BEE110B11642 /* Pods-RunnerTests.debug.xcconfig */, - 91653C25A16732D6D1699E5A /* Pods-RunnerTests.release.xcconfig */, - C4D362161EB554F367F7E9E7 /* Pods-RunnerTests.profile.xcconfig */, + 823E822652161FDACC420786 /* Pods-Runner.debug.xcconfig */, + 1F5D52D89FF5F3FC074A093C /* Pods-Runner.release.xcconfig */, + CDD18E53B3D0BB011AFA8C44 /* Pods-Runner.profile.xcconfig */, + DD6CCDCA2672B6A4F381839C /* Pods-RunnerTests.debug.xcconfig */, + CE44BF236E1BD0C36A568AA1 /* Pods-RunnerTests.release.xcconfig */, + 19F520CF0B8598B21F21E35D /* Pods-RunnerTests.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -137,7 +137,7 @@ 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, 697901FF368C5DA3A4CA46A0 /* Pods */, - 58644EB040BB5BEDAFF2CD5F /* Frameworks */, + 568D30F7218BAC6CA6F26C90 /* Frameworks */, ); sourceTree = ""; }; @@ -173,7 +173,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 005E706ADC7DC70A01BC32C5 /* [CP] Check Pods Manifest.lock */, + BF3E28A893917F2766F78391 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, 83542EEFCC0959E7B844E8CA /* Frameworks */, @@ -192,14 +192,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 48B9C5DA509103240CAD37F8 /* [CP] Check Pods Manifest.lock */, + EC24DA874FC9ADBA74522F88 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - CA35B1F2B3EDD6A977D48ECC /* [CP] Embed Pods Frameworks */, + E9E213D6C6EC5BE831912A8A /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -271,45 +271,38 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 005E706ADC7DC70A01BC32C5 /* [CP] Check Pods Manifest.lock */ = { + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); + name = "Thin Binary"; outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); - name = "Thin Binary"; + name = "Run Script"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - 48B9C5DA509103240CAD37F8 /* [CP] Check Pods Manifest.lock */ = { + BF3E28A893917F2766F78391 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -324,43 +317,50 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 9740EEB61CF901F6004384FC /* Run Script */ = { + E9E213D6C6EC5BE831912A8A /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); - inputPaths = ( + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "Run Script"; - outputPaths = ( + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; }; - CA35B1F2B3EDD6A977D48ECC /* [CP] Embed Pods Frameworks */ = { + EC24DA874FC9ADBA74522F88 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Embed Pods Frameworks"; + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -498,7 +498,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = BFE1153F1F57BEE110B11642 /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = DD6CCDCA2672B6A4F381839C /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -517,7 +517,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 91653C25A16732D6D1699E5A /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = CE44BF236E1BD0C36A568AA1 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -534,7 +534,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = C4D362161EB554F367F7E9E7 /* Pods-RunnerTests.profile.xcconfig */; + baseConfigurationReference = 19F520CF0B8598B21F21E35D /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; diff --git a/mobile-app/lib/features/components/custom_text_field.dart b/mobile-app/lib/features/components/custom_text_field.dart index 10a66b94..02e360e8 100644 --- a/mobile-app/lib/features/components/custom_text_field.dart +++ b/mobile-app/lib/features/components/custom_text_field.dart @@ -4,6 +4,8 @@ import 'package:resonance_network_wallet/features/components/label.dart'; import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart'; import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; +enum TextFieldVariant { primary, secondary } + class CustomTextField extends StatelessWidget { final String? labelText; final TextStyle? textStyle; @@ -19,6 +21,7 @@ class CustomTextField extends StatelessWidget { final String? errorMsg; final double? leftPadding; final bool? disabled; + final TextFieldVariant variant; const CustomTextField({ super.key, @@ -36,10 +39,18 @@ class CustomTextField extends StatelessWidget { this.leftPadding, this.controller, this.disabled = false, + this.variant = TextFieldVariant.primary, }) : assert(initialValue == null || controller == null, 'Cannot provide both an initialValue and a controller.'); @override Widget build(BuildContext context) { + final effectiveTextStyle = variant == TextFieldVariant.primary + ? context.themeText.smallTitle + : context.themeText.paragraph; + final effectiveHintStyle = variant == TextFieldVariant.primary + ? context.themeText.smallTitle?.copyWith(color: context.themeColors.textPrimary.useOpacity(0.5)) + : context.themeText.paragraph?.copyWith(color: context.themeColors.textPrimary.useOpacity(0.5)); + // The main container for the entire widget return SizedBox( width: double.infinity, @@ -60,7 +71,7 @@ class CustomTextField extends StatelessWidget { onChanged: onChanged, obscureText: obscureText, // Styling for the text inside the input field - style: textStyle ?? context.themeText.smallTitle, + style: textStyle ?? effectiveTextStyle, decoration: InputDecoration( fillColor: fillColor, isDense: true, // Reduces vertical padding @@ -78,9 +89,7 @@ class CustomTextField extends StatelessWidget { ), // Removes default padding hintText: hintText, // Style for the hint text when the field is empty - hintStyle: - hintStyle ?? - context.themeText.smallTitle?.copyWith(color: context.themeColors.textPrimary.useOpacity(0.5)), + hintStyle: hintStyle ?? effectiveHintStyle, ), ), diff --git a/mobile-app/lib/features/components/gradient_text.dart b/mobile-app/lib/features/components/gradient_text.dart new file mode 100644 index 00000000..eead202e --- /dev/null +++ b/mobile-app/lib/features/components/gradient_text.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +class GradientText extends StatelessWidget { + final String text; + final List colors; + final TextStyle? style; + + const GradientText(this.text, {super.key, required this.colors, required this.style}); + + @override + Widget build(BuildContext context) { + return ShaderMask( + shaderCallback: (bounds) => LinearGradient( + colors: colors, + begin: const Alignment(0.00, -1.00), + end: const Alignment(0, 1), + ).createShader(bounds), + child: Text(text, style: style?.copyWith(color: Colors.white)), + ); + } +} diff --git a/mobile-app/lib/features/components/steps.dart b/mobile-app/lib/features/components/steps.dart new file mode 100644 index 00000000..1053d4c0 --- /dev/null +++ b/mobile-app/lib/features/components/steps.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart'; + +class StepsIndicator extends StatelessWidget { + final int currentStep; + final int totalSteps; + final double lineHeight = 2; + final double iconHeight = 6.56; + + const StepsIndicator({super.key, required this.currentStep, required this.totalSteps}) + : assert(currentStep >= 1 && currentStep <= totalSteps); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + SizedBox( + height: iconHeight, + child: Row( + children: List.generate(totalSteps, (index) { + return Expanded( + child: Row( + children: [if (index < totalSteps) _buildStepLine(context, index), _buildStepPoint(context, index)], + ), + ); + }), + ), + ), + ], + ); + } + + Widget _buildStepPoint(BuildContext context, int index) { + final isCompleted = index < currentStep - 1; + final isCurrent = index == currentStep - 1; + final iconPath = (isCompleted || isCurrent) + ? 'assets/high_security/step_indicator_active_icon.svg' + : 'assets/high_security/step_indicator_icon.svg'; + + return Center(child: SvgPicture.asset(iconPath, width: 4, height: iconHeight)); + } + + Widget _buildStepLine(BuildContext context, int index) { + final isCompleted = index < currentStep - 1; + final isCurrent = index == currentStep - 1; + final lineColor = (isCompleted || isCurrent) ? context.themeColors.checksum : const Color(0x66FFFFFF); + + return Expanded( + child: Container(height: lineHeight, color: lineColor), + ); + } +} diff --git a/mobile-app/lib/features/components/tree_list.dart b/mobile-app/lib/features/components/tree_list.dart new file mode 100644 index 00000000..535111fe --- /dev/null +++ b/mobile-app/lib/features/components/tree_list.dart @@ -0,0 +1,358 @@ +import 'package:flutter/material.dart'; + +// Data model for tree nodes +class TreeNode { + final T data; + final List> children; + bool isExpanded; + + TreeNode({required this.data, this.children = const [], this.isExpanded = true}); + + bool get hasChildren => children.isNotEmpty; + bool get isLeaf => children.isEmpty; +} + +// Tree structure list widget +class TreeListView extends StatefulWidget { + final List> nodes; + final Widget Function(BuildContext context, TreeNode node, int depth) nodeBuilder; + + final bool showExpandCollapse; + final EdgeInsetsGeometry? padding; + final ScrollPhysics? physics; + final bool shrinkWrap; + + const TreeListView({ + super.key, + required this.nodes, + required this.nodeBuilder, + this.showExpandCollapse = true, + this.padding, + this.physics, + this.shrinkWrap = false, + }); + + @override + State> createState() => _TreeListViewState(); +} + +class _TreeListViewState extends State> { + final Color lineColor = const Color(0x66FFFFFF); + final double lineWidth = 1.0; + final double indentWidth = 24.0; + + @override + Widget build(BuildContext context) { + return ListView( + padding: widget.padding, + physics: widget.physics, + shrinkWrap: widget.shrinkWrap, + children: _buildTreeNodes(widget.nodes, 0, []), + ); + } + + List _buildTreeNodes(List> nodes, int depth, List parentLines) { + List widgets = []; + + for (int i = 0; i < nodes.length; i++) { + final node = nodes[i]; + final isLast = i == nodes.length - 1; + final currentParentLines = List.from(parentLines)..add(!isLast); + + widgets.add( + _TreeNodeWidget( + node: node, + depth: depth, + isLast: isLast, + parentLines: parentLines, + indentWidth: indentWidth, + lineColor: lineColor, + lineWidth: lineWidth, + showExpandCollapse: widget.showExpandCollapse, + nodeBuilder: widget.nodeBuilder, + onToggleExpanded: () { + setState(() { + node.isExpanded = !node.isExpanded; + }); + }, + ), + ); + + if (node.hasChildren && node.isExpanded) { + widgets.addAll(_buildTreeNodes(node.children, depth + 1, currentParentLines)); + } + } + + return widgets; + } +} + +class _TreeNodeWidget extends StatelessWidget { + final TreeNode node; + final int depth; + final bool isLast; + final List parentLines; + final double indentWidth; + final Color lineColor; + final double lineWidth; + final bool showExpandCollapse; + final Widget Function(BuildContext context, TreeNode node, int depth) nodeBuilder; + final VoidCallback onToggleExpanded; + + const _TreeNodeWidget({ + required this.node, + required this.depth, + required this.isLast, + required this.parentLines, + required this.indentWidth, + required this.lineColor, + required this.lineWidth, + required this.showExpandCollapse, + required this.nodeBuilder, + required this.onToggleExpanded, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + // Tree lines + SizedBox( + width: (depth + 1) * indentWidth, + height: 56, + child: CustomPaint( + painter: TreeLinePainter( + depth: depth, + isLast: isLast, + parentLines: parentLines, + lineColor: lineColor, + lineWidth: lineWidth, + indentWidth: indentWidth, + ), + ), + ), + // Expand/collapse button + if (showExpandCollapse && node.hasChildren) + GestureDetector( + onTap: onToggleExpanded, + child: Container( + width: 20, + height: 20, + margin: const EdgeInsets.only(right: 4), + decoration: BoxDecoration( + border: Border.all(color: lineColor), + color: Colors.white, + ), + child: Icon(node.isExpanded ? Icons.remove : Icons.add, size: 12, color: lineColor), + ), + ) + else if (showExpandCollapse) + const SizedBox(width: 24), + // Node content + Expanded(child: nodeBuilder(context, node, depth)), + ], + ); + } +} + +class TreeLinePainter extends CustomPainter { + final int depth; + final bool isLast; + final List parentLines; + final Color lineColor; + final double lineWidth; + final double indentWidth; + + TreeLinePainter({ + required this.depth, + required this.isLast, + required this.parentLines, + required this.lineColor, + required this.lineWidth, + required this.indentWidth, + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = lineColor + ..strokeWidth = lineWidth + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round; + + // Draw vertical lines for parent levels + for (int i = 0; i < parentLines.length; i++) { + if (parentLines[i]) { + final x = (i + 1) * indentWidth - indentWidth / 2; + canvas.drawLine(Offset(x, 0), Offset(x, size.height), paint); + } + } + + if (depth >= 0) { + final x = (depth + 1) * indentWidth - indentWidth / 2; + final centerY = size.height / 2; + + // Draw vertical line (up to center or full height) + if (!isLast) { + canvas.drawLine(Offset(x, 0), Offset(x, size.height), paint); + } else { + canvas.drawLine(Offset(x, 0), Offset(x, centerY), paint); + } + + // Draw horizontal line to the node (shorter to make room for arrow) + final horizontalEndX = x + indentWidth / 2 - 6; // Leave space for arrow + canvas.drawLine(Offset(x, centerY), Offset(horizontalEndX, centerY), paint); + + // Draw arrow at the end of horizontal line + _drawArrow(canvas, paint, Offset(horizontalEndX, centerY)); + } + } + + void _drawArrow(Canvas canvas, Paint paint, Offset position) { + final arrowSize = 5.0; + + // Draw horizontal line for arrow shaft + canvas.drawLine(Offset(position.dx, position.dy), Offset(position.dx + arrowSize, position.dy), paint); + + // Draw arrow head (two diagonal lines forming >) + canvas.drawLine( + Offset(position.dx + arrowSize, position.dy), + Offset(position.dx + arrowSize - 2, position.dy - 2), + paint, + ); + canvas.drawLine( + Offset(position.dx + arrowSize, position.dy), + Offset(position.dx + arrowSize - 2, position.dy + 2), + paint, + ); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} + +// Simple tree builder utility functions +List> buildTreeFromMap(Map data, T Function(String key, dynamic value) converter) { + List> nodes = []; + + data.forEach((key, value) { + if (value is Map) { + nodes.add(TreeNode(data: converter(key, value), children: buildTreeFromMap(value, converter))); + } else if (value is List) { + nodes.add( + TreeNode( + data: converter(key, value), + children: value.map>((item) => TreeNode(data: converter(item.toString(), item))).toList(), + ), + ); + } else { + nodes.add(TreeNode(data: converter(key, value))); + } + }); + + return nodes; +} + +// File system item for demo +class FileSystemItem { + final String name; + final bool isDirectory; + final String? extension; + + FileSystemItem({required this.name, required this.isDirectory, this.extension}); + + IconData get icon { + if (isDirectory) return Icons.folder; + switch (extension?.toLowerCase()) { + case 'dart': + return Icons.code; + case 'md': + return Icons.description; + case 'json': + return Icons.data_object; + case 'yaml': + case 'yml': + return Icons.settings; + default: + return Icons.insert_drive_file; + } + } + + Color get iconColor { + if (isDirectory) return Colors.blue; + switch (extension?.toLowerCase()) { + case 'dart': + return Colors.blue; + case 'md': + return Colors.orange; + case 'json': + return Colors.green; + case 'yaml': + case 'yml': + return Colors.purple; + default: + return Colors.grey; + } + } +} + +// Demo widget +class TreeListViewDemo extends StatelessWidget { + const TreeListViewDemo({super.key}); + + @override + Widget build(BuildContext context) { + final fileSystem = [ + TreeNode(data: FileSystemItem(name: 'lib', isDirectory: true)), + TreeNode(data: FileSystemItem(name: 'assets', isDirectory: true)), + TreeNode( + data: FileSystemItem(name: 'pubspec.yaml', isDirectory: false, extension: 'yaml'), + ), + TreeNode( + data: FileSystemItem(name: 'README.md', isDirectory: false, extension: 'md'), + ), + ]; + + return Scaffold( + appBar: AppBar( + title: const Text('Tree Structure List'), + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('File System Tree:', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const SizedBox(height: 16), + Expanded( + child: TreeListView( + showExpandCollapse: false, + nodes: fileSystem, + nodeBuilder: (context, node, depth) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + children: [ + Icon(node.data.icon, size: 18, color: node.data.iconColor), + const SizedBox(width: 8), + Text( + node.data.name, + style: TextStyle( + fontSize: 14, + fontWeight: node.data.isDirectory ? FontWeight.w500 : FontWeight.normal, + ), + ), + ], + ), + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/mobile-app/lib/features/components/wallet_action_button.dart b/mobile-app/lib/features/components/wallet_action_button.dart new file mode 100644 index 00000000..912a1079 --- /dev/null +++ b/mobile-app/lib/features/components/wallet_action_button.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/features/styles/app_size_theme.dart'; + +class WalletActionButton extends StatelessWidget { + final String assetPath; + const WalletActionButton({super.key, required this.assetPath}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(4), + decoration: ShapeDecoration( + color: Colors.white.useOpacity(0.15), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), + ), + child: SvgPicture.asset( + assetPath, + width: context.themeSize.mainMenuIconSize, + height: context.themeSize.mainMenuIconSize, + ), + ); + } +} diff --git a/mobile-app/lib/features/components/wallet_app_bar.dart b/mobile-app/lib/features/components/wallet_app_bar.dart index d877722c..bfdcc71a 100644 --- a/mobile-app/lib/features/components/wallet_app_bar.dart +++ b/mobile-app/lib/features/components/wallet_app_bar.dart @@ -10,6 +10,9 @@ abstract class WalletAppBar extends StatelessWidget implements PreferredSizeWidg factory WalletAppBar.simple({Key? key, required String title}) => _SimpleWalletAppBar(key: key, title: title); + factory WalletAppBar.simpleWithBackButton({Key? key, required String title, VoidCallback? onBack}) => + _StandardWalletAppBar(key: key, title: title, onBack: onBack); + factory WalletAppBar.custom({Key? key, required Widget titleWidget, Widget? leadingWidget, List? actions}) => _CustomWalletAppBar(key: key, titleWidget: titleWidget, leadingWidget: leadingWidget, actions: actions); diff --git a/mobile-app/lib/features/main/screens/account_settings_screen.dart b/mobile-app/lib/features/main/screens/account_settings_screen.dart index e9db52c0..14f3fa69 100644 --- a/mobile-app/lib/features/main/screens/account_settings_screen.dart +++ b/mobile-app/lib/features/main/screens/account_settings_screen.dart @@ -16,11 +16,13 @@ import 'package:resonance_network_wallet/features/styles/app_size_theme.dart'; import 'package:resonance_network_wallet/features/components/sphere.dart'; import 'package:resonance_network_wallet/features/components/wallet_app_bar.dart'; import 'package:resonance_network_wallet/features/main/screens/create_account_screen.dart'; +import 'package:resonance_network_wallet/features/main/screens/high_security/high_security_get_started_screen.dart'; import 'package:resonance_network_wallet/features/main/screens/receive_screen.dart'; import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart'; import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; import 'package:resonance_network_wallet/shared/extensions/clipboard_extensions.dart'; import 'package:resonance_network_wallet/shared/extensions/media_query_data_extension.dart'; +import 'package:resonance_network_wallet/utils/feature_flags.dart'; class AccountSettingsScreen extends ConsumerStatefulWidget { final Account account; @@ -165,10 +167,11 @@ class _AccountSettingsScreenState extends ConsumerState { _buildShareSection(), const SizedBox(height: 20), _buildAddressSection(), - const SizedBox(height: 20), - _buildSecuritySection(), - const SizedBox(height: 20), - if (widget.account.accountType == AccountType.keystone) _buildDisconnectWalletButton(), + if (FeatureFlags.enableHighSecurity) ...[const SizedBox(height: 20), _buildSecuritySection()], + if (widget.account.accountType == AccountType.keystone) ...[ + const SizedBox(height: 20), + _buildDisconnectWalletButton(), + ], const SizedBox(height: 30), ], ), @@ -276,25 +279,24 @@ class _AccountSettingsScreenState extends ConsumerState { Widget _buildSecuritySection() { return _buildSettingCard( - child: Padding( - padding: const EdgeInsets.only(top: 12.0, left: 12.0, bottom: 12.0, right: 26.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - SvgPicture.asset('assets/high_security_icon.svg', width: context.isTablet ? 28 : 20), - const SizedBox(width: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('High Security', style: context.themeText.largeTag), - Text('COMING SOON', style: context.themeText.detail?.copyWith(color: context.themeColors.checksum)), - ], - ), - ], - ), - ], + child: InkWell( + onTap: () { + Navigator.push(context, MaterialPageRoute(builder: (context) => const HighSecurityGetStartedScreen())); + }, + child: Padding( + padding: const EdgeInsets.only(top: 12.0, left: 12.0, bottom: 12.0, right: 26.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + SvgPicture.asset('assets/high_security_icon.svg', width: context.isTablet ? 28 : 20), + const SizedBox(width: 12), + Text('High Security', style: context.themeText.largeTag), + ], + ), + ], + ), ), ), ); diff --git a/mobile-app/lib/features/main/screens/accounts_screen.dart b/mobile-app/lib/features/main/screens/accounts_screen.dart index e3a13691..26a8356c 100644 --- a/mobile-app/lib/features/main/screens/accounts_screen.dart +++ b/mobile-app/lib/features/main/screens/accounts_screen.dart @@ -8,6 +8,7 @@ import 'package:resonance_network_wallet/features/components/scaffold_base.dart' import 'package:resonance_network_wallet/features/components/select.dart'; import 'package:resonance_network_wallet/features/components/select_action_sheet.dart'; import 'package:resonance_network_wallet/features/components/sphere.dart'; +import 'package:resonance_network_wallet/features/components/tree_list.dart'; import 'package:resonance_network_wallet/features/components/wallet_app_bar.dart'; import 'package:resonance_network_wallet/features/main/screens/account_settings_screen.dart'; import 'package:resonance_network_wallet/features/main/screens/add_hardware_account_screen.dart'; @@ -18,9 +19,11 @@ import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart'; import 'package:resonance_network_wallet/features/styles/app_size_theme.dart'; import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/providers/entrusted_account_provider.dart'; import 'package:resonance_network_wallet/providers/wallet_providers.dart'; import 'package:resonance_network_wallet/shared/extensions/media_query_data_extension.dart'; import 'package:resonance_network_wallet/utils/feature_flags.dart'; +import 'dart:math'; enum _WalletMoreAction { createWallet, importWallet, addHardwareWallet } @@ -109,7 +112,7 @@ class _AccountsScreenState extends ConsumerState { Item(value: _WalletMoreAction.importWallet, label: 'Import wallet'), ]; - if (FeatureFlags.showKeystoneHardwareWallet) { + if (FeatureFlags.enableKeystoneHardwareWallet) { items.add(Item(value: _WalletMoreAction.addHardwareWallet, label: 'Add hardware wallet')); } @@ -267,6 +270,13 @@ class _AccountsScreenState extends ConsumerState { } Widget _buildAccountListItem(Account account, bool isActive, int index) { + final entrustedAccountsAsync = ref.watch(entrustedAccountsProvider(account)); + final entrustedAccountsData = entrustedAccountsAsync.value ?? []; + + final entrustedNodes = entrustedAccountsData.map((entrusted) => TreeNode(data: entrusted)).toList(); + + final double constraintMaxHeight = min(entrustedNodes.length * 52, 104); + return InkWell( onTap: () async { await ref.read(activeAccountProvider.notifier).setActiveAccount(account); @@ -275,153 +285,234 @@ class _AccountsScreenState extends ConsumerState { child: Stack( clipBehavior: Clip.hardEdge, children: [ - Row( + Column( children: [ - Expanded( - child: Container( - padding: EdgeInsets.symmetric(horizontal: context.isTablet ? 20 : 8, vertical: 8), - decoration: ShapeDecoration( - color: isActive ? context.themeColors.surfaceActive : context.themeColors.surface, - shape: RoundedRectangleBorder( - side: BorderSide(width: 1, color: context.themeColors.borderLight), - borderRadius: BorderRadius.circular(5), - ), - ), - child: Row( - children: [ - const SizedBox(width: 24), - Expanded( - child: Consumer( - builder: (context, ref, child) { - final balanceAsync = ref.watch(balanceProviderFamily(account.accountId)); - - return FutureBuilder( - future: _checksumService.getHumanReadableName(account.accountId), - builder: (context, checksumSnapshot) { - final humanChecksum = checksumSnapshot.data ?? ''; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - account.name, - style: context.themeText.paragraph?.copyWith( - color: isActive ? Colors.black : Colors.white, - ), - ), - Text( - humanChecksum, - style: context.themeText.detail?.copyWith( - color: isActive - ? context.themeColors.checksum - : context.themeColors.checksumDarker, - ), - ), - Row( + Row( + children: [ + Expanded( + child: Container( + padding: EdgeInsets.symmetric(horizontal: context.isTablet ? 20 : 8, vertical: 8), + height: context.themeSize.accountListItemHeight, + decoration: ShapeDecoration( + color: isActive ? context.themeColors.surfaceActive : context.themeColors.surface, + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, color: context.themeColors.borderLight), + borderRadius: BorderRadius.circular(5), + ), + ), + child: Row( + children: [ + const SizedBox(width: 24), + Expanded( + child: Consumer( + builder: (context, ref, child) { + final balanceAsync = ref.watch(balanceProviderFamily(account.accountId)); + + return FutureBuilder( + future: _checksumService.getHumanReadableName(account.accountId), + builder: (context, checksumSnapshot) { + final humanChecksum = checksumSnapshot.data ?? ''; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + account.name, + style: context.themeText.smallParagraph?.copyWith( + color: isActive ? Colors.black : Colors.white, + ), + ), + if (entrustedNodes.isNotEmpty) + const AccountTag('Guardian', color: Color(0xFF9747FF)), + ], + ), Text( - context.isTablet - ? account.accountId - // ignore: lines_longer_than_80_chars - : AddressFormattingService.formatAddress(account.accountId), + humanChecksum, style: context.themeText.detail?.copyWith( color: isActive - ? context.themeColors.darkGray - : context.themeColors.textMuted, + ? context.themeColors.checksumDarker + : context.themeColors.checksum, ), ), - ], - ), - const SizedBox(height: 2), - balanceAsync.when( - loading: () => Text( - 'loading balance...', - style: context.themeText.detail?.copyWith( - color: isActive ? context.themeColors.darkGray : context.themeColors.light, - ), - ), - error: (error, _) => Text( - 'error loading', - style: context.themeText.detail?.copyWith( - color: isActive - ? context.themeColors.darkGray - : context.themeColors.textPrimary, - ), - ), - data: (balance) => Text.rich( - TextSpan( + Row( children: [ - TextSpan( - text: _formattingService.formatBalance(balance), - style: context.themeText.smallParagraph?.copyWith( + Text( + context.isTablet + ? account.accountId + // ignore: lines_longer_than_80_chars + : AddressFormattingService.formatAddress(account.accountId), + style: context.themeText.tiny?.copyWith( color: isActive ? context.themeColors.darkGray - : context.themeColors.textPrimary, + : context.themeColors.textMuted, ), ), + ], + ), + const SizedBox(height: 2), + balanceAsync.when( + loading: () => Text( + 'loading balance...', + style: context.themeText.detail?.copyWith( + color: isActive + ? context.themeColors.darkGray + : context.themeColors.light, + ), + ), + error: (error, _) => Text( + 'error loading', + style: context.themeText.detail?.copyWith( + color: isActive + ? context.themeColors.darkGray + : context.themeColors.textPrimary, + ), + ), + data: (balance) => Text.rich( TextSpan( - text: ' ${AppConstants.tokenSymbol}', - style: context.themeText.detail?.copyWith( - color: isActive - ? context.themeColors.darkGray - : context.themeColors.textPrimary, - ), + children: [ + TextSpan( + text: _formattingService.formatBalance(balance), + style: context.themeText.detail?.copyWith( + color: isActive + ? context.themeColors.darkGray + : context.themeColors.textPrimary, + ), + ), + TextSpan( + text: ' ${AppConstants.tokenSymbol}', + style: context.themeText.tiny?.copyWith( + color: isActive + ? context.themeColors.darkGray + : context.themeColors.textPrimary, + ), + ), + ], ), - ], + ), ), - ), - ), - ], + ], + ); + }, ); }, - ); - }, - ), + ), + ), + ], ), - ], + ), ), - ), - ), - IconButton( - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - icon: SvgPicture.asset( - 'assets/settings_icon_off.svg', - width: context.isTablet ? 28 : 21, - colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), - ), - onPressed: () async { - // Get current data from providers - final balanceAsync = ref.read(balanceProviderFamily(account.accountId)); - final checksumName = await _checksumService.getHumanReadableName(account.accountId); - - balanceAsync.when( - loading: () { - // Show loading or handle appropriately - }, - error: (error, _) { - // Handle error + IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + icon: SvgPicture.asset( + 'assets/settings_icon_off.svg', + width: context.isTablet ? 28 : 21, + colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), + ), + onPressed: () async { + // Get current data from providers + final balanceAsync = ref.read(balanceProviderFamily(account.accountId)); + final checksumName = await _checksumService.getHumanReadableName(account.accountId); + + balanceAsync.when( + loading: () { + // Show loading or handle appropriately + }, + error: (error, _) { + // Handle error + }, + data: (balance) async { + if (!mounted) return; + await Navigator.push( + context, + MaterialPageRoute( + settings: const RouteSettings(name: AppConstants.accountSettingsRouteName), + builder: (context) => AccountSettingsScreen( + account: account, + balance: _formattingService.formatBalance(balance, addSymbol: true), + checksumName: checksumName, + ), + ), + ); + // Providers will automatically refresh if needed + }, + ); }, - data: (balance) async { - if (!mounted) return; - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => AccountSettingsScreen( - account: account, - balance: _formattingService.formatBalance(balance, addSymbol: true), - checksumName: checksumName, + ), + ], + ), + if (entrustedNodes.isNotEmpty) + ConstrainedBox( + constraints: BoxConstraints(maxHeight: constraintMaxHeight), + child: TreeListView( + showExpandCollapse: false, + nodes: entrustedNodes, + nodeBuilder: (context, node, depth) { + final entrusted = node.data; + return Row( + children: [ + Expanded( + child: Container( + decoration: ShapeDecoration( + color: context.themeColors.darkGray, + shape: RoundedRectangleBorder( + side: const BorderSide(color: Color(0x26FFFFFF)), + borderRadius: BorderRadius.circular(5), + ), + ), + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(entrusted.name, style: context.themeText.smallParagraph), + const AccountTag('Entrusted'), + ], + ), + ), ), - ), + IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + icon: SvgPicture.asset( + 'assets/settings_icon_off.svg', + width: context.isTablet ? 28 : 21, + colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), + ), + onPressed: () async { + // Get current data from providers + final balanceAsync = ref.read(balanceProviderFamily(entrusted.accountId)); + final checksumName = await _checksumService.getHumanReadableName(entrusted.accountId); + + balanceAsync.when( + loading: () {}, + error: (error, _) {}, + data: (balance) async { + if (!mounted) return; + await Navigator.push( + context, + MaterialPageRoute( + settings: const RouteSettings(name: AppConstants.accountSettingsRouteName), + builder: (context) => AccountSettingsScreen( + account: entrusted, + balance: _formattingService.formatBalance(balance, addSymbol: true), + checksumName: checksumName, + ), + ), + ); + }, + ); + }, + ), + ], ); - // Providers will automatically refresh if needed }, - ); - }, - ), + ), + ), ], ), - Positioned( // calculating the middle point top: (context.themeSize.accountListItemHeight / 2) - (context.themeSize.accountListItemLogoWidth / 2), @@ -437,3 +528,25 @@ class _AccountsScreenState extends ConsumerState { ); } } + +class AccountTag extends StatelessWidget { + final String label; + final Color? color; + + const AccountTag(this.label, {super.key, this.color}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: color ?? const Color(0xFFFFD541), // Default to Entrusted color (Yellow-ish) + borderRadius: BorderRadius.circular(4), + ), + child: Text( + label, + style: context.themeText.tiny?.copyWith(color: Colors.black, fontWeight: FontWeight.bold, fontSize: 10), + ), + ); + } +} diff --git a/mobile-app/lib/features/main/screens/high_security/guardian_account_info_sheet.dart b/mobile-app/lib/features/main/screens/high_security/guardian_account_info_sheet.dart new file mode 100644 index 00000000..ab41e26b --- /dev/null +++ b/mobile-app/lib/features/main/screens/high_security/guardian_account_info_sheet.dart @@ -0,0 +1,136 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/features/components/button.dart'; +import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart'; +import 'package:resonance_network_wallet/features/styles/app_size_theme.dart'; +import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; +import 'package:resonance_network_wallet/shared/extensions/media_query_data_extension.dart'; + +class GuardianAccountInfoSheet extends StatefulWidget { + const GuardianAccountInfoSheet({super.key}); + + @override + State createState() => _GuardianAccountInfoSheetState(); +} + +class _GuardianAccountInfoSheetState extends State { + void _closeSheet() { + Navigator.pop(context); + } + + @override + Widget build(BuildContext context) { + return SafeArea( + bottom: false, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 35, vertical: 16), + decoration: ShapeDecoration( + color: context.themeColors.background, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.all(7), + decoration: ShapeDecoration(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(100))), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + InkWell( + onTap: _closeSheet, + child: Icon(Icons.close, size: context.isTablet ? 28 : 24), + ), + ], + ), + ), + const SizedBox(height: 24), + Row( + children: [ + Icon(Icons.info_outline, size: context.themeSize.infoSheetTitleIcon), + const SizedBox(width: 22), + Text('What is a Guardian Account', style: context.themeText.largeTag), + ], + ), + const SizedBox(height: 22), + Text( + 'A Guardian account acts as a secure backstop. It intercepts transactions by diverting funds to itself if the Entrusted account (this account) is compromised. The Guardian account can be owned by you or a trusted 3rd party.', + style: context.themeText.smallParagraph, + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('A Guardian account can:', style: context.themeText.smallParagraph), + Text('• Intercept any transaction', style: context.themeText.smallParagraph), + Text('• Pull all funds from this account', style: context.themeText.smallParagraph), + Text('• Change the recovery address', style: context.themeText.smallParagraph), + ], + ), + ), + const SizedBox(height: 16), + Text( + 'The Guardian account should not be in the same wallet as the Entrusted account as in the case of theft both would be exposed.', + style: context.themeText.smallParagraph, + ), + const SizedBox(height: 16), + Text( + 'The harder the Guardian account is to access, the higher the security. An account on a cold storage wallet is the most secure.', + style: context.themeText.smallParagraph, + ), + const SizedBox(height: 40), + Button( + variant: ButtonVariant.primary, + label: 'Got it!', + onPressed: () { + _closeSheet(); + }, + ), + SizedBox(height: context.themeSize.bottomButtonSpacing), + ], + ), + ), + ); + } +} + +// Helper function to show the receive sheet +void showGuardianAccountInfoSheet(BuildContext context) { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isScrollControlled: true, + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width, // Ensure full width + ), + builder: (context) => Stack( + children: [ + Positioned.fill( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.black, const Color(0xFF312E6E).useOpacity(0.4), Colors.black], + ), + ), + ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3), + child: Container(color: Colors.black.useOpacity(0.3), child: const GuardianAccountInfoSheet()), + ), + ), + ], + ), + ); +} diff --git a/mobile-app/lib/features/main/screens/high_security/high_security_cancel_warning_sheet.dart b/mobile-app/lib/features/main/screens/high_security/high_security_cancel_warning_sheet.dart new file mode 100644 index 00000000..454c150a --- /dev/null +++ b/mobile-app/lib/features/main/screens/high_security/high_security_cancel_warning_sheet.dart @@ -0,0 +1,132 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/features/components/button.dart'; +import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart'; +import 'package:resonance_network_wallet/features/styles/app_size_theme.dart'; +import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; +import 'package:resonance_network_wallet/shared/extensions/media_query_data_extension.dart'; + +class HighSecurityCancelWarningSheet extends StatefulWidget { + const HighSecurityCancelWarningSheet({super.key}); + + @override + State createState() => _HighSecurityCancelWarningSheetState(); +} + +class _HighSecurityCancelWarningSheetState extends State { + void _returnToAccountSetting() { + if (!mounted) return; + Navigator.of(context).popUntil(ModalRoute.withName(AppConstants.accountSettingsRouteName)); + } + + void _continueSetup() { + Navigator.pop(context); + } + + @override + Widget build(BuildContext context) { + return SafeArea( + bottom: false, + child: Container( + height: MediaQuery.of(context).size.height * 0.8, + padding: const EdgeInsets.symmetric(horizontal: 35, vertical: 16), + decoration: ShapeDecoration( + color: context.themeColors.background, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), + ), + child: Column( + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.all(7), + decoration: ShapeDecoration(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(100))), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + InkWell( + onTap: _continueSetup, + child: Icon(Icons.close, size: context.isTablet ? 28 : 24), + ), + ], + ), + ), + const SizedBox(height: 69), + Text( + 'Are you sure you want to exit High Security Setup?', + textAlign: TextAlign.center, + style: context.themeText.smallTitle, + ), + const SizedBox(height: 16), + Text( + 'Your preferences will be discarded', + textAlign: TextAlign.center, + style: context.themeText.smallTitle, + ), + const SizedBox(height: 69), + Row( + spacing: context.themeSize.buttonsHorizontalSpacing, + children: [ + Expanded( + child: Button( + variant: ButtonVariant.danger, + label: 'Exit anyway', + textStyle: context.themeText.smallParagraph?.copyWith(fontWeight: FontWeight.w600), + onPressed: () { + _returnToAccountSetting(); + }, + ), + ), + Expanded( + child: Button( + variant: ButtonVariant.neutral, + label: 'Continue', + textStyle: context.themeText.smallParagraph?.copyWith(fontWeight: FontWeight.w600), + onPressed: _continueSetup, + ), + ), + ], + ), + SizedBox(height: context.themeSize.bottomButtonSpacing), + ], + ), + ), + ); + } +} + +void showHighSecurityCancelWarningSheet(BuildContext context) { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isScrollControlled: true, + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width, // Ensure full width + ), + builder: (context) => Stack( + children: [ + Positioned.fill( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.black, const Color(0xFF312E6E).useOpacity(0.4), Colors.black], + ), + ), + ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3), + child: Container(color: Colors.black.useOpacity(0.3), child: const HighSecurityCancelWarningSheet()), + ), + ), + ], + ), + ); +} diff --git a/mobile-app/lib/features/main/screens/high_security/high_security_confirmation_sheet.dart b/mobile-app/lib/features/main/screens/high_security/high_security_confirmation_sheet.dart new file mode 100644 index 00000000..f4ffceda --- /dev/null +++ b/mobile-app/lib/features/main/screens/high_security/high_security_confirmation_sheet.dart @@ -0,0 +1,195 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/features/components/button.dart'; +import 'package:resonance_network_wallet/features/main/screens/high_security/high_security_cancel_warning_sheet.dart'; +import 'package:resonance_network_wallet/features/main/screens/high_security/high_security_created_sheet.dart'; +import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart'; +import 'package:resonance_network_wallet/features/styles/app_size_theme.dart'; +import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; +import 'package:resonance_network_wallet/providers/high_security_form_provider.dart'; +import 'package:resonance_network_wallet/shared/extensions/media_query_data_extension.dart'; + +class HighSecurityConfirmationSheet extends ConsumerStatefulWidget { + const HighSecurityConfirmationSheet({super.key}); + + @override + ConsumerState createState() => _HighSecurityConfirmationSheetState(); +} + +class _HighSecurityConfirmationSheetState extends ConsumerState { + final HighSecurityService _highSecurityService = HighSecurityService(); + final SettingsService _settingsService = SettingsService(); + + BigInt? _networkFee; + bool _isSubmitting = false; + + void _confirmSetup() async { + setState(() { + _isSubmitting = true; + }); + + final formData = ref.read(highSecurityFormProvider); + final activeAccount = (await _settingsService.getActiveAccount())!; + + await _highSecurityService.setupHighSecurityAccount(activeAccount, formData); + + setState(() { + _isSubmitting = false; + }); + + if (mounted) { + showHighSecurityCreatedSheet(context); + } + } + + void _cancelSetup() { + showHighSecurityCancelWarningSheet(context); + } + + void _fetchNetworkFee() async { + final activeAccount = (await _settingsService.getActiveAccount())!; + final formData = ref.read(highSecurityFormProvider); + + final fee = await _highSecurityService.getHighSecuritySetupFee(activeAccount, formData); + + setState(() { + _networkFee = fee.fee; + }); + } + + @override + void initState() { + super.initState(); + + _fetchNetworkFee(); + } + + @override + Widget build(BuildContext context) { + return SafeArea( + bottom: false, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 35, vertical: 16), + decoration: ShapeDecoration( + color: context.themeColors.background, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.all(7), + decoration: ShapeDecoration(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(100))), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + InkWell( + onTap: _cancelSetup, + child: Icon(Icons.close, size: context.isTablet ? 28 : 24), + ), + ], + ), + ), + const SizedBox(height: 24), + Text('WARNING:', style: context.themeText.largeTitle?.copyWith(color: context.themeColors.buttonDanger)), + const SizedBox(height: 11), + Text( + 'These features are designed to help keep your funds safer, but once confirmed this account CANNOT:', + style: context.themeText.largeTag, + ), + const SizedBox(height: 11), + SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('• Turn off High Security', style: context.themeText.smallParagraph), + Text('• Reverse a transaction', style: context.themeText.smallParagraph), + Text('• Change the Guardian account', style: context.themeText.smallParagraph), + Text('• Change the Recovery account', style: context.themeText.smallParagraph), + Text('• Change the Safeguard window', style: context.themeText.smallParagraph), + Text('• Deny a recovery request', style: context.themeText.smallParagraph), + ], + ), + ), + const SizedBox(height: 11), + Text( + 'You will still be able to send and receive crypto and view requests.', + style: context.themeText.smallParagraph, + ), + const SizedBox(height: 116), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Network Fee', style: context.themeText.detail?.copyWith(fontWeight: FontWeight.w600)), + Text( + '${_networkFee ?? 'Fetching...'} ${AppConstants.tokenSymbol}', + style: context.themeText.detail?.copyWith(fontWeight: FontWeight.w600), + ), + ], + ), + const SizedBox(height: 13), + Button( + isLoading: _isSubmitting, + variant: ButtonVariant.primary, + label: 'Confirm', + onPressed: () { + _confirmSetup(); + }, + ), + const SizedBox(height: 13), + Button( + isDisabled: _isSubmitting, + variant: ButtonVariant.dangerOutline, + label: 'Cancel', + onPressed: () { + _cancelSetup(); + }, + ), + SizedBox(height: context.themeSize.bottomButtonSpacing), + ], + ), + ), + ); + } +} + +void showHighSecurityConfirmationSheet(BuildContext context) { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isScrollControlled: true, + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width, // Ensure full width + ), + builder: (context) => Stack( + children: [ + Positioned.fill( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.black, const Color(0xFF312E6E).useOpacity(0.4), Colors.black], + ), + ), + ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3), + child: Container(color: Colors.black.useOpacity(0.3), child: const HighSecurityConfirmationSheet()), + ), + ), + ], + ), + ); +} diff --git a/mobile-app/lib/features/main/screens/high_security/high_security_created_sheet.dart b/mobile-app/lib/features/main/screens/high_security/high_security_created_sheet.dart new file mode 100644 index 00000000..ea9fad0f --- /dev/null +++ b/mobile-app/lib/features/main/screens/high_security/high_security_created_sheet.dart @@ -0,0 +1,111 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/features/components/button.dart'; +import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart'; +import 'package:resonance_network_wallet/features/styles/app_size_theme.dart'; +import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; +import 'package:resonance_network_wallet/shared/extensions/media_query_data_extension.dart'; + +class HighSecurityCreatedSheet extends StatefulWidget { + const HighSecurityCreatedSheet({super.key}); + + @override + State createState() => _HighSecurityCreatedSheetState(); +} + +class _HighSecurityCreatedSheetState extends State { + void _returnToAccountSetting() { + if (!mounted) return; + Navigator.of(context).popUntil(ModalRoute.withName(AppConstants.accountSettingsRouteName)); + } + + @override + Widget build(BuildContext context) { + return SafeArea( + bottom: false, + child: Container( + height: MediaQuery.of(context).size.height * 0.8, + padding: const EdgeInsets.symmetric(horizontal: 35, vertical: 16), + decoration: ShapeDecoration( + color: context.themeColors.background, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), + ), + child: Column( + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.all(7), + decoration: ShapeDecoration(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(100))), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + InkWell( + onTap: _returnToAccountSetting, + child: Icon(Icons.close, size: context.isTablet ? 28 : 24), + ), + ], + ), + ), + const SizedBox(height: 69), + SvgPicture.asset('assets/logo/logo.svg', width: 91, height: 85), + const SizedBox(height: 18), + Text('CONFIRMED', style: context.themeText.largeTitle), + const SizedBox(height: 46), + Text( + 'High Security has been successfully setup on this account', + style: context.themeText.largeTag, + textAlign: TextAlign.center, + ), + const SizedBox(height: 46), + Button(variant: ButtonVariant.neutral, label: 'Done', width: 188, onPressed: _returnToAccountSetting), + SizedBox(height: context.themeSize.bottomButtonSpacing), + ], + ), + ), + ); + } +} + +void showHighSecurityCreatedSheet(BuildContext context) { + void returnToAccountSetting() { + Navigator.of(context).popUntil(ModalRoute.withName(AppConstants.accountSettingsRouteName)); + } + + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isScrollControlled: true, + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width, // Ensure full width + ), + builder: (context) => Stack( + children: [ + Positioned.fill( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.black, const Color(0xFF312E6E).useOpacity(0.4), Colors.black], + ), + ), + ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3), + child: Container(color: Colors.black.useOpacity(0.3), child: const HighSecurityCreatedSheet()), + ), + ), + ], + ), + ).whenComplete(() { + returnToAccountSetting(); + }); +} diff --git a/mobile-app/lib/features/main/screens/high_security/high_security_get_started_screen.dart b/mobile-app/lib/features/main/screens/high_security/high_security_get_started_screen.dart new file mode 100644 index 00000000..6d7cffdc --- /dev/null +++ b/mobile-app/lib/features/main/screens/high_security/high_security_get_started_screen.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:resonance_network_wallet/features/components/button.dart'; +import 'package:resonance_network_wallet/features/components/scaffold_base.dart'; +import 'package:resonance_network_wallet/features/components/wallet_app_bar.dart'; +import 'package:resonance_network_wallet/features/main/screens/high_security/high_security_guardian_wizard.dart'; +import 'package:resonance_network_wallet/features/styles/app_size_theme.dart'; +import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; +import 'package:resonance_network_wallet/providers/high_security_form_provider.dart'; + +class HighSecurityGetStartedScreen extends ConsumerWidget { + const HighSecurityGetStartedScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final formNotifier = ref.read(highSecurityFormProvider.notifier); + + return ScaffoldBase( + appBar: WalletAppBar.simpleWithBackButton(title: 'Security Settings'), + child: Column( + children: [ + const SizedBox(height: 73), + SvgPicture.asset('assets/high_security/security_icon_big.svg', width: 140, height: 175), + const SizedBox(height: 26), + Text('HIGH SECURITY', style: context.themeText.largeTitle), + const SizedBox(height: 25), + Text( + "Don't risk your funds!\nEnabling High Security is a great way to keep your money safe. But safety comes at the cost of convenience.", + textAlign: TextAlign.center, + style: context.themeText.paragraph, + ), + const SizedBox(height: 13), + Text( + 'Once you enable this feature it cannot be disabled', + textAlign: TextAlign.center, + style: context.themeText.paragraph?.copyWith(fontWeight: FontWeight.w600), + ), + const Expanded(child: SizedBox()), + Button( + variant: ButtonVariant.neutral, + label: 'Start', + onPressed: () { + formNotifier.resetState(); + Navigator.push(context, MaterialPageRoute(builder: (context) => const HighSecurityGuardianWizard())); + }, + ), + SizedBox(height: context.themeSize.bottomButtonSpacing), + ], + ), + ); + } +} diff --git a/mobile-app/lib/features/main/screens/high_security/high_security_guardian_wizard.dart b/mobile-app/lib/features/main/screens/high_security/high_security_guardian_wizard.dart new file mode 100644 index 00000000..bf71b6b5 --- /dev/null +++ b/mobile-app/lib/features/main/screens/high_security/high_security_guardian_wizard.dart @@ -0,0 +1,162 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/features/components/button.dart'; +import 'package:resonance_network_wallet/features/components/custom_text_field.dart'; +import 'package:resonance_network_wallet/features/components/gradient_text.dart'; +import 'package:resonance_network_wallet/features/components/scaffold_base.dart'; +import 'package:resonance_network_wallet/features/components/steps.dart'; +import 'package:resonance_network_wallet/features/components/wallet_action_button.dart'; +import 'package:resonance_network_wallet/features/components/wallet_app_bar.dart'; +import 'package:resonance_network_wallet/features/main/screens/high_security/guardian_account_info_sheet.dart'; +import 'package:resonance_network_wallet/features/main/screens/high_security/high_security_safeguard_window_wizard.dart'; +import 'package:resonance_network_wallet/features/main/screens/send/qr_scanner_screen.dart'; +import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart'; +import 'package:resonance_network_wallet/features/styles/app_size_theme.dart'; +import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; +import 'package:resonance_network_wallet/providers/high_security_form_provider.dart'; + +class HighSecurityGuardianWizard extends ConsumerStatefulWidget { + const HighSecurityGuardianWizard({super.key}); + + @override + ConsumerState createState() => _HighSecurityGuardianWizardState(); +} + +class _HighSecurityGuardianWizardState extends ConsumerState { + Future _scanQRCode() async { + final formNotifier = ref.read(highSecurityFormProvider.notifier); + + final scannedAddress = await Navigator.push( + context, + MaterialPageRoute(builder: (context) => const QRScannerScreen(), fullscreenDialog: true), + ); + + if (scannedAddress != null && mounted) { + formNotifier.updateGuardianAddress(scannedAddress); + } + } + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final formNotifier = ref.read(highSecurityFormProvider.notifier); + final guardianAddress = ref.watch(highSecurityFormProvider).guardianAddress; + + final bool isDisabled = guardianAddress.isEmpty; + + return ScaffoldBase( + appBar: WalletAppBar.simpleWithBackButton(title: 'Theft Deterrence'), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 36), + const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 204, + child: StepsIndicator(currentStep: 1, totalSteps: AppConstants.highSecurityStepsCount), + ), + ], + ), + const SizedBox(height: 32), + GradientText('THEFT DETERRENCE', colors: context.themeColors.aquaBlue, style: context.themeText.largeTitle), + const SizedBox(height: 4), + Text( + 'Intercept any transaction or “pull” all funds in the case of theft.', + style: context.themeText.smallParagraph, + ), + const SizedBox(height: 38), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Guardian Account', style: context.themeText.largeTag), + InkWell( + onTap: () { + showGuardianAccountInfoSheet(context); + }, + child: const Icon(Icons.info_outline), + ), + ], + ), + const SizedBox(height: 4), + Text( + 'Choose an account that keeps your funds safe if your main wallet is compromised.', + style: context.themeText.smallParagraph, + ), + const SizedBox(height: 13), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + GestureDetector( + onTap: () async { + final data = await Clipboard.getData('text/plain'); + if (data != null && data.text != null) { + formNotifier.updateGuardianAddress(data.text!); + } + }, + child: const WalletActionButton(assetPath: 'assets/paste_icon_1.svg'), + ), + const SizedBox(width: 12), + GestureDetector( + onTap: _scanQRCode, + child: const WalletActionButton(assetPath: 'assets/scan_1.svg'), + ), + ], + ), + const SizedBox(height: 13), + CustomTextField( + variant: TextFieldVariant.secondary, + initialValue: guardianAddress, + onChanged: formNotifier.updateGuardianAddress, + hintText: 'Enter address', + ), + const SizedBox(height: 13), + Text( + 'The harder the Guardian account is to access the higher the security. An address on a cold storage wallet is the most secure.', + style: context.themeText.smallParagraph?.copyWith(color: context.themeColors.textMuted), + ), + const Expanded(child: SizedBox()), + Row( + spacing: 36, + children: [ + Expanded( + child: Button( + label: 'Back', + onPressed: () { + Navigator.pop(context); + }, + ), + ), + Expanded( + child: Button( + isDisabled: isDisabled, + variant: ButtonVariant.neutral, + label: 'Next', + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const HighSecuritySafeguardWindowWizard()), + ); + }, + ), + ), + ], + ), + SizedBox(height: context.themeSize.bottomButtonSpacing), + ], + ), + ); + } +} diff --git a/mobile-app/lib/features/main/screens/high_security/high_security_safeguard_window_wizard.dart b/mobile-app/lib/features/main/screens/high_security/high_security_safeguard_window_wizard.dart new file mode 100644 index 00000000..03a41798 --- /dev/null +++ b/mobile-app/lib/features/main/screens/high_security/high_security_safeguard_window_wizard.dart @@ -0,0 +1,141 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/features/components/button.dart'; +import 'package:resonance_network_wallet/features/components/gradient_text.dart'; +import 'package:resonance_network_wallet/features/components/scaffold_base.dart'; +import 'package:resonance_network_wallet/features/components/steps.dart'; +import 'package:resonance_network_wallet/features/components/wallet_app_bar.dart'; +import 'package:resonance_network_wallet/features/main/screens/high_security/high_security_summary_wizard.dart'; +import 'package:resonance_network_wallet/features/main/screens/high_security/safeguard_window_picker_sheet.dart'; +import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart'; +import 'package:resonance_network_wallet/features/styles/app_size_theme.dart'; +import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; +import 'package:resonance_network_wallet/providers/high_security_form_provider.dart'; +import 'package:resonance_network_wallet/shared/extensions/media_query_data_extension.dart'; + +class HighSecuritySafeguardWindowWizard extends ConsumerStatefulWidget { + const HighSecuritySafeguardWindowWizard({super.key}); + + @override + ConsumerState createState() => _HighSecuritySafeguardWindowWizardState(); +} + +class _HighSecuritySafeguardWindowWizardState extends ConsumerState { + @override + Widget build(BuildContext context) { + final formNotifier = ref.read(highSecurityFormProvider.notifier); + final safeguardTimeSeconds = ref.watch(highSecurityFormProvider).safeguardWindow; + + final int secondsInADay = 86400; + final int secondsInAMonth = secondsInADay * 30; // 86400 seconds/day * 30 days/month + + /// This is an approximation. + final int safeguardTimeMonths = safeguardTimeSeconds ~/ secondsInAMonth; + final int safeguardTimeDays = (safeguardTimeSeconds % secondsInAMonth) ~/ secondsInADay; + final int safeguardTimeHours = (safeguardTimeSeconds % secondsInADay) ~/ 3600; + + final bool isDisabled = safeguardTimeSeconds == 0; + + return ScaffoldBase( + appBar: WalletAppBar.simpleWithBackButton(title: 'Safeguard Window'), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 36), + const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 204, + child: StepsIndicator(currentStep: 2, totalSteps: AppConstants.highSecurityStepsCount), + ), + ], + ), + const SizedBox(height: 32), + GradientText('SAFEGUARD WINDOW', colors: context.themeColors.aquaBlue, style: context.themeText.largeTitle), + const SizedBox(height: 4), + Text( + 'The time window in which the Guardian can deny or intercept a transaction.', + style: context.themeText.smallParagraph, + ), + const SizedBox(height: 38), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [Text('Safeguard Window', style: context.themeText.largeTag)], + ), + const SizedBox(height: 4), + Text( + 'Set how long the Guardian account has to act once a transaction is initiated.', + style: context.themeText.smallParagraph, + ), + const SizedBox(height: 13), + GestureDetector( + onTap: () { + showSafeguardWindowPickerSheet( + context, + safeguardTimeMonths: safeguardTimeMonths, + safeguardTimeDays: safeguardTimeDays, + safeguardTimeHours: safeguardTimeHours, + setSafeguardTimeSeconds: formNotifier.updateSafeguardWindow, + ); + }, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + decoration: ShapeDecoration( + color: const Color(0xFF313131), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + DatetimeFormattingService.formatSafeguardTime( + safeguardTimeMonths, + safeguardTimeDays, + safeguardTimeHours, + ), + style: context.themeText.smallParagraph, + ), + Icon(Icons.edit, color: Colors.white70, size: context.isTablet ? 22 : 14), + ], + ), + ), + ), + const SizedBox(height: 13), + Text( + 'Allow a reasonable window for your Guardian account to respond in an emergency.', + style: context.themeText.smallParagraph?.copyWith(color: context.themeColors.textMuted), + ), + const Expanded(child: SizedBox()), + Row( + spacing: 36, + children: [ + Expanded( + child: Button( + label: 'Back', + onPressed: () { + Navigator.pop(context); + }, + ), + ), + Expanded( + child: Button( + isDisabled: isDisabled, + variant: ButtonVariant.neutral, + label: 'Next', + onPressed: () { + Navigator.push(context, MaterialPageRoute(builder: (context) => const HighSecuritySummaryWizard())); + }, + ), + ), + ], + ), + SizedBox(height: context.themeSize.bottomButtonSpacing), + ], + ), + ); + } +} diff --git a/mobile-app/lib/features/main/screens/high_security/high_security_summary_wizard.dart b/mobile-app/lib/features/main/screens/high_security/high_security_summary_wizard.dart new file mode 100644 index 00000000..d7d9508b --- /dev/null +++ b/mobile-app/lib/features/main/screens/high_security/high_security_summary_wizard.dart @@ -0,0 +1,157 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/features/components/button.dart'; +import 'package:resonance_network_wallet/features/components/gradient_text.dart'; +import 'package:resonance_network_wallet/features/components/scaffold_base.dart'; +import 'package:resonance_network_wallet/features/components/steps.dart'; +import 'package:resonance_network_wallet/features/components/wallet_app_bar.dart'; +import 'package:resonance_network_wallet/features/main/screens/high_security/high_security_confirmation_sheet.dart'; +import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart'; +import 'package:resonance_network_wallet/features/styles/app_size_theme.dart'; +import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; +import 'package:resonance_network_wallet/providers/high_security_form_provider.dart'; + +class HighSecuritySummaryWizard extends ConsumerStatefulWidget { + const HighSecuritySummaryWizard({super.key}); + + @override + ConsumerState createState() => _HighSecuritySummaryWizardState(); +} + +class _HighSecuritySummaryWizardState extends ConsumerState { + final HumanReadableChecksumService _humanReadableChecksumService = HumanReadableChecksumService(); + + @override + Widget build(BuildContext context) { + final formData = ref.read(highSecurityFormProvider); + + final guardianChecksumFuture = _humanReadableChecksumService.getHumanReadableName(formData.guardianAddress); + + return ScaffoldBase( + appBar: WalletAppBar.simpleWithBackButton(title: 'Summary'), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 36), + const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 204, + child: StepsIndicator(currentStep: 3, totalSteps: AppConstants.highSecurityStepsCount), + ), + ], + ), + const SizedBox(height: 32), + GradientText('SUMMARY', colors: context.themeColors.aquaBlue, style: context.themeText.largeTitle), + const SizedBox(height: 19), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 4, + children: [ + Text('HIGH SECURITY ACCOUNT:', style: context.themeText.detail), + Text('Everyday Account', style: context.themeText.smallTitle), + Text( + 'Grain-Red-Flash-Hyper-Cloud', + style: context.themeText.smallParagraph?.copyWith(color: context.themeColors.checksumDarker), + ), + SizedBox( + width: 220, + child: Text( + '5FEUm MJ6w5 36upW fhFcK n61jN UniW3 norvT ULjwj MhbfN cs4N', + style: context.themeText.detail?.copyWith(color: Colors.white.useOpacity(0.6000000238418579)), + ), + ), + ], + ), + const SizedBox(height: 19), + SummaryCard( + type: SummaryType.guardian, + checksumFuture: guardianChecksumFuture, + address: AddressFormattingService.splitIntoChunks(formData.guardianAddress).join(' '), + ), + const Expanded(child: SizedBox()), + Row( + spacing: 36, + children: [ + Expanded( + child: Button( + label: 'Back', + onPressed: () { + Navigator.pop(context); + }, + ), + ), + Expanded( + child: Button( + variant: ButtonVariant.neutral, + label: 'Next', + onPressed: () { + showHighSecurityConfirmationSheet(context); + }, + ), + ), + ], + ), + SizedBox(height: context.themeSize.bottomButtonSpacing), + ], + ), + ); + } +} + +enum SummaryType { guardian, recovery } + +class SummaryCard extends StatelessWidget { + final SummaryType type; + final Future checksumFuture; + final String address; + + const SummaryCard({super.key, required this.type, required this.checksumFuture, required this.address}); + + @override + Widget build(BuildContext context) { + final String label = type == SummaryType.guardian ? 'GUARDIAN ACCOUNT:' : 'RECOVERY ACCOUNT:'; + final Color checksumColor = type == SummaryType.guardian + ? context.themeColors.yellow + : context.themeColors.buttonDanger; + + return Container( + width: double.infinity, + padding: const EdgeInsets.all(10), + decoration: ShapeDecoration( + color: const Color(0x99313131), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 2, + children: [ + Text(label, style: context.themeText.detail), + FutureBuilder( + future: checksumFuture, + builder: (context, snapshot) { + String text = 'Loading checksum...'; + + if (snapshot.error != null) { + text = 'Failed loading checksum: ${snapshot.error}'; + } else if (snapshot.hasData) { + text = snapshot.data!; + } + + return Text(text, style: context.themeText.smallParagraph?.copyWith(color: checksumColor)); + }, + ), + SizedBox( + width: 220, + child: Text( + address, + style: context.themeText.detail?.copyWith(color: Colors.white.useOpacity(0.6000000238418579)), + ), + ), + ], + ), + ); + } +} diff --git a/mobile-app/lib/features/main/screens/high_security/safeguard_window_picker_sheet.dart b/mobile-app/lib/features/main/screens/high_security/safeguard_window_picker_sheet.dart new file mode 100644 index 00000000..4b8c6bf8 --- /dev/null +++ b/mobile-app/lib/features/main/screens/high_security/safeguard_window_picker_sheet.dart @@ -0,0 +1,239 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:resonance_network_wallet/features/components/app_modal_bottom_sheet.dart'; +import 'package:resonance_network_wallet/features/components/button.dart'; +import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart'; +import 'package:resonance_network_wallet/features/styles/app_size_theme.dart'; +import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; + +class SafeguardWindowPickerSheet extends StatelessWidget { + final int safeguardTimeMonths; + final int safeguardTimeDays; + final int safeguardTimeHours; + final Function(int) setSafeguardTimeSeconds; + + const SafeguardWindowPickerSheet({ + super.key, + required this.safeguardTimeMonths, + required this.safeguardTimeDays, + required this.safeguardTimeHours, + required this.setSafeguardTimeSeconds, + }); + + @override + Widget build(BuildContext context) { + var selectedMonths = safeguardTimeMonths; + var selectedDays = safeguardTimeDays; + var selectedHours = safeguardTimeHours; + + return Container( + height: MediaQuery.of(context).size.height * 0.75, + padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 60), + decoration: const BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Column( + children: [ + // Header + Column( + children: [ + SvgPicture.asset('assets/hourglass.svg', width: 29), + const SizedBox(height: 16), + Text( + 'Set Safeguard Window', + textAlign: TextAlign.center, + style: context.themeText.smallTitle?.copyWith(color: context.themeColors.checksum), + ), + const SizedBox(height: 4), + SizedBox( + width: context.themeSize.timePickerSubtitleWidth, + child: Text( + 'The Guardian can intercept a transaction during this period', + textAlign: TextAlign.center, + style: context.themeText.detail, + ), + ), + ], + ), + + const SizedBox(height: 20), + + // Time pickers + Expanded( + child: Row( + children: [ + // Months + Expanded( + child: Column( + children: [ + Text('Months', style: context.themeText.largeTag?.copyWith(color: context.themeColors.textMuted)), + const SizedBox(height: 8), + Expanded( + child: Row( + children: [ + Expanded( + child: CupertinoPicker( + scrollController: FixedExtentScrollController(initialItem: selectedMonths), + itemExtent: 40, + onSelectedItemChanged: (index) => selectedMonths = index, + children: List.generate( + 13, + (index) => Center( + child: Text( + index.toString().padLeft(2, '0'), + style: const TextStyle( + color: Colors.white, + fontSize: 28, + fontFamily: 'Fira Code', + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + ), + const Text(':', style: TextStyle(color: Colors.white, fontSize: 28)), + ], + ), + ), + ], + ), + ), + // Days + Expanded( + child: Column( + children: [ + Text('Days', style: context.themeText.largeTag?.copyWith(color: context.themeColors.textMuted)), + const SizedBox(height: 8), + Expanded( + child: Row( + children: [ + Expanded( + child: CupertinoPicker( + scrollController: FixedExtentScrollController(initialItem: selectedDays), + itemExtent: 40, + onSelectedItemChanged: (index) => selectedDays = index, + children: List.generate( + 30, + (index) => Center( + child: Text( + index.toString().padLeft(2, '0'), + style: const TextStyle( + color: Colors.white, + fontSize: 28, + fontFamily: 'Fira Code', + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + ), + const Text(':', style: TextStyle(color: Colors.white, fontSize: 28)), + ], + ), + ), + ], + ), + ), + // Hours + Expanded( + child: Column( + children: [ + Text('Hours', style: context.themeText.largeTag?.copyWith(color: context.themeColors.textMuted)), + const SizedBox(height: 8), + Expanded( + child: CupertinoPicker( + scrollController: FixedExtentScrollController(initialItem: selectedHours), + itemExtent: 40, + onSelectedItemChanged: (index) => selectedHours = index, + children: List.generate( + 24, + (index) => Center( + child: Text( + index.toString().padLeft(2, '0'), + style: const TextStyle( + color: Colors.white, + fontSize: 28, + fontFamily: 'Fira Code', + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + + const SizedBox(height: 20), + + // Action buttons + Row( + children: [ + Expanded( + child: Button( + variant: ButtonVariant.neutral, + label: 'Cancel', + textStyle: context.themeText.paragraph?.copyWith( + color: context.themeColors.textSecondary, + fontWeight: FontWeight.w600, + ), + onPressed: () { + Navigator.pop(context); + }, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Button( + variant: ButtonVariant.success, + label: 'Set', + textStyle: context.themeText.paragraph?.copyWith( + color: context.themeColors.textSecondary, + fontWeight: FontWeight.w600, + ), + onPressed: () { + final int secondsInAMonth = 86400 * 30; // 86400 seconds/day * 30 days/month + final newTimeSeconds = + (selectedMonths * secondsInAMonth) + (selectedDays * 86400) + (selectedHours * 3600); + + setSafeguardTimeSeconds(newTimeSeconds); + Navigator.pop(context); + }, + ), + ), + ], + ), + const SizedBox(height: 35), + ], + ), + ); + } +} + +void showSafeguardWindowPickerSheet( + BuildContext context, { + required int safeguardTimeMonths, + required int safeguardTimeDays, + required int safeguardTimeHours, + + required Function(int) setSafeguardTimeSeconds, +}) { + showAppModalBottomSheet( + context: context, + builder: (context) => SafeguardWindowPickerSheet( + safeguardTimeMonths: safeguardTimeMonths, + safeguardTimeDays: safeguardTimeDays, + safeguardTimeHours: safeguardTimeHours, + + setSafeguardTimeSeconds: setSafeguardTimeSeconds, + ), + ); +} diff --git a/mobile-app/lib/features/styles/app_colors_theme.dart b/mobile-app/lib/features/styles/app_colors_theme.dart index 637f9dbb..f5dab625 100644 --- a/mobile-app/lib/features/styles/app_colors_theme.dart +++ b/mobile-app/lib/features/styles/app_colors_theme.dart @@ -8,6 +8,7 @@ class AppColorsTheme extends ThemeExtension { final Color secondary; // What we use + final List aquaBlue; final Color purple; final Color pink; final Color yellow; @@ -43,6 +44,7 @@ class AppColorsTheme extends ThemeExtension { required this.primary, required this.secondary, + required this.aquaBlue, required this.purple, required this.pink, required this.yellow, @@ -80,6 +82,7 @@ class AppColorsTheme extends ThemeExtension { primary: const Color(0xFF6B46C1), secondary: const Color(0xFF9F7AEA), + aquaBlue: const [Color(0xFF16CECE), Color(0xFF0000FF)], purple: const Color(0xFFB259F2), pink: const Color(0xFFED4CCE), yellow: const Color(0xFFFFE91F), @@ -117,6 +120,7 @@ class AppColorsTheme extends ThemeExtension { primary: const Color(0xFF6B46C1), secondary: const Color(0xFF9F7AEA), + aquaBlue: const [Color(0xFF16CECE), Color(0xFF0000FF)], purple: const Color(0xFFB259F2), pink: const Color(0xFFED4CCE), yellow: const Color(0xFFFFE91F), @@ -153,6 +157,7 @@ class AppColorsTheme extends ThemeExtension { AppColorsTheme copyWith({ Color? primary, Color? secondary, + List? aquaBlue, Color? purple, Color? pink, Color? yellow, @@ -188,6 +193,7 @@ class AppColorsTheme extends ThemeExtension { return AppColorsTheme( primary: primary ?? this.primary, secondary: secondary ?? this.secondary, + aquaBlue: aquaBlue ?? this.aquaBlue, purple: purple ?? this.purple, pink: pink ?? this.pink, yellow: yellow ?? this.yellow, @@ -227,6 +233,7 @@ class AppColorsTheme extends ThemeExtension { return AppColorsTheme( primary: Color.lerp(primary, other.primary, t) ?? primary, secondary: Color.lerp(secondary, other.secondary, t) ?? secondary, + aquaBlue: other.aquaBlue, purple: Color.lerp(purple, other.purple, t) ?? purple, pink: Color.lerp(pink, other.pink, t) ?? pink, yellow: Color.lerp(yellow, other.yellow, t) ?? yellow, diff --git a/mobile-app/lib/features/styles/app_size_theme.dart b/mobile-app/lib/features/styles/app_size_theme.dart index bbabb172..9da58c2e 100644 --- a/mobile-app/lib/features/styles/app_size_theme.dart +++ b/mobile-app/lib/features/styles/app_size_theme.dart @@ -27,6 +27,8 @@ class AppSizeTheme extends ThemeExtension { final double pasteIconSize; final double timePickerSubtitleWidth; final double bottomButtonSpacing; + final double buttonsHorizontalSpacing; + final double infoSheetTitleIcon; const AppSizeTheme({ required this.logoHeight, @@ -54,6 +56,8 @@ class AppSizeTheme extends ThemeExtension { required this.pasteIconSize, required this.timePickerSubtitleWidth, required this.bottomButtonSpacing, + required this.buttonsHorizontalSpacing, + required this.infoSheetTitleIcon, }); const AppSizeTheme.defaultTheme() @@ -83,6 +87,8 @@ class AppSizeTheme extends ThemeExtension { pasteIconSize: 18.0, timePickerSubtitleWidth: 249, bottomButtonSpacing: 16, + buttonsHorizontalSpacing: 28, + infoSheetTitleIcon: 25, ); const AppSizeTheme.iPad() @@ -112,6 +118,8 @@ class AppSizeTheme extends ThemeExtension { pasteIconSize: 24.0, timePickerSubtitleWidth: 400, bottomButtonSpacing: 16, + buttonsHorizontalSpacing: 28, + infoSheetTitleIcon: 28, ); @override @@ -170,6 +178,8 @@ class AppSizeTheme extends ThemeExtension { pasteIconSize: pasteIconSize ?? this.pasteIconSize, timePickerSubtitleWidth: timePickerSubtitleWidth ?? this.timePickerSubtitleWidth, bottomButtonSpacing: bottomButtonSpacing ?? this.bottomButtonSpacing, + buttonsHorizontalSpacing: buttonsHorizontalSpacing ?? this.buttonsHorizontalSpacing, + infoSheetTitleIcon: infoSheetTitleIcon ?? this.infoSheetTitleIcon, ); } @@ -206,6 +216,9 @@ class AppSizeTheme extends ThemeExtension { pasteIconSize: pasteIconSize + (other.pasteIconSize - pasteIconSize) * t, timePickerSubtitleWidth: timePickerSubtitleWidth + (other.timePickerSubtitleWidth - timePickerSubtitleWidth) * t, bottomButtonSpacing: bottomButtonSpacing + (other.bottomButtonSpacing - bottomButtonSpacing) * t, + buttonsHorizontalSpacing: + buttonsHorizontalSpacing + (other.buttonsHorizontalSpacing - buttonsHorizontalSpacing) * t, + infoSheetTitleIcon: infoSheetTitleIcon + (other.infoSheetTitleIcon - infoSheetTitleIcon) * t, ); } } diff --git a/mobile-app/lib/providers/entrusted_account_provider.dart b/mobile-app/lib/providers/entrusted_account_provider.dart new file mode 100644 index 00000000..cf9f4651 --- /dev/null +++ b/mobile-app/lib/providers/entrusted_account_provider.dart @@ -0,0 +1,31 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:quantus_sdk/quantus_sdk.dart'; + +final entrustedAccountsProvider = FutureProvider.family, Account>((ref, account) async { + // TODO: Implement actual fetching of entrusted accounts from SDK/API + // For now we simulate the delay and return empty list or dummy data + + await Future.delayed(const Duration(milliseconds: 500)); + + // Dummy data logic for demonstration/development + // If you want to see the UI, you can uncomment this or use a specific account ID + if (account.name.startsWith('G')) { + // arbitrary condition for testing + return [ + const Account( + walletIndex: 0, + index: 0, + name: 'Entrusted Account 1', + accountId: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', + ), + const Account( + walletIndex: 0, + index: 0, + name: 'Zander Sky', + accountId: '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty', + ), + ]; + } + + return []; +}); diff --git a/mobile-app/lib/providers/high_security_form_provider.dart b/mobile-app/lib/providers/high_security_form_provider.dart new file mode 100644 index 00000000..c0b44207 --- /dev/null +++ b/mobile-app/lib/providers/high_security_form_provider.dart @@ -0,0 +1,22 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:quantus_sdk/quantus_sdk.dart'; + +class HighSecurityFormNotifier extends StateNotifier { + HighSecurityFormNotifier() : super(const HighSecurityData()); + void updateGuardianAddress(String address) { + state = state.copyWith(guardianAddress: address); + } + + void updateSafeguardWindow(int window) { + state = state.copyWith(safeguardWindow: window); + } + + void resetState() { + state = const HighSecurityData(); + } +} + +// Provider +final highSecurityFormProvider = StateNotifierProvider((ref) { + return HighSecurityFormNotifier(); +}); diff --git a/mobile-app/lib/utils/feature_flags.dart b/mobile-app/lib/utils/feature_flags.dart index 0f93d5f5..07623f6d 100644 --- a/mobile-app/lib/utils/feature_flags.dart +++ b/mobile-app/lib/utils/feature_flags.dart @@ -1,5 +1,6 @@ // Simple feature flags for things we want in the code but not yet in the production app class FeatureFlags { static const bool enableTestButtons = false; // Only show in debug mode - static const bool showKeystoneHardwareWallet = false; // turn keystone hw wallet on and off + static const bool enableKeystoneHardwareWallet = false; // turn keystone hw wallet on and off + static const bool enableHighSecurity = false; // turn keystone hw wallet on and off } diff --git a/mobile-app/pubspec.yaml b/mobile-app/pubspec.yaml index a8992a56..14ebec12 100644 --- a/mobile-app/pubspec.yaml +++ b/mobile-app/pubspec.yaml @@ -46,7 +46,7 @@ dependencies: share_plus: ^12.0.1 flutter_riverpod: ^2.6.1 riverpod_annotation: ^2.6.1 - telemetrydecksdk: ^2.5.0 + telemetrydecksdk: ^3.0.0 async: ^2.13.0 app_links: ^6.4.1 flutter_dotenv: ^5.1.0 @@ -94,6 +94,9 @@ flutter: - assets/qq-logo.png - assets/navbar/qcat_navbar_icon.png - assets/notification/notification_top_icon.png + - assets/high_security/security_icon_big.svg + - assets/high_security/step_indicator_active_icon.svg + - assets/high_security/step_indicator_icon.svg fonts: - family: Fira Code diff --git a/quantus_sdk/lib/quantus_sdk.dart b/quantus_sdk/lib/quantus_sdk.dart index 132f6652..70c67a1f 100644 --- a/quantus_sdk/lib/quantus_sdk.dart +++ b/quantus_sdk/lib/quantus_sdk.dart @@ -15,6 +15,7 @@ export 'src/extensions/keypair_extensions.dart'; export 'src/extensions/string_extensions.dart'; // UI-related exports export 'src/models/account.dart'; +export 'src/models/high_security_data.dart'; export 'src/models/account_stats.dart'; export 'src/models/account_associations.dart'; export 'src/models/event_type.dart'; @@ -43,6 +44,7 @@ export 'src/services/chain_history_service.dart'; export 'src/services/connectivity_service.dart'; export 'src/services/datetime_formatting_service.dart'; export 'src/services/hd_wallet_service.dart'; +export 'src/services/high_security_service.dart'; export 'src/services/human_readable_checksum_service.dart'; export 'src/services/migration_service.dart'; export 'src/services/network/redundant_endpoint.dart'; diff --git a/quantus_sdk/lib/src/constants/app_constants.dart b/quantus_sdk/lib/src/constants/app_constants.dart index 3119b43a..a6014055 100644 --- a/quantus_sdk/lib/src/constants/app_constants.dart +++ b/quantus_sdk/lib/src/constants/app_constants.dart @@ -57,4 +57,7 @@ class AppConstants { // This starts the hardware wallet flow using a soft wallet - quite useful for debugging // hardware wallet flow without using a hardware wallet. static const debugHardwareWallet = false; + + static const String accountSettingsRouteName = 'account-settings'; + static const int highSecurityStepsCount = 3; } diff --git a/quantus_sdk/lib/src/models/high_security_data.dart b/quantus_sdk/lib/src/models/high_security_data.dart new file mode 100644 index 00000000..16020882 --- /dev/null +++ b/quantus_sdk/lib/src/models/high_security_data.dart @@ -0,0 +1,16 @@ +class HighSecurityData { + final String guardianAddress; + final int safeguardWindow; + + const HighSecurityData({ + this.guardianAddress = '', + this.safeguardWindow = 10 * 60 * 60, // 10 hours in seconds + }); + + HighSecurityData copyWith({String? guardianAddress, int? safeguardWindow}) { + return HighSecurityData( + guardianAddress: guardianAddress ?? this.guardianAddress, + safeguardWindow: safeguardWindow ?? this.safeguardWindow, + ); + } +} diff --git a/quantus_sdk/lib/src/services/datetime_formatting_service.dart b/quantus_sdk/lib/src/services/datetime_formatting_service.dart index 82b61687..24c44168 100644 --- a/quantus_sdk/lib/src/services/datetime_formatting_service.dart +++ b/quantus_sdk/lib/src/services/datetime_formatting_service.dart @@ -125,4 +125,17 @@ class DatetimeFormattingService { static String _pluralize(int count) { return count == 1 ? '' : 's'; } + + static String formatSafeguardTime(int months, int days, int hours) { + if (months > 0) { + return '$months month${months > 1 ? 's' : ''}, ' + '$days day${days != 1 ? 's' : ''}, \n' + '$hours hr${hours != 1 ? 's' : ''}'; + } else if (days > 0) { + return '$days day${days != 1 ? 's' : ''}, ' + '$hours hr${hours != 1 ? 's' : ''}'; + } else { + return '$hours hr${hours != 1 ? 's' : ''}'; + } + } } diff --git a/quantus_sdk/lib/src/services/high_security_service.dart b/quantus_sdk/lib/src/services/high_security_service.dart new file mode 100644 index 00000000..c8caa1ea --- /dev/null +++ b/quantus_sdk/lib/src/services/high_security_service.dart @@ -0,0 +1,46 @@ +import 'dart:async'; + +import 'package:quantus_sdk/quantus_sdk.dart'; + +class HighSecurityService { + static final HighSecurityService _instance = HighSecurityService._internal(); + factory HighSecurityService() => _instance; + HighSecurityService._internal(); + + // ignore: unused_field + final SubstrateService _substrateService = SubstrateService(); + + Future setupHighSecurityAccount(Account account, HighSecurityData formData) async { + try { + await Future.delayed(const Duration(seconds: 2)); + // Submit the extrinsic and return its result + // return await _substrateService.submitExtrinsic(account, runtimeCall); + } catch (e, stackTrace) { + print('Failed to setup: $e'); + print('Failed to setup: $stackTrace'); + throw Exception('Failed to setup: $e'); + } + } + + // TODO replace with actual fee calculation + Future getHighSecuritySetupFee(Account account, HighSecurityData formData) async { + try { + await Future.delayed(const Duration(seconds: 2)); + + // Mock fetch + return ExtrinsicFeeData( + fee: BigInt.from(1000000000000000000), // 1.0 + blockHash: '0x0', + blockNumber: 0, + ); + } catch (e, stackTrace) { + print('Failed to get setup fee: $e'); + print('Failed to get setup fee: $stackTrace'); + throw Exception('Failed to get setup fee: $e'); + } + } + + void getHighSecuritySetupCall(HighSecurityData formData) { + throw Exception('No Implementation'); + } +}