From c5509175edb6a94122cce6fe6f63a43d44904dc9 Mon Sep 17 00:00:00 2001 From: Bassam El Obeid <127500305+bassam-deriv@users.noreply.github.com> Date: Mon, 20 May 2024 11:46:41 +0400 Subject: [PATCH] feat(deriv_passkeys): [UPM-547] Deriv passkeys package (#425) --- .gitignore | 2 + CHANGELOG.md | 28 + .../lib/l10n/deriv_auth/app_en.arb | 2 +- packages/deriv_passkeys/.gitignore | 30 + packages/deriv_passkeys/.metadata | 33 + packages/deriv_passkeys/CHANGELOG.md | 3 + packages/deriv_passkeys/LICENSE | 1 + packages/deriv_passkeys/README.md | 96 +++ packages/deriv_passkeys/analysis_options.yaml | 120 +++ packages/deriv_passkeys/android/.gitignore | 9 + packages/deriv_passkeys/android/build.gradle | 76 ++ .../deriv_passkeys/android/gradle.properties | 15 + .../deriv_passkeys/android/settings.gradle | 1 + .../android/src/main/AndroidManifest.xml | 7 + .../deriv_passkeys/DerivPasskeysPlugin.kt | 153 ++++ .../android/src/main/proguard-rules.pro | 4 + .../assets/svg/add_passkey_icon.svg | 28 + .../svg/effortless_login_passkey_icon.svg | 22 + .../deriv_passkeys/assets/svg/face_id.svg | 15 + .../assets/svg/fingerprint_icon.svg | 3 + .../assets/svg/learn_more_passkeys_icon.svg | 3 + .../assets/svg/light_bulb_icon.svg | 10 + .../deriv_passkeys/assets/svg/lock_icon.svg | 10 + .../svg/passkey_created_success_icon.svg | 28 + .../assets/svg/passkey_icon.svg | 3 + .../deriv_passkeys/assets/svg/sync_icon.svg | 6 + packages/deriv_passkeys/example/.gitignore | 44 ++ packages/deriv_passkeys/example/.metadata | 45 ++ packages/deriv_passkeys/example/README.md | 8 + .../example/analysis_options.yaml | 29 + .../deriv_passkeys/example/android/.gitignore | 13 + .../example/android/app/build.gradle | 72 ++ .../android/app/src/debug/AndroidManifest.xml | 7 + .../android/app/src/main/AndroidManifest.xml | 34 + .../com/example/passkeys_poc/MainActivity.kt | 5 + .../res/drawable-v21/launch_background.xml | 12 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values-night/styles.xml | 18 + .../app/src/main/res/values/styles.xml | 18 + .../app/src/profile/AndroidManifest.xml | 7 + .../example/android/build.gradle | 31 + .../example/android/gradle.properties | 3 + .../example/android/settings.gradle | 11 + .../example/devtools_options.yaml | 1 + .../deriv_passkeys/example/ios/.gitignore | 34 + .../ios/Flutter/AppFrameworkInfo.plist | 26 + .../example/ios/Flutter/Debug.xcconfig | 2 + .../example/ios/Flutter/Release.xcconfig | 2 + packages/deriv_passkeys/example/ios/Podfile | 43 + .../deriv_passkeys/example/ios/Podfile.lock | 85 ++ .../ios/Runner.xcodeproj/project.pbxproj | 747 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 98 +++ .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../example/ios/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 122 +++ .../Icon-App-1024x1024@1x.png | Bin 0 -> 10932 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 295 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 450 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 282 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 462 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 704 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 586 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 1674 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 762 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 1226 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 1418 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + .../Runner/Base.lproj/LaunchScreen.storyboard | 37 + .../ios/Runner/Base.lproj/Main.storyboard | 26 + .../example/ios/Runner/Info.plist | 49 ++ .../ios/Runner/Runner-Bridging-Header.h | 1 + .../example/ios/Runner/Runner.entitlements | 15 + .../example/ios/RunnerTests/RunnerTests.swift | 12 + packages/deriv_passkeys/example/lib/main.dart | 111 +++ packages/deriv_passkeys/example/pubspec.yaml | 77 ++ .../example/test/widget_test.dart | 30 + packages/deriv_passkeys/ios/.gitignore | 38 + .../ios/Classes/DerivPasskeysManager.swift | 388 +++++++++ .../ios/Classes/DerivPasskeysPlugin.swift | 72 ++ .../PublicKeyCredentialRequestOptions.swift | 215 +++++ .../deriv_passkeys/ios/deriv_passkeys.podspec | 23 + .../deriv_passkeys/lib/deriv_passkeys.dart | 6 + .../deriv_passkeys/lib/src/data/data.dart | 5 + .../base_deriv_passkeys_data_source.dart | 46 ++ .../deriv_passkeys_data_source.dart | 164 ++++ .../data/mappers/deriv_passkeys_mapper.dart | 63 ++ .../src/data/models/deriv_passkey_model.dart | 58 ++ .../models/deriv_passkeys_options_model.dart | 51 ++ ...deriv_passkeys_register_options_model.dart | 18 + ...verify_credentials_request_body_model.dart | 48 ++ ...eys_verify_credentials_response_model.dart | 15 + .../passkeys_connection_info_model.dart | 24 + .../deriv_passkeys_method_channel.dart | 71 ++ .../deriv_passkeys_repository.dart | 71 ++ .../base_deriv_passkeys_repository.dart | 35 + .../deriv_passkeys/lib/src/domain/domain.dart | 5 + .../domain/entities/deriv_passkey_entity.dart | 53 ++ .../deriv_passkeys_options_entity.dart | 60 ++ ..._passkeys_register_credentials_entity.dart | 25 + ...eriv_passkeys_register_options_entity.dart | 23 + ...erify_credentials_request_body_entity.dart | 40 + ...ys_verify_credentials_response_entity.dart | 15 + .../passkeys_connection_info_entity.dart | 14 + .../base_deriv_passkeys_method_channel.dart | 35 + .../src/exceptions/platform_exceptions.dart | 29 + .../lib/src/exceptions/server_exceptions.dart | 14 + .../src/extensions/context_extensions.dart | 9 + .../lib/src/interactor/interactor.dart | 3 + .../services/deriv_passkeys_service.dart | 111 +++ .../src/presentation/constants/assets.dart | 32 + .../pages/effortless_passkeys_login_page.dart | 213 +++++ .../pages/learn_more_passkeys_page.dart | 253 ++++++ .../pages/manage_passkeys_page.dart | 195 +++++ .../pages/passkey_created_page.dart | 78 ++ .../lib/src/presentation/presentation.dart | 6 + .../states/bloc/deriv_passkeys_bloc.dart | 151 ++++ .../states/bloc/deriv_passkeys_event.dart | 70 ++ .../states/bloc/deriv_passkeys_state.dart | 67 ++ .../presentation/utils/date_time_utils.dart | 10 + .../utils/handle_errors_utils.dart | 58 ++ .../presentation/utils/platform_utils.dart | 15 + .../widgets/continue_with_passkey_button.dart | 80 ++ .../widgets/icon_text_row_widget.dart | 29 + .../passkey_created_call_to_action.dart | 52 ++ .../presentation/widgets/passkey_widget.dart | 110 +++ .../widgets/section_title_and_content.dart | 42 + .../widgets/unordered_list_widget.dart | 88 +++ packages/deriv_passkeys/pubspec.yaml | 111 +++ ...deriv_passkeys_data_source_mock_setup.dart | 123 +++ .../data/deriv_passkeys_data_source_test.dart | 211 +++++ .../test/data/deriv_passkeys_mapper_test.dart | 132 ++++ .../deriv_passkeys_method_channel_test.dart | 147 ++++ .../data/deriv_passkeys_repository_test.dart | 189 +++++ .../deriv_passkeys_service_test.dart | 149 ++++ .../pages/learn_more_passkeys_page_test.dart | 104 +++ .../bloc/deriv_passkeys_bloc_setup.dart | 38 + .../states/bloc/deriv_passkeys_bloc_test.dart | 271 +++++++ .../bloc/deriv_passkeys_event_test.dart | 64 ++ .../bloc/deriv_passkeys_state_test.dart | 67 ++ .../utils/date_time_utils_test.dart | 22 + .../continue_with_passkey_button_test.dart | 158 ++++ .../widgets/icon_text_row_widget_test.dart | 37 + .../widgets/passkey_widget_test.dart | 37 + .../section_title_and_content_test.dart | 30 + .../widgets/unordered_list_widget_test.dart | 53 ++ 163 files changed, 8023 insertions(+), 1 deletion(-) create mode 100644 packages/deriv_passkeys/.gitignore create mode 100644 packages/deriv_passkeys/.metadata create mode 100644 packages/deriv_passkeys/CHANGELOG.md create mode 100644 packages/deriv_passkeys/LICENSE create mode 100644 packages/deriv_passkeys/README.md create mode 100644 packages/deriv_passkeys/analysis_options.yaml create mode 100644 packages/deriv_passkeys/android/.gitignore create mode 100644 packages/deriv_passkeys/android/build.gradle create mode 100644 packages/deriv_passkeys/android/gradle.properties create mode 100644 packages/deriv_passkeys/android/settings.gradle create mode 100644 packages/deriv_passkeys/android/src/main/AndroidManifest.xml create mode 100644 packages/deriv_passkeys/android/src/main/kotlin/com/deriv/passkeys/deriv_passkeys/DerivPasskeysPlugin.kt create mode 100644 packages/deriv_passkeys/android/src/main/proguard-rules.pro create mode 100644 packages/deriv_passkeys/assets/svg/add_passkey_icon.svg create mode 100644 packages/deriv_passkeys/assets/svg/effortless_login_passkey_icon.svg create mode 100644 packages/deriv_passkeys/assets/svg/face_id.svg create mode 100644 packages/deriv_passkeys/assets/svg/fingerprint_icon.svg create mode 100644 packages/deriv_passkeys/assets/svg/learn_more_passkeys_icon.svg create mode 100644 packages/deriv_passkeys/assets/svg/light_bulb_icon.svg create mode 100644 packages/deriv_passkeys/assets/svg/lock_icon.svg create mode 100644 packages/deriv_passkeys/assets/svg/passkey_created_success_icon.svg create mode 100644 packages/deriv_passkeys/assets/svg/passkey_icon.svg create mode 100644 packages/deriv_passkeys/assets/svg/sync_icon.svg create mode 100644 packages/deriv_passkeys/example/.gitignore create mode 100644 packages/deriv_passkeys/example/.metadata create mode 100644 packages/deriv_passkeys/example/README.md create mode 100644 packages/deriv_passkeys/example/analysis_options.yaml create mode 100644 packages/deriv_passkeys/example/android/.gitignore create mode 100644 packages/deriv_passkeys/example/android/app/build.gradle create mode 100644 packages/deriv_passkeys/example/android/app/src/debug/AndroidManifest.xml create mode 100644 packages/deriv_passkeys/example/android/app/src/main/AndroidManifest.xml create mode 100644 packages/deriv_passkeys/example/android/app/src/main/kotlin/com/example/passkeys_poc/MainActivity.kt create mode 100644 packages/deriv_passkeys/example/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 packages/deriv_passkeys/example/android/app/src/main/res/drawable/launch_background.xml create mode 100644 packages/deriv_passkeys/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 packages/deriv_passkeys/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 packages/deriv_passkeys/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 packages/deriv_passkeys/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 packages/deriv_passkeys/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 packages/deriv_passkeys/example/android/app/src/main/res/values-night/styles.xml create mode 100644 packages/deriv_passkeys/example/android/app/src/main/res/values/styles.xml create mode 100644 packages/deriv_passkeys/example/android/app/src/profile/AndroidManifest.xml create mode 100644 packages/deriv_passkeys/example/android/build.gradle create mode 100644 packages/deriv_passkeys/example/android/gradle.properties create mode 100644 packages/deriv_passkeys/example/android/settings.gradle create mode 100644 packages/deriv_passkeys/example/devtools_options.yaml create mode 100644 packages/deriv_passkeys/example/ios/.gitignore create mode 100644 packages/deriv_passkeys/example/ios/Flutter/AppFrameworkInfo.plist create mode 100644 packages/deriv_passkeys/example/ios/Flutter/Debug.xcconfig create mode 100644 packages/deriv_passkeys/example/ios/Flutter/Release.xcconfig create mode 100644 packages/deriv_passkeys/example/ios/Podfile create mode 100644 packages/deriv_passkeys/example/ios/Podfile.lock create mode 100644 packages/deriv_passkeys/example/ios/Runner.xcodeproj/project.pbxproj create mode 100644 packages/deriv_passkeys/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 packages/deriv_passkeys/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/deriv_passkeys/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 packages/deriv_passkeys/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 packages/deriv_passkeys/example/ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 packages/deriv_passkeys/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/deriv_passkeys/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 packages/deriv_passkeys/example/ios/Runner/AppDelegate.swift create mode 100644 packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100644 packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100644 packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100644 packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100644 packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 packages/deriv_passkeys/example/ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 packages/deriv_passkeys/example/ios/Runner/Base.lproj/Main.storyboard create mode 100644 packages/deriv_passkeys/example/ios/Runner/Info.plist create mode 100644 packages/deriv_passkeys/example/ios/Runner/Runner-Bridging-Header.h create mode 100644 packages/deriv_passkeys/example/ios/Runner/Runner.entitlements create mode 100644 packages/deriv_passkeys/example/ios/RunnerTests/RunnerTests.swift create mode 100644 packages/deriv_passkeys/example/lib/main.dart create mode 100644 packages/deriv_passkeys/example/pubspec.yaml create mode 100644 packages/deriv_passkeys/example/test/widget_test.dart create mode 100644 packages/deriv_passkeys/ios/.gitignore create mode 100644 packages/deriv_passkeys/ios/Classes/DerivPasskeysManager.swift create mode 100644 packages/deriv_passkeys/ios/Classes/DerivPasskeysPlugin.swift create mode 100644 packages/deriv_passkeys/ios/Classes/PublicKeyCredentialRequestOptions.swift create mode 100644 packages/deriv_passkeys/ios/deriv_passkeys.podspec create mode 100644 packages/deriv_passkeys/lib/deriv_passkeys.dart create mode 100644 packages/deriv_passkeys/lib/src/data/data.dart create mode 100644 packages/deriv_passkeys/lib/src/data/data_sources/base_deriv_passkeys_data_source.dart create mode 100644 packages/deriv_passkeys/lib/src/data/data_sources/deriv_passkeys_data_source.dart create mode 100644 packages/deriv_passkeys/lib/src/data/mappers/deriv_passkeys_mapper.dart create mode 100644 packages/deriv_passkeys/lib/src/data/models/deriv_passkey_model.dart create mode 100644 packages/deriv_passkeys/lib/src/data/models/deriv_passkeys_options_model.dart create mode 100644 packages/deriv_passkeys/lib/src/data/models/deriv_passkeys_register_options_model.dart create mode 100644 packages/deriv_passkeys/lib/src/data/models/deriv_passkeys_verify_credentials_request_body_model.dart create mode 100644 packages/deriv_passkeys/lib/src/data/models/deriv_passkeys_verify_credentials_response_model.dart create mode 100644 packages/deriv_passkeys/lib/src/data/models/passkeys_connection_info_model.dart create mode 100644 packages/deriv_passkeys/lib/src/data/platform/deriv_passkeys_method_channel.dart create mode 100644 packages/deriv_passkeys/lib/src/data/repositories/deriv_passkeys_repository.dart create mode 100644 packages/deriv_passkeys/lib/src/domain/base_repositories/base_deriv_passkeys_repository.dart create mode 100644 packages/deriv_passkeys/lib/src/domain/domain.dart create mode 100644 packages/deriv_passkeys/lib/src/domain/entities/deriv_passkey_entity.dart create mode 100644 packages/deriv_passkeys/lib/src/domain/entities/deriv_passkeys_options_entity.dart create mode 100644 packages/deriv_passkeys/lib/src/domain/entities/deriv_passkeys_register_credentials_entity.dart create mode 100644 packages/deriv_passkeys/lib/src/domain/entities/deriv_passkeys_register_options_entity.dart create mode 100644 packages/deriv_passkeys/lib/src/domain/entities/deriv_passkeys_verify_credentials_request_body_entity.dart create mode 100644 packages/deriv_passkeys/lib/src/domain/entities/deriv_passkeys_verify_credentials_response_entity.dart create mode 100644 packages/deriv_passkeys/lib/src/domain/entities/passkeys_connection_info_entity.dart create mode 100644 packages/deriv_passkeys/lib/src/domain/platform/base_deriv_passkeys_method_channel.dart create mode 100644 packages/deriv_passkeys/lib/src/exceptions/platform_exceptions.dart create mode 100644 packages/deriv_passkeys/lib/src/exceptions/server_exceptions.dart create mode 100644 packages/deriv_passkeys/lib/src/extensions/context_extensions.dart create mode 100644 packages/deriv_passkeys/lib/src/interactor/interactor.dart create mode 100644 packages/deriv_passkeys/lib/src/interactor/services/deriv_passkeys_service.dart create mode 100644 packages/deriv_passkeys/lib/src/presentation/constants/assets.dart create mode 100644 packages/deriv_passkeys/lib/src/presentation/pages/effortless_passkeys_login_page.dart create mode 100644 packages/deriv_passkeys/lib/src/presentation/pages/learn_more_passkeys_page.dart create mode 100644 packages/deriv_passkeys/lib/src/presentation/pages/manage_passkeys_page.dart create mode 100644 packages/deriv_passkeys/lib/src/presentation/pages/passkey_created_page.dart create mode 100644 packages/deriv_passkeys/lib/src/presentation/presentation.dart create mode 100644 packages/deriv_passkeys/lib/src/presentation/states/bloc/deriv_passkeys_bloc.dart create mode 100644 packages/deriv_passkeys/lib/src/presentation/states/bloc/deriv_passkeys_event.dart create mode 100644 packages/deriv_passkeys/lib/src/presentation/states/bloc/deriv_passkeys_state.dart create mode 100644 packages/deriv_passkeys/lib/src/presentation/utils/date_time_utils.dart create mode 100644 packages/deriv_passkeys/lib/src/presentation/utils/handle_errors_utils.dart create mode 100644 packages/deriv_passkeys/lib/src/presentation/utils/platform_utils.dart create mode 100644 packages/deriv_passkeys/lib/src/presentation/widgets/continue_with_passkey_button.dart create mode 100644 packages/deriv_passkeys/lib/src/presentation/widgets/icon_text_row_widget.dart create mode 100644 packages/deriv_passkeys/lib/src/presentation/widgets/passkey_created_call_to_action.dart create mode 100644 packages/deriv_passkeys/lib/src/presentation/widgets/passkey_widget.dart create mode 100644 packages/deriv_passkeys/lib/src/presentation/widgets/section_title_and_content.dart create mode 100644 packages/deriv_passkeys/lib/src/presentation/widgets/unordered_list_widget.dart create mode 100644 packages/deriv_passkeys/pubspec.yaml create mode 100644 packages/deriv_passkeys/test/data/deriv_passkeys_data_source_mock_setup.dart create mode 100644 packages/deriv_passkeys/test/data/deriv_passkeys_data_source_test.dart create mode 100644 packages/deriv_passkeys/test/data/deriv_passkeys_mapper_test.dart create mode 100644 packages/deriv_passkeys/test/data/deriv_passkeys_method_channel_test.dart create mode 100644 packages/deriv_passkeys/test/data/deriv_passkeys_repository_test.dart create mode 100644 packages/deriv_passkeys/test/interactor/deriv_passkeys_service_test.dart create mode 100644 packages/deriv_passkeys/test/presentation/pages/learn_more_passkeys_page_test.dart create mode 100644 packages/deriv_passkeys/test/presentation/states/bloc/deriv_passkeys_bloc_setup.dart create mode 100644 packages/deriv_passkeys/test/presentation/states/bloc/deriv_passkeys_bloc_test.dart create mode 100644 packages/deriv_passkeys/test/presentation/states/bloc/deriv_passkeys_event_test.dart create mode 100644 packages/deriv_passkeys/test/presentation/states/bloc/deriv_passkeys_state_test.dart create mode 100644 packages/deriv_passkeys/test/presentation/utils/date_time_utils_test.dart create mode 100644 packages/deriv_passkeys/test/presentation/widgets/continue_with_passkey_button_test.dart create mode 100644 packages/deriv_passkeys/test/presentation/widgets/icon_text_row_widget_test.dart create mode 100644 packages/deriv_passkeys/test/presentation/widgets/passkey_widget_test.dart create mode 100644 packages/deriv_passkeys/test/presentation/widgets/section_title_and_content_test.dart create mode 100644 packages/deriv_passkeys/test/presentation/widgets/unordered_list_widget_test.dart diff --git a/.gitignore b/.gitignore index 986f8f684..86f284979 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,8 @@ lib/basic_api/generated/*.json *.iws .idea/ +.fvm + # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. diff --git a/CHANGELOG.md b/CHANGELOG.md index 8493420f7..5c667901c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -632,6 +632,34 @@ Packages with dependency updates only: - **REFACTOR**(deriv_ui): update deriv_ui dependencies ([#499](https://github.com/regentmarkets/flutter-deriv-packages/issues/499)). ([2ade47f5](https://github.com/regentmarkets/flutter-deriv-packages/commit/2ade47f5f42a37325e4e4906c8c095fc26d777b6)) +## 2024-02-29 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`deriv_ui` - `v0.0.6+4`](#deriv_ui---v0064) + - [`deriv_auth` - `v6.0.2`](#deriv_auth---v602) + +Packages with dependency updates only: + +> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project. + + - `deriv_auth` - `v6.0.2` + +--- + +#### `deriv_ui` - `v0.0.6+4` + + - **REFACTOR**(deriv_ui): update deriv_ui dependencies ([#499](https://github.com/regentmarkets/flutter-deriv-packages/issues/499)). ([2ade47f5](https://github.com/regentmarkets/flutter-deriv-packages/commit/2ade47f5f42a37325e4e4906c8c095fc26d777b6)) + + ## 2024-02-29 ### Changes diff --git a/packages/deriv_localizations/lib/l10n/deriv_auth/app_en.arb b/packages/deriv_localizations/lib/l10n/deriv_auth/app_en.arb index e3602bb49..8ac9be2bf 100644 --- a/packages/deriv_localizations/lib/l10n/deriv_auth/app_en.arb +++ b/packages/deriv_localizations/lib/l10n/deriv_auth/app_en.arb @@ -13,7 +13,7 @@ "actionOk": "OK", "warnNotAvailableCountries": "If you have any questions, contact us via ", "labelLiveChat": "Live chat", - "actionGetAFreeAccount": "Get a free account", + "actionSignUpForFree": "Sign up for free", "actionLogin": "Log in", "labelTwoFactorAuth": "Two-factor authentication", "informEnterTwoFactorAuthCode": "Enter the 6-digit code from the authenticator app on your phone.", diff --git a/packages/deriv_passkeys/.gitignore b/packages/deriv_passkeys/.gitignore new file mode 100644 index 000000000..96486fd93 --- /dev/null +++ b/packages/deriv_passkeys/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/packages/deriv_passkeys/.metadata b/packages/deriv_passkeys/.metadata new file mode 100644 index 000000000..7e5bb4c81 --- /dev/null +++ b/packages/deriv_passkeys/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "ead455963c12b453cdb2358cad34969c76daf180" + channel: "stable" + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: ead455963c12b453cdb2358cad34969c76daf180 + base_revision: ead455963c12b453cdb2358cad34969c76daf180 + - platform: android + create_revision: ead455963c12b453cdb2358cad34969c76daf180 + base_revision: ead455963c12b453cdb2358cad34969c76daf180 + - platform: ios + create_revision: ead455963c12b453cdb2358cad34969c76daf180 + base_revision: ead455963c12b453cdb2358cad34969c76daf180 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/deriv_passkeys/CHANGELOG.md b/packages/deriv_passkeys/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/packages/deriv_passkeys/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/deriv_passkeys/LICENSE b/packages/deriv_passkeys/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/packages/deriv_passkeys/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/packages/deriv_passkeys/README.md b/packages/deriv_passkeys/README.md new file mode 100644 index 000000000..ef9c026b7 --- /dev/null +++ b/packages/deriv_passkeys/README.md @@ -0,0 +1,96 @@ +# Deriv Passkeys + +Deriv Passkeys is a Flutter plugin that allows you to generate passkeys for Deriv applications (based on WebAuthn). + +## Features + +This package contains everything you need to set up passkeys in your Deriv Flutter App: + +* Android and iOS support +* Login with passkeys +* Create passkeys +* List passkeys + +## Dependencies to Other Deriv Packages + +* [flutter_deriv_api](https://github.com/deriv-com/flutter-deriv-api) +* [deriv_theme](https://github.com/regentmarkets/flutter-deriv-packages/tree/master/packages/deriv_theme) +* [deriv_ui](https://github.com/regentmarkets/flutter-deriv-packages/tree/master/packages/deriv_ui) + +## Getting started + +To use this package, add `deriv_passkeys` as a dependency in your pubspec.yaml file: + +```yaml +dependencies: + deriv_passkeys: + git: + url: git@github.com:regentmarkets/flutter-deriv-packages.git + path: packages/deriv_passkeys + ref: deriv_passkeys-v0.0.1 +``` + +### Android + +No additional setup required. + +### iOS + +- Add the associated domains to your signing capabilities in Xcode: +- Add the following values to your associated domains: + - webcredentials:deriv.com + - applinks:deriv.com + + +## Usage + +### Import the package + +```dart +import 'package:deriv_passkeys/deriv_passkeys.dart'; +``` + +### DerivPasskeysBloc: + +```dart + final DerivPasskeysBloc derivPasskeysBloc = DerivPasskeysBloc( + getJwtToken: derivJwtService.getJwtToken, + derivPasskeysService: DerivPasskeysService( + DerivPasskeysRepository( + DerivPasskeysDataSource( + DerivPasskeysMapper(), + ), + ), + ), + connectionInfo: PasskeysConnectionInfoEntity( + appId: FlavorConfig.instance.flavorValues.appId!, + endpoint: FlavorConfig.instance.flavorValues.endPoint!, + ), + ); +``` + +### ContinueWithPasskeyButton + +```dart + ContinueWithPasskeyButton( + derivPasskeysBloc: context.read(), + ), +``` + +- This widget is used to continue with passkey. +- It requires `derivPasskeysBloc` as a parameter. +- It will return not show anything if the user's device does not support passkeys. +- When it's clicked it will either be successful or show an error message. +- in case of success the DerivPasskeysBloc will emit DerivPasskeysCredentialVerifiedState. +- The DerivPasskeysCredentialVerifiedState will contain the token which can be used to authenticate the user. + ```dart + DerivPasskeysCredentialVerifiedState( + token: token, + ), + ``` + + + + + + diff --git a/packages/deriv_passkeys/analysis_options.yaml b/packages/deriv_passkeys/analysis_options.yaml new file mode 100644 index 000000000..fb40ccd5d --- /dev/null +++ b/packages/deriv_passkeys/analysis_options.yaml @@ -0,0 +1,120 @@ +analyzer: + language: + strict-raw-types: true + +linter: + rules: + - always_declare_return_types + - always_put_control_body_on_new_line + - always_put_required_named_parameters_first + - always_require_non_null_named_parameters + - always_specify_types + - annotate_overrides + - avoid_bool_literals_in_conditional_expressions + - avoid_catches_without_on_clauses + - avoid_empty_else + - avoid_equals_and_hash_code_on_mutable_classes + - avoid_field_initializers_in_const_classes + - avoid_function_literals_in_foreach_calls + - avoid_init_to_null + - avoid_null_checks_in_equality_operators + - avoid_positional_boolean_parameters + - avoid_print + - avoid_redundant_argument_values + - avoid_relative_lib_imports + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + - avoid_returning_null + - avoid_returning_null_for_future + - avoid_returning_null_for_void + - avoid_setters_without_getters + - avoid_shadowing_type_parameters + - avoid_single_cascade_in_expression_statements + - avoid_slow_async_io + - avoid_types_as_parameter_names + - avoid_unnecessary_containers + - avoid_unused_constructor_parameters + - avoid_void_async + - await_only_futures + - camel_case_types + - cancel_subscriptions + - cascade_invocations + - close_sinks + - constant_identifier_names + - control_flow_in_finally + - curly_braces_in_flow_control_structures + - empty_catches + - empty_constructor_bodies + - empty_statements + - file_names + - flutter_style_todos + - hash_and_equals + - implementation_imports + - join_return_with_assignment + - library_names + - library_prefixes + - no_adjacent_strings_in_list + - no_duplicate_case_values + - non_constant_identifier_names + - null_closures + - only_throw_errors + - overridden_fields + - package_api_docs + - package_names + - package_prefixed_library_names + - parameter_assignments + - prefer_adjacent_string_concatenation + - prefer_asserts_in_initializer_lists + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_const_constructors + - prefer_const_constructors_in_immutables + - prefer_const_declarations + - prefer_const_literals_to_create_immutables + - prefer_constructors_over_static_methods + - prefer_contains + - prefer_expression_function_bodies + - prefer_final_fields + - prefer_final_in_for_each + - prefer_final_locals + - prefer_foreach + - prefer_function_declarations_over_variables + - prefer_generic_function_type_aliases + - prefer_initializing_formals + - prefer_int_literals + - prefer_interpolation_to_compose_strings + - prefer_is_empty + - prefer_is_not_empty + - prefer_iterable_whereType + - prefer_mixin + - prefer_single_quotes + - prefer_typing_uninitialized_variables + - prefer_void_to_null + - public_member_api_docs + - recursive_getters + - slash_for_doc_comments + - sort_constructors_first + - sort_unnamed_constructors_first + - test_types_in_equals + - throw_in_finally + - type_init_formals + - unawaited_futures + - unnecessary_await_in_return + - unnecessary_brace_in_string_interps + - unnecessary_const + - unnecessary_getters_setters + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_null_in_if_null_operators + - unnecessary_overrides + - unnecessary_parenthesis + - unnecessary_statements + - unnecessary_this + - unrelated_type_equality_checks + - use_function_type_syntax_for_parameters + - use_rethrow_when_possible + - use_setters_to_change_properties + - use_string_buffers + - use_to_and_as_if_applicable + - valid_regexps + - void_checks diff --git a/packages/deriv_passkeys/android/.gitignore b/packages/deriv_passkeys/android/.gitignore new file mode 100644 index 000000000..161bdcdaf --- /dev/null +++ b/packages/deriv_passkeys/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/packages/deriv_passkeys/android/build.gradle b/packages/deriv_passkeys/android/build.gradle new file mode 100644 index 000000000..935fccbf9 --- /dev/null +++ b/packages/deriv_passkeys/android/build.gradle @@ -0,0 +1,76 @@ +group 'com.deriv.passkeys.deriv_passkeys' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.8.22' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:8.0.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + if (project.android.hasProperty("namespace")) { + namespace 'com.deriv.passkeys.deriv_passkeys' + } + + compileSdk 34 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + test.java.srcDirs += 'src/test/kotlin' + } + + defaultConfig { + minSdkVersion 21 + } + + dependencies { + testImplementation 'org.jetbrains.kotlin:kotlin-test' + testImplementation 'org.mockito:mockito-core:5.0.0' + + implementation "androidx.credentials:credentials:1.2.2" + implementation "androidx.credentials:credentials-play-services-auth:1.2.2" + + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1" + + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1" + + } + + testOptions { + unitTests.all { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} diff --git a/packages/deriv_passkeys/android/gradle.properties b/packages/deriv_passkeys/android/gradle.properties new file mode 100644 index 000000000..fc53bee90 --- /dev/null +++ b/packages/deriv_passkeys/android/gradle.properties @@ -0,0 +1,15 @@ +## For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx1024m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +# +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +#Tue Dec 26 15:23:43 GST 2023 +android.enableJetifier=true +android.useAndroidX=true diff --git a/packages/deriv_passkeys/android/settings.gradle b/packages/deriv_passkeys/android/settings.gradle new file mode 100644 index 000000000..dcd8a9248 --- /dev/null +++ b/packages/deriv_passkeys/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'deriv_passkeys' diff --git a/packages/deriv_passkeys/android/src/main/AndroidManifest.xml b/packages/deriv_passkeys/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..de887b8df --- /dev/null +++ b/packages/deriv_passkeys/android/src/main/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/packages/deriv_passkeys/android/src/main/kotlin/com/deriv/passkeys/deriv_passkeys/DerivPasskeysPlugin.kt b/packages/deriv_passkeys/android/src/main/kotlin/com/deriv/passkeys/deriv_passkeys/DerivPasskeysPlugin.kt new file mode 100644 index 000000000..0493071d1 --- /dev/null +++ b/packages/deriv_passkeys/android/src/main/kotlin/com/deriv/passkeys/deriv_passkeys/DerivPasskeysPlugin.kt @@ -0,0 +1,153 @@ +package com.deriv.passkeys.deriv_passkeys + +import android.app.Activity +import androidx.credentials.CreatePublicKeyCredentialRequest +import androidx.credentials.CreatePublicKeyCredentialResponse +import androidx.credentials.CredentialManager +import androidx.credentials.GetCredentialRequest +import androidx.credentials.GetPublicKeyCredentialOption +import androidx.credentials.PublicKeyCredential +import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialDomException +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import kotlinx.coroutines.launch +import org.json.JSONObject +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding +import io.flutter.embedding.engine.plugins.activity.ActivityAware + +/// DerivPasskeysPlugin is a Flutter plugin that provides a way to create and get credentials using the WebAuthn API. +class DerivPasskeysPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, ViewModel() { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel: MethodChannel + private lateinit var activity: Activity + + + private fun createCredential(options: String, callback: (credential: String?, e: Exception?) -> Unit) { + JSONObject(options) + val createPublicKeyCredentialRequest = CreatePublicKeyCredentialRequest( + requestJson = options, + preferImmediatelyAvailableCredentials = false + ) + viewModelScope.launch { + try { + val credentialManager = CredentialManager.create(activity) + val result = credentialManager.createCredential( + context = activity, + request = createPublicKeyCredentialRequest, + ) + val credential = result as CreatePublicKeyCredentialResponse + callback(credential.registrationResponseJson, null) + } catch (e: Exception) { + callback(null, e) + } + } + } + + private fun getCredential(options: String, callback: (credential: String?, e: Exception?) -> Unit) { + JSONObject(options) + val getPublicKeyCredentialOption = GetPublicKeyCredentialOption( + requestJson = options + ) + viewModelScope.launch { + try { + val credentialManager = CredentialManager.create(activity) + val result = credentialManager.getCredential( + context = activity, + request = GetCredentialRequest(listOf(getPublicKeyCredentialOption)), + ) + val credential = result.credential as PublicKeyCredential + callback(credential.authenticationResponseJson, null) + } catch (e: Exception) { + callback(null, e) + } + } + } + + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "deriv_passkeys") + channel.setMethodCallHandler(this) + } + + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + this.activity = binding.activity; + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } + + override fun onDetachedFromActivity() { + } + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + this.activity = binding.activity; + } + + override fun onDetachedFromActivityForConfigChanges() { + } + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + "isPlatformSupported" -> { + val isSupported = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P + result.success(isSupported) + } + "createCredential" -> { + val options = call.argument("options") as String? + options?.let { + try { + createCredential(options) { credential, e -> + if (credential != null) { + result.success(credential) + } + else if (e != null) { + var exceptionType = e.javaClass.kotlin.simpleName + if (e is CreatePublicKeyCredentialDomException) { + exceptionType = "$exceptionType(${e.domError.javaClass.kotlin.simpleName ?: "DomError"})" + } + result.error(exceptionType ?: "Exception", e.message ?: "Exception occurred", null) + } + else { + result.error("Error", "Unknown error", null) + } + } + } catch (e: Exception) { + result.error(e.javaClass.kotlin.simpleName ?: "Exception", e.message ?: "Exception occurred", null) + } + } ?: run { + result.error("InvalidParameterException", "Options not found", null) + } + } + "getCredential" -> { + val options = call.argument("options") as String? + options?.let { + try { + getCredential(options) { credential, e -> + if (credential != null) { + result.success(credential) + } + else if (e != null) { + result.error(e.javaClass.kotlin.simpleName ?: "Exception", e.message ?: "Exception occurred", null) + } + else { + result.error("Error", "Unknown error", null) + } + } + } catch (e: Exception) { + result.error(e.javaClass.kotlin.simpleName ?: "Exception", e.message ?: "Exception occurred", null) + } + } ?: run { + result.error("InvalidParameterException", "Options not found", null) + } + } + else -> result.notImplemented() + } + } +} diff --git a/packages/deriv_passkeys/android/src/main/proguard-rules.pro b/packages/deriv_passkeys/android/src/main/proguard-rules.pro new file mode 100644 index 000000000..5bf7d0357 --- /dev/null +++ b/packages/deriv_passkeys/android/src/main/proguard-rules.pro @@ -0,0 +1,4 @@ +-if class androidx.credentials.CredentialManager +-keep class androidx.credentials.playservices.** { + *; +} diff --git a/packages/deriv_passkeys/assets/svg/add_passkey_icon.svg b/packages/deriv_passkeys/assets/svg/add_passkey_icon.svg new file mode 100644 index 000000000..8e9207994 --- /dev/null +++ b/packages/deriv_passkeys/assets/svg/add_passkey_icon.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/deriv_passkeys/assets/svg/effortless_login_passkey_icon.svg b/packages/deriv_passkeys/assets/svg/effortless_login_passkey_icon.svg new file mode 100644 index 000000000..d41e62fac --- /dev/null +++ b/packages/deriv_passkeys/assets/svg/effortless_login_passkey_icon.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/deriv_passkeys/assets/svg/face_id.svg b/packages/deriv_passkeys/assets/svg/face_id.svg new file mode 100644 index 000000000..eefdd4626 --- /dev/null +++ b/packages/deriv_passkeys/assets/svg/face_id.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/packages/deriv_passkeys/assets/svg/fingerprint_icon.svg b/packages/deriv_passkeys/assets/svg/fingerprint_icon.svg new file mode 100644 index 000000000..610bf2721 --- /dev/null +++ b/packages/deriv_passkeys/assets/svg/fingerprint_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/deriv_passkeys/assets/svg/learn_more_passkeys_icon.svg b/packages/deriv_passkeys/assets/svg/learn_more_passkeys_icon.svg new file mode 100644 index 000000000..fd8279ecf --- /dev/null +++ b/packages/deriv_passkeys/assets/svg/learn_more_passkeys_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/deriv_passkeys/assets/svg/light_bulb_icon.svg b/packages/deriv_passkeys/assets/svg/light_bulb_icon.svg new file mode 100644 index 000000000..c3c24b18a --- /dev/null +++ b/packages/deriv_passkeys/assets/svg/light_bulb_icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/deriv_passkeys/assets/svg/lock_icon.svg b/packages/deriv_passkeys/assets/svg/lock_icon.svg new file mode 100644 index 000000000..c9ce9da67 --- /dev/null +++ b/packages/deriv_passkeys/assets/svg/lock_icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/deriv_passkeys/assets/svg/passkey_created_success_icon.svg b/packages/deriv_passkeys/assets/svg/passkey_created_success_icon.svg new file mode 100644 index 000000000..af74a8fbe --- /dev/null +++ b/packages/deriv_passkeys/assets/svg/passkey_created_success_icon.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/deriv_passkeys/assets/svg/passkey_icon.svg b/packages/deriv_passkeys/assets/svg/passkey_icon.svg new file mode 100644 index 000000000..16812e500 --- /dev/null +++ b/packages/deriv_passkeys/assets/svg/passkey_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/deriv_passkeys/assets/svg/sync_icon.svg b/packages/deriv_passkeys/assets/svg/sync_icon.svg new file mode 100644 index 000000000..4ba1b1be0 --- /dev/null +++ b/packages/deriv_passkeys/assets/svg/sync_icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/deriv_passkeys/example/.gitignore b/packages/deriv_passkeys/example/.gitignore new file mode 100644 index 000000000..24476c5d1 --- /dev/null +++ b/packages/deriv_passkeys/example/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/deriv_passkeys/example/.metadata b/packages/deriv_passkeys/example/.metadata new file mode 100644 index 000000000..ab3e1c09c --- /dev/null +++ b/packages/deriv_passkeys/example/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "ead455963c12b453cdb2358cad34969c76daf180" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: ead455963c12b453cdb2358cad34969c76daf180 + base_revision: ead455963c12b453cdb2358cad34969c76daf180 + - platform: android + create_revision: ead455963c12b453cdb2358cad34969c76daf180 + base_revision: ead455963c12b453cdb2358cad34969c76daf180 + - platform: ios + create_revision: ead455963c12b453cdb2358cad34969c76daf180 + base_revision: ead455963c12b453cdb2358cad34969c76daf180 + - platform: linux + create_revision: ead455963c12b453cdb2358cad34969c76daf180 + base_revision: ead455963c12b453cdb2358cad34969c76daf180 + - platform: macos + create_revision: ead455963c12b453cdb2358cad34969c76daf180 + base_revision: ead455963c12b453cdb2358cad34969c76daf180 + - platform: web + create_revision: ead455963c12b453cdb2358cad34969c76daf180 + base_revision: ead455963c12b453cdb2358cad34969c76daf180 + - platform: windows + create_revision: ead455963c12b453cdb2358cad34969c76daf180 + base_revision: ead455963c12b453cdb2358cad34969c76daf180 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/deriv_passkeys/example/README.md b/packages/deriv_passkeys/example/README.md new file mode 100644 index 000000000..f121535bb --- /dev/null +++ b/packages/deriv_passkeys/example/README.md @@ -0,0 +1,8 @@ +# passkeys_poc + +Passkeys POC is a simple Flutter app that demonstrates how to use the `deriv_passkeys` package to authenticate a user in a Deriv app using the passkeys authentication method. + + +## Getting Started + +To make the example app work, you need to fill in the `appId` and `endpoint` in the `connectionInfo` object. Open the file `lib/main.dart` and locate the following code: diff --git a/packages/deriv_passkeys/example/analysis_options.yaml b/packages/deriv_passkeys/example/analysis_options.yaml new file mode 100644 index 000000000..61b6c4de1 --- /dev/null +++ b/packages/deriv_passkeys/example/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/deriv_passkeys/example/android/.gitignore b/packages/deriv_passkeys/example/android/.gitignore new file mode 100644 index 000000000..6f568019d --- /dev/null +++ b/packages/deriv_passkeys/example/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/packages/deriv_passkeys/example/android/app/build.gradle b/packages/deriv_passkeys/example/android/app/build.gradle new file mode 100644 index 000000000..63de18e36 --- /dev/null +++ b/packages/deriv_passkeys/example/android/app/build.gradle @@ -0,0 +1,72 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + namespace "com.example.passkeys_poc" + compileSdk 34 + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.passkeys_poc" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. + minSdkVersion 21 + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/packages/deriv_passkeys/example/android/app/src/debug/AndroidManifest.xml b/packages/deriv_passkeys/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000..399f6981d --- /dev/null +++ b/packages/deriv_passkeys/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/deriv_passkeys/example/android/app/src/main/AndroidManifest.xml b/packages/deriv_passkeys/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..b6e879786 --- /dev/null +++ b/packages/deriv_passkeys/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + diff --git a/packages/deriv_passkeys/example/android/app/src/main/kotlin/com/example/passkeys_poc/MainActivity.kt b/packages/deriv_passkeys/example/android/app/src/main/kotlin/com/example/passkeys_poc/MainActivity.kt new file mode 100644 index 000000000..ede17fb8d --- /dev/null +++ b/packages/deriv_passkeys/example/android/app/src/main/kotlin/com/example/passkeys_poc/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.passkeys_poc + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() diff --git a/packages/deriv_passkeys/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/deriv_passkeys/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 000000000..f74085f3f --- /dev/null +++ b/packages/deriv_passkeys/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/deriv_passkeys/example/android/app/src/main/res/drawable/launch_background.xml b/packages/deriv_passkeys/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 000000000..304732f88 --- /dev/null +++ b/packages/deriv_passkeys/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/deriv_passkeys/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/deriv_passkeys/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/packages/deriv_passkeys/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/deriv_passkeys/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/packages/deriv_passkeys/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/deriv_passkeys/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/packages/deriv_passkeys/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/deriv_passkeys/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/packages/deriv_passkeys/example/android/app/src/main/res/values-night/styles.xml b/packages/deriv_passkeys/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 000000000..06952be74 --- /dev/null +++ b/packages/deriv_passkeys/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/deriv_passkeys/example/android/app/src/main/res/values/styles.xml b/packages/deriv_passkeys/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000..cb1ef8805 --- /dev/null +++ b/packages/deriv_passkeys/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/deriv_passkeys/example/android/app/src/profile/AndroidManifest.xml b/packages/deriv_passkeys/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 000000000..399f6981d --- /dev/null +++ b/packages/deriv_passkeys/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/deriv_passkeys/example/android/build.gradle b/packages/deriv_passkeys/example/android/build.gradle new file mode 100644 index 000000000..f7eb7f63c --- /dev/null +++ b/packages/deriv_passkeys/example/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.3.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/packages/deriv_passkeys/example/android/gradle.properties b/packages/deriv_passkeys/example/android/gradle.properties new file mode 100644 index 000000000..94adc3a3f --- /dev/null +++ b/packages/deriv_passkeys/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/packages/deriv_passkeys/example/android/settings.gradle b/packages/deriv_passkeys/example/android/settings.gradle new file mode 100644 index 000000000..44e62bcf0 --- /dev/null +++ b/packages/deriv_passkeys/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/packages/deriv_passkeys/example/devtools_options.yaml b/packages/deriv_passkeys/example/devtools_options.yaml new file mode 100644 index 000000000..7e7e7f67d --- /dev/null +++ b/packages/deriv_passkeys/example/devtools_options.yaml @@ -0,0 +1 @@ +extensions: diff --git a/packages/deriv_passkeys/example/ios/.gitignore b/packages/deriv_passkeys/example/ios/.gitignore new file mode 100644 index 000000000..7a7f9873a --- /dev/null +++ b/packages/deriv_passkeys/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/packages/deriv_passkeys/example/ios/Flutter/AppFrameworkInfo.plist b/packages/deriv_passkeys/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000..7c5696400 --- /dev/null +++ b/packages/deriv_passkeys/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/packages/deriv_passkeys/example/ios/Flutter/Debug.xcconfig b/packages/deriv_passkeys/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 000000000..ec97fc6f3 --- /dev/null +++ b/packages/deriv_passkeys/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/deriv_passkeys/example/ios/Flutter/Release.xcconfig b/packages/deriv_passkeys/example/ios/Flutter/Release.xcconfig new file mode 100644 index 000000000..c4855bfe2 --- /dev/null +++ b/packages/deriv_passkeys/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/deriv_passkeys/example/ios/Podfile b/packages/deriv_passkeys/example/ios/Podfile new file mode 100644 index 000000000..9b392a5ea --- /dev/null +++ b/packages/deriv_passkeys/example/ios/Podfile @@ -0,0 +1,43 @@ +platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/packages/deriv_passkeys/example/ios/Podfile.lock b/packages/deriv_passkeys/example/ios/Podfile.lock new file mode 100644 index 000000000..81840226b --- /dev/null +++ b/packages/deriv_passkeys/example/ios/Podfile.lock @@ -0,0 +1,85 @@ +PODS: + - connectivity_plus (0.0.1): + - Flutter + - ReachabilitySwift + - deriv_passkeys (0.0.1): + - Flutter + - device_info_plus (0.0.1): + - Flutter + - Flutter (1.0.0) + - flutter_deriv_api (0.0.1): + - Flutter + - flutter_inappwebview_ios (0.0.1): + - Flutter + - flutter_inappwebview_ios/Core (= 0.0.1) + - OrderedSet (~> 5.0) + - flutter_inappwebview_ios/Core (0.0.1): + - Flutter + - OrderedSet (~> 5.0) + - flutter_system_proxy (0.0.1): + - Flutter + - OrderedSet (5.0.0) + - package_info_plus (0.4.5): + - Flutter + - ReachabilitySwift (5.0.0) + - url_launcher_ios (0.0.1): + - Flutter + - webview_flutter_wkwebview (0.0.1): + - Flutter + +DEPENDENCIES: + - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) + - deriv_passkeys (from `.symlinks/plugins/deriv_passkeys/ios`) + - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) + - Flutter (from `Flutter`) + - flutter_deriv_api (from `.symlinks/plugins/flutter_deriv_api/ios`) + - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) + - flutter_system_proxy (from `.symlinks/plugins/flutter_system_proxy/ios`) + - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`) + +SPEC REPOS: + trunk: + - OrderedSet + - ReachabilitySwift + +EXTERNAL SOURCES: + connectivity_plus: + :path: ".symlinks/plugins/connectivity_plus/ios" + deriv_passkeys: + :path: ".symlinks/plugins/deriv_passkeys/ios" + device_info_plus: + :path: ".symlinks/plugins/device_info_plus/ios" + Flutter: + :path: Flutter + flutter_deriv_api: + :path: ".symlinks/plugins/flutter_deriv_api/ios" + flutter_inappwebview_ios: + :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" + flutter_system_proxy: + :path: ".symlinks/plugins/flutter_system_proxy/ios" + package_info_plus: + :path: ".symlinks/plugins/package_info_plus/ios" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + webview_flutter_wkwebview: + :path: ".symlinks/plugins/webview_flutter_wkwebview/ios" + +SPEC CHECKSUMS: + connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d + deriv_passkeys: fadd039a48bae6f15ba8b955481a0f8f2b5bdede + device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + flutter_deriv_api: 9e29abd7cc5091b72303f9c8be549618415f1437 + flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0 + flutter_system_proxy: 96eb97e3857a1d1bc533a6f7387a1f0dcb63d782 + OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c + package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 + ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 + url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812 + webview_flutter_wkwebview: 4f3e50f7273d31e5500066ed267e3ae4309c5ae4 + +PODFILE CHECKSUM: 23c5db403a9f2d103e08afd68b26a1601b49de37 + +COCOAPODS: 1.15.2 diff --git a/packages/deriv_passkeys/example/ios/Runner.xcodeproj/project.pbxproj b/packages/deriv_passkeys/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000..2aab18b65 --- /dev/null +++ b/packages/deriv_passkeys/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,747 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 747C8817C0330ABBFB890B14 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1C7398745361BCCB0E96EA4D /* Pods_Runner.framework */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 912B875A1A7FB5C581324BD6 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D7BD61FE28A65EBE1195EF83 /* Pods_RunnerTests.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 */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 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 = ""; }; + 1C7398745361BCCB0E96EA4D /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 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 = ""; }; + 49A0B9C2519474E7D84EAC8C /* 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 = ""; }; + 532A03FC307182502C1BD3CD /* 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 = ""; }; + 5C02BF7919AD58348F432BF5 /* 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 = ""; }; + 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 = ""; }; + 7D84192F82F1C69314508FE0 /* 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 = ""; }; + 7F9ECC56F29EED37FD5095E7 /* 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 = ""; }; + 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 = ""; }; + 9AFCD78B2B50133C003DE0AD /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; + CBC9E18AB54CF7F16148E91E /* 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 = ""; }; + D7BD61FE28A65EBE1195EF83 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 29E5AF7624FE1A1191CC1B37 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 912B875A1A7FB5C581324BD6 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 747C8817C0330ABBFB890B14 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 845CB8412A1191B744F55060 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 1C7398745361BCCB0E96EA4D /* Pods_Runner.framework */, + D7BD61FE28A65EBE1195EF83 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 8669CBC0392B21497E31E534 /* Pods */ = { + isa = PBXGroup; + children = ( + 7F9ECC56F29EED37FD5095E7 /* Pods-Runner.debug.xcconfig */, + 49A0B9C2519474E7D84EAC8C /* Pods-Runner.release.xcconfig */, + 532A03FC307182502C1BD3CD /* Pods-Runner.profile.xcconfig */, + CBC9E18AB54CF7F16148E91E /* Pods-RunnerTests.debug.xcconfig */, + 5C02BF7919AD58348F432BF5 /* Pods-RunnerTests.release.xcconfig */, + 7D84192F82F1C69314508FE0 /* Pods-RunnerTests.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + 8669CBC0392B21497E31E534 /* Pods */, + 845CB8412A1191B744F55060 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 9AFCD78B2B50133C003DE0AD /* Runner.entitlements */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 78EBCC64ED315B282CABDF60 /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + 29E5AF7624FE1A1191CC1B37 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + BBFD2B46E15A0299A268D1AE /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + F0ABF9E044B3B98C9A697E0E /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1430; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 78EBCC64ED315B282CABDF60 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + 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; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + BBFD2B46E15A0299A268D1AE /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-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; + }; + F0ABF9E044B3B98C9A697E0E /* [CP] Embed Pods Frameworks */ = { + 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"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 36S5Q8S4V5; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.deriv.sample; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Deriv sample app development profile"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CBC9E18AB54CF7F16148E91E /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.deriv.sample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5C02BF7919AD58348F432BF5 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.deriv.sample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7D84192F82F1C69314508FE0 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.deriv.sample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 36S5Q8S4V5; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.deriv.sample; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Deriv sample app development profile"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 36S5Q8S4V5; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.deriv.sample; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Deriv sample app development profile"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/deriv_passkeys/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/deriv_passkeys/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/packages/deriv_passkeys/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/deriv_passkeys/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/deriv_passkeys/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/packages/deriv_passkeys/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/deriv_passkeys/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/deriv_passkeys/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000..f9b0d7c5e --- /dev/null +++ b/packages/deriv_passkeys/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/deriv_passkeys/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/deriv_passkeys/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000..87131a09b --- /dev/null +++ b/packages/deriv_passkeys/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/deriv_passkeys/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/deriv_passkeys/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..21a3cc14c --- /dev/null +++ b/packages/deriv_passkeys/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/deriv_passkeys/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/deriv_passkeys/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/packages/deriv_passkeys/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/deriv_passkeys/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/deriv_passkeys/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000..f9b0d7c5e --- /dev/null +++ b/packages/deriv_passkeys/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/deriv_passkeys/example/ios/Runner/AppDelegate.swift b/packages/deriv_passkeys/example/ios/Runner/AppDelegate.swift new file mode 100644 index 000000000..70693e4a8 --- /dev/null +++ b/packages/deriv_passkeys/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..d36b1fab2 --- /dev/null +++ b/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..dc9ada4725e9b0ddb1deab583e5b5102493aa332 GIT binary patch literal 10932 zcmeHN2~<R zh`|8`A_PQ1nSu(UMFx?8j8PC!!VDphaL#`F42fd#7Vlc`zIE4n%Y~eiz4y1j|NDpi z?<@|pSJ-HM`qifhf@m%MamgwK83`XpBA<+azdF#2QsT{X@z0A9Bq>~TVErigKH1~P zRX-!h-f0NJ4Mh++{D}J+K>~~rq}d%o%+4dogzXp7RxX4C>Km5XEI|PAFDmo;DFm6G zzjVoB`@qW98Yl0Kvc-9w09^PrsobmG*Eju^=3f?0o-t$U)TL1B3;sZ^!++3&bGZ!o-*6w?;oOhf z=A+Qb$scV5!RbG+&2S}BQ6YH!FKb0``VVX~T$dzzeSZ$&9=X$3)_7Z{SspSYJ!lGE z7yig_41zpQ)%5dr4ff0rh$@ky3-JLRk&DK)NEIHecf9c*?Z1bUB4%pZjQ7hD!A0r-@NF(^WKdr(LXj|=UE7?gBYGgGQV zidf2`ZT@pzXf7}!NH4q(0IMcxsUGDih(0{kRSez&z?CFA0RVXsVFw3^u=^KMtt95q z43q$b*6#uQDLoiCAF_{RFc{!H^moH_cmll#Fc^KXi{9GDl{>%+3qyfOE5;Zq|6#Hb zp^#1G+z^AXfRKaa9HK;%b3Ux~U@q?xg<2DXP%6k!3E)PA<#4$ui8eDy5|9hA5&{?v z(-;*1%(1~-NTQ`Is1_MGdQ{+i*ccd96ab$R$T3=% zw_KuNF@vI!A>>Y_2pl9L{9h1-C6H8<)J4gKI6{WzGBi<@u3P6hNsXG=bRq5c+z;Gc3VUCe;LIIFDmQAGy+=mRyF++u=drBWV8-^>0yE9N&*05XHZpPlE zxu@?8(ZNy7rm?|<+UNe0Vs6&o?l`Pt>P&WaL~M&#Eh%`rg@Mbb)J&@DA-wheQ>hRV z<(XhigZAT z>=M;URcdCaiO3d^?H<^EiEMDV+7HsTiOhoaMX%P65E<(5xMPJKxf!0u>U~uVqnPN7T!X!o@_gs3Ct1 zlZ_$5QXP4{Aj645wG_SNT&6m|O6~Tsl$q?nK*)(`{J4b=(yb^nOATtF1_aS978$x3 zx>Q@s4i3~IT*+l{@dx~Hst21fR*+5}S1@cf>&8*uLw-0^zK(+OpW?cS-YG1QBZ5q! zgTAgivzoF#`cSz&HL>Ti!!v#?36I1*l^mkrx7Y|K6L#n!-~5=d3;K<;Zqi|gpNUn_ z_^GaQDEQ*jfzh;`j&KXb66fWEk1K7vxQIMQ_#Wu_%3 z4Oeb7FJ`8I>Px;^S?)}2+4D_83gHEq>8qSQY0PVP?o)zAv3K~;R$fnwTmI-=ZLK`= zTm+0h*e+Yfr(IlH3i7gUclNH^!MU>id$Jw>O?2i0Cila#v|twub21@e{S2v}8Z13( zNDrTXZVgris|qYm<0NU(tAPouG!QF4ZNpZPkX~{tVf8xY690JqY1NVdiTtW+NqyRP zZ&;T0ikb8V{wxmFhlLTQ&?OP7 z;(z*<+?J2~z*6asSe7h`$8~Se(@t(#%?BGLVs$p``;CyvcT?7Y!{tIPva$LxCQ&4W z6v#F*);|RXvI%qnoOY&i4S*EL&h%hP3O zLsrFZhv&Hu5tF$Lx!8(hs&?!Kx5&L(fdu}UI5d*wn~A`nPUhG&Rv z2#ixiJdhSF-K2tpVL=)5UkXRuPAFrEW}7mW=uAmtVQ&pGE-&az6@#-(Te^n*lrH^m@X-ftVcwO_#7{WI)5v(?>uC9GG{lcGXYJ~Q8q zbMFl7;t+kV;|;KkBW2!P_o%Czhw&Q(nXlxK9ak&6r5t_KH8#1Mr-*0}2h8R9XNkr zto5-b7P_auqTJb(TJlmJ9xreA=6d=d)CVbYP-r4$hDn5|TIhB>SReMfh&OVLkMk-T zYf%$taLF0OqYF?V{+6Xkn>iX@TuqQ?&cN6UjC9YF&%q{Ut3zv{U2)~$>-3;Dp)*(? zg*$mu8^i=-e#acaj*T$pNowo{xiGEk$%DusaQiS!KjJH96XZ-hXv+jk%ard#fu=@Q z$AM)YWvE^{%tDfK%nD49=PI|wYu}lYVbB#a7wtN^Nml@CE@{Gv7+jo{_V?I*jkdLD zJE|jfdrmVbkfS>rN*+`#l%ZUi5_bMS<>=MBDNlpiSb_tAF|Zy`K7kcp@|d?yaTmB^ zo?(vg;B$vxS|SszusORgDg-*Uitzdi{dUV+glA~R8V(?`3GZIl^egW{a919!j#>f` znL1o_^-b`}xnU0+~KIFLQ)$Q6#ym%)(GYC`^XM*{g zv3AM5$+TtDRs%`2TyR^$(hqE7Y1b&`Jd6dS6B#hDVbJlUXcG3y*439D8MrK!2D~6gn>UD4Imctb z+IvAt0iaW73Iq$K?4}H`7wq6YkTMm`tcktXgK0lKPmh=>h+l}Y+pDtvHnG>uqBA)l zAH6BV4F}v$(o$8Gfo*PB>IuaY1*^*`OTx4|hM8jZ?B6HY;F6p4{`OcZZ(us-RVwDx zUzJrCQlp@mz1ZFiSZ*$yX3c_#h9J;yBE$2g%xjmGF4ca z&yL`nGVs!Zxsh^j6i%$a*I3ZD2SoNT`{D%mU=LKaEwbN(_J5%i-6Va?@*>=3(dQy` zOv%$_9lcy9+(t>qohkuU4r_P=R^6ME+wFu&LA9tw9RA?azGhjrVJKy&8=*qZT5Dr8g--d+S8zAyJ$1HlW3Olryt`yE zFIph~Z6oF&o64rw{>lgZISC6p^CBer9C5G6yq%?8tC+)7*d+ib^?fU!JRFxynRLEZ zj;?PwtS}Ao#9whV@KEmwQgM0TVP{hs>dg(1*DiMUOKHdQGIqa0`yZnHk9mtbPfoLx zo;^V6pKUJ!5#n`w2D&381#5#_t}AlTGEgDz$^;u;-vxDN?^#5!zN9ngytY@oTv!nc zp1Xn8uR$1Z;7vY`-<*?DfPHB;x|GUi_fI9@I9SVRv1)qETbNU_8{5U|(>Du84qP#7 z*l9Y$SgA&wGbj>R1YeT9vYjZuC@|{rajTL0f%N@>3$DFU=`lSPl=Iv;EjuGjBa$Gw zHD-;%YOE@<-!7-Mn`0WuO3oWuL6tB2cpPw~Nvuj|KM@))ixuDK`9;jGMe2d)7gHin zS<>k@!x;!TJEc#HdL#RF(`|4W+H88d4V%zlh(7#{q2d0OQX9*FW^`^_<3r$kabWAB z$9BONo5}*(%kx zOXi-yM_cmB3>inPpI~)duvZykJ@^^aWzQ=eQ&STUa}2uT@lV&WoRzkUoE`rR0)`=l zFT%f|LA9fCw>`enm$p7W^E@U7RNBtsh{_-7vVz3DtB*y#*~(L9+x9*wn8VjWw|Q~q zKFsj1Yl>;}%MG3=PY`$g$_mnyhuV&~O~u~)968$0b2!Jkd;2MtAP#ZDYw9hmK_+M$ zb3pxyYC&|CuAbtiG8HZjj?MZJBFbt`ryf+c1dXFuC z0*ZQhBzNBd*}s6K_G}(|Z_9NDV162#y%WSNe|FTDDhx)K!c(mMJh@h87@8(^YdK$&d*^WQe8Z53 z(|@MRJ$Lk-&ii74MPIs80WsOFZ(NX23oR-?As+*aq6b?~62@fSVmM-_*cb1RzZ)`5$agEiL`-E9s7{GM2?(KNPgK1(+c*|-FKoy}X(D_b#etO|YR z(BGZ)0Ntfv-7R4GHoXp?l5g#*={S1{u-QzxCGng*oWr~@X-5f~RA14b8~B+pLKvr4 zfgL|7I>jlak9>D4=(i(cqYf7#318!OSR=^`xxvI!bBlS??`xxWeg?+|>MxaIdH1U~#1tHu zB{QMR?EGRmQ_l4p6YXJ{o(hh-7Tdm>TAX380TZZZyVkqHNzjUn*_|cb?T? zt;d2s-?B#Mc>T-gvBmQZx(y_cfkXZO~{N zT6rP7SD6g~n9QJ)8F*8uHxTLCAZ{l1Y&?6v)BOJZ)=R-pY=Y=&1}jE7fQ>USS}xP#exo57uND0i*rEk@$;nLvRB@u~s^dwRf?G?_enN@$t* zbL%JO=rV(3Ju8#GqUpeE3l_Wu1lN9Y{D4uaUe`g>zlj$1ER$6S6@{m1!~V|bYkhZA z%CvrDRTkHuajMU8;&RZ&itnC~iYLW4DVkP<$}>#&(`UO>!n)Po;Mt(SY8Yb`AS9lt znbX^i?Oe9r_o=?})IHKHoQGKXsps_SE{hwrg?6dMI|^+$CeC&z@*LuF+P`7LfZ*yr+KN8B4{Nzv<`A(wyR@!|gw{zB6Ha ziwPAYh)oJ(nlqSknu(8g9N&1hu0$vFK$W#mp%>X~AU1ay+EKWcFdif{% z#4!4aoVVJ;ULmkQf!ke2}3hqxLK>eq|-d7Ly7-J9zMpT`?dxo6HdfJA|t)?qPEVBDv z{y_b?4^|YA4%WW0VZd8C(ZgQzRI5(I^)=Ub`Y#MHc@nv0w-DaJAqsbEHDWG8Ia6ju zo-iyr*sq((gEwCC&^TYBWt4_@|81?=B-?#P6NMff(*^re zYqvDuO`K@`mjm_Jd;mW_tP`3$cS?R$jR1ZN09$YO%_iBqh5ftzSpMQQtxKFU=FYmP zeY^jph+g<4>YO;U^O>-NFLn~-RqlHvnZl2yd2A{Yc1G@Ga$d+Q&(f^tnPf+Z7serIU};17+2DU_f4Z z@GaPFut27d?!YiD+QP@)T=77cR9~MK@bd~pY%X(h%L={{OIb8IQmf-!xmZkm8A0Ga zQSWONI17_ru5wpHg3jI@i9D+_Y|pCqVuHJNdHUauTD=R$JcD2K_liQisqG$(sm=k9;L* z!L?*4B~ql7uioSX$zWJ?;q-SWXRFhz2Jt4%fOHA=Bwf|RzhwqdXGr78y$J)LR7&3T zE1WWz*>GPWKZ0%|@%6=fyx)5rzUpI;bCj>3RKzNG_1w$fIFCZ&UR0(7S?g}`&Pg$M zf`SLsz8wK82Vyj7;RyKmY{a8G{2BHG%w!^T|Njr!h9TO2LaP^_f22Q1=l$QiU84ao zHe_#{S6;qrC6w~7{y(hs-?-j?lbOfgH^E=XcSgnwW*eEz{_Z<_xN#0001NP)t-s|Ns9~ z#rXRE|M&d=0au&!`~QyF`q}dRnBDt}*!qXo`c{v z{Djr|@Adh0(D_%#_&mM$D6{kE_x{oE{l@J5@%H*?%=t~i_`ufYOPkAEn!pfkr2$fs z652Tz0001XNklqeeKN4RM4i{jKqmiC$?+xN>3Apn^ z0QfuZLym_5b<*QdmkHjHlj811{If)dl(Z2K0A+ekGtrFJb?g|wt#k#pV-#A~bK=OT ts8>{%cPtyC${m|1#B1A6#u!Q;umknL1chzTM$P~L002ovPDHLkV1lTfnu!1a literal 0 HcmV?d00001 diff --git a/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..797d452e458972bab9d994556c8305db4c827017 GIT binary patch literal 406 zcmV;H0crk;P))>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed2d933e1120817fe9182483a228007b18ab6ae GIT binary patch literal 450 zcmV;z0X_bSP)iGWQ_5NJQ_~rNh*z)}eT%KUb z`7gNk0#AwF^#0T0?hIa^`~Ck;!}#m+_uT050aTR(J!bU#|IzRL%^UsMS#KsYnTF*!YeDOytlP4VhV?b} z%rz_<=#CPc)tU1MZTq~*2=8~iZ!lSa<{9b@2Jl;?IEV8)=fG217*|@)CCYgFze-x? zIFODUIA>nWKpE+bn~n7;-89sa>#DR>TSlqWk*!2hSN6D~Qb#VqbP~4Fk&m`@1$JGr zXPIdeRE&b2Thd#{MtDK$px*d3-Wx``>!oimf%|A-&-q*6KAH)e$3|6JV%HX{Hig)k suLT-RhftRq8b9;(V=235Wa|I=027H2wCDra;{X5v07*qoM6N<$f;9x^2LJ#7 literal 0 HcmV?d00001 diff --git a/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..4cd7b0099ca80c806f8fe495613e8d6c69460d76 GIT binary patch literal 282 zcmV+#0p(^bcu7P-R4C8Q z&e;xxFbF_Vrezo%_kH*OKhshZ6BFpG-Y1e10`QXJKbND7AMQ&cMj60B5TNObaZxYybcN07*qoM6N<$g3m;S%K!iX literal 0 HcmV?d00001 diff --git a/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fe730945a01f64a61e2235dbe3f45b08f7729182 GIT binary patch literal 462 zcmV;<0WtoGP)-}iV`2<;=$?g5M=KQbZ{F&YRNy7Nn@%_*5{gvDM0aKI4?ESmw z{NnZg)A0R`+4?NF_RZexyVB&^^ZvN!{I28tr{Vje;QNTz`dG&Jz0~Ek&f2;*Z7>B|cg}xYpxEFY+0YrKLF;^Q+-HreN0P{&i zK~zY`?b7ECf-n?@;d<&orQ*Q7KoR%4|C>{W^h6@&01>0SKS`dn{Q}GT%Qj_{PLZ_& zs`MFI#j-(>?bvdZ!8^xTwlY{qA)T4QLbY@j(!YJ7aXJervHy6HaG_2SB`6CC{He}f zHVw(fJWApwPq!6VY7r1w-Fs)@ox~N+q|w~e;JI~C4Vf^@d>Wvj=fl`^u9x9wd9 zR%3*Q+)t%S!MU_`id^@&Y{y7-r98lZX0?YrHlfmwb?#}^1b{8g&KzmkE(L>Z&)179 zp<)v6Y}pRl100G2FL_t(o!|l{-Q-VMg#&MKg7c{O0 z2wJImOS3Gy*Z2Qifdv~JYOp;v+U)a|nLoc7hNH;I$;lzDt$}rkaFw1mYK5_0Q(Sut zvbEloxON7$+HSOgC9Z8ltuC&0OSF!-mXv5caV>#bc3@hBPX@I$58-z}(ZZE!t-aOG zpjNkbau@>yEzH(5Yj4kZiMH32XI!4~gVXNnjAvRx;Sdg^`>2DpUEwoMhTs_st8pKG z(%SHyHdU&v%f36~uERh!bd`!T2dw;z6PrOTQ7Vt*#9F2uHlUVnb#ev_o^fh}Dzmq} zWtlk35}k=?xj28uO|5>>$yXadTUE@@IPpgH`gJ~Ro4>jd1IF|(+IX>8M4Ps{PNvmI zNj4D+XgN83gPt_Gm}`Ybv{;+&yu-C(Grdiahmo~BjG-l&mWM+{e5M1sm&=xduwgM9 z`8OEh`=F3r`^E{n_;%9weN{cf2%7=VzC@cYj+lg>+3|D|_1C@{hcU(DyQG_BvBWe? zvTv``=%b1zrol#=R`JB)>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..502f463a9bc882b461c96aadf492d1729e49e725 GIT binary patch literal 586 zcmV-Q0=4~#P)+}#`wDE{8-2Mebf5<{{PqV{TgVcv*r8?UZ3{-|G?_}T*&y;@cqf{ z{Q*~+qr%%p!1pS*_Uicl#q9lc(D`!D`LN62sNwq{oYw(Wmhk)k<@f$!$@ng~_5)Ru z0Z)trIA5^j{DIW^c+vT2%lW+2<(RtE2wR;4O@)Tm`Xr*?A(qYoM}7i5Yxw>D(&6ou zxz!_Xr~yNF+waPe00049Nkl*;a!v6h%{rlvIH#gW3s8p;bFr=l}mRqpW2h zw=OA%hdyL~z+UHOzl0eKhEr$YYOL-c-%Y<)=j?(bzDweB7{b+%_ypvm_cG{SvM=DK zhv{K@m>#Bw>2W$eUI#iU)Wdgs8Y3U+A$Gd&{+j)d)BmGKx+43U_!tik_YlN)>$7G! zhkE!s;%oku3;IwG3U^2kw?z+HM)jB{@zFhK8P#KMSytSthr+4!c(5c%+^UBn`0X*2 zy3(k600_CSZj?O$Qu%&$;|TGUJrptR(HzyIx>5E(2r{eA(<6t3e3I0B)7d6s7?Z5J zZ!rtKvA{MiEBm&KFtoifx>5P^Z=vl)95XJn()aS5%ad(s?4-=Tkis9IGu{`Fy8r+H07*qoM6N<$f20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0ec303439225b78712f49115768196d8d76f6790 GIT binary patch literal 862 zcmV-k1EKthP)20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..e9f5fea27c705180eb716271f41b582e76dcbd90 GIT binary patch literal 1674 zcmV;526g#~P){YQnis^a@{&-nmRmq)<&%Mztj67_#M}W?l>kYSliK<%xAp;0j{!}J0!o7b zE>q9${Lb$D&h7k=+4=!ek^n+`0zq>LL1O?lVyea53S5x`Nqqo2YyeuIrQrJj9XjOp z{;T5qbj3}&1vg1VK~#9!?b~^C5-}JC@Pyrv-6dSEqJqT}#j9#dJ@GzT@B8}x zU&J@bBI>f6w6en+CeI)3^kC*U?}X%OD8$Fd$H&LV$H&LV$H&LV#|K5~mLYf|VqzOc zkc7qL~0sOYuM{tG`rYEDV{DWY`Z8&)kW*hc2VkBuY+^Yx&92j&StN}Wp=LD zxoGxXw6f&8sB^u})h@b@z0RBeD`K7RMR9deyL(ZJu#39Z>rT)^>v}Khq8U-IbIvT> z?4pV9qGj=2)TNH3d)=De<+^w;>S7m_eFKTvzeaBeir45xY!^m!FmxnljbSS_3o=g( z->^wC9%qkR{kbGnW8MfFew_o9h3(r55Is`L$8KI@d+*%{=Nx+FXJ98L0PjFIu;rGnnfY zn1R5Qnp<{Jq0M1vX=X&F8gtLmcWv$1*M@4ZfF^9``()#hGTeKeP`1!iED ztNE(TN}M5}3Bbc*d=FIv`DNv&@|C6yYj{sSqUj5oo$#*0$7pu|Dd2TLI>t5%I zIa4Dvr(iayb+5x=j*Vum9&irk)xV1`t509lnPO0%skL8_1c#Xbamh(2@f?4yUI zhhuT5<#8RJhGz4%b$`PJwKPAudsm|at?u;*hGgnA zU1;9gnxVBC)wA(BsB`AW54N{|qmikJR*%x0c`{LGsSfa|NK61pYH(r-UQ4_JXd!Rsz)=k zL{GMc5{h138)fF5CzHEDM>+FqY)$pdN3}Ml+riTgJOLN0F*Vh?{9ESR{SVVg>*>=# zix;VJHPtvFFCRY$Ks*F;VX~%*r9F)W`PmPE9F!(&s#x07n2<}?S{(ygpXgX-&B&OM zONY&BRQ(#%0%jeQs?oJ4P!p*R98>qCy5p8w>_gpuh39NcOlp)(wOoz0sY-Qz55eB~ z7OC-fKBaD1sE3$l-6QgBJO!n?QOTza`!S_YK z_v-lm^7{VO^8Q@M_^8F)09Ki6%=s?2_5eupee(w1FB%aqSweusQ-T+CH0Xt{` zFjMvW{@C&TB)k25()nh~_yJ9coBRL(0oO@HK~z}7?bm5j;y@69;bvlHb2tf!$ReA~x{22wTq550 z?f?Hnw(;m3ip30;QzdV~7pi!wyMYhDtXW#cO7T>|f=bdFhu+F!zMZ2UFj;GUKX7tI z;hv3{q~!*pMj75WP_c}>6)IWvg5_yyg<9Op()eD1hWC19M@?_9_MHec{Z8n3FaF{8 z;u`Mw0ly(uE>*CgQYv{be6ab2LWhlaH1^iLIM{olnag$78^Fd}%dR7;JECQ+hmk|o z!u2&!3MqPfP5ChDSkFSH8F2WVOEf0(E_M(JL17G}Y+fg0_IuW%WQ zG(mG&u?|->YSdk0;8rc{yw2@2Z&GA}z{Wb91Ooz9VhA{b2DYE7RmG zjL}?eq#iX%3#k;JWMx_{^2nNax`xPhByFiDX+a7uTGU|otOvIAUy|dEKkXOm-`aWS z27pUzD{a)Ct<6p{{3)+lq@i`t@%>-wT4r?*S}k)58e09WZYP0{{R3FC5Sl00039P)t-s|Ns9~ z#rP?<_5oL$Q^olD{r_0T`27C={r>*`|Nj71npVa5OTzc(_WfbW_({R{p56NV{r*M2 z_xt?)2V0#0NsfV0u>{42ctGP(8vQj-Btk1n|O0ZD=YLwd&R{Ko41Gr9H= zY@z@@bOAMB5Ltl$E>bJJ{>JP30ZxkmI%?eW{k`b?Wy<&gOo;dS`~CR$Vwb@XWtR|N zi~t=w02?-0&j0TD{>bb6sNwsK*!p?V`RMQUl(*DVjk-9Cx+-z1KXab|Ka2oXhX5f% z`$|e!000AhNklrxs)5QTeTVRiEmz~MKK1WAjCw(c-JK6eox;2O)?`? zTG`AHia671e^vgmp!llKp|=5sVHk#C7=~epA~VAf-~%aPC=%Qw01h8mnSZ|p?hz91 z7p83F3%LVu9;S$tSI$C^%^yud1dfTM_6p2|+5Ejp$bd`GDvbR|xit>i!ZD&F>@CJrPmu*UjD&?DfZs=$@e3FQA(vNiU+$A*%a} z?`XcG2jDxJ_ZQ#Md`H{4Lpf6QBDp81_KWZ6Tk#yCy1)32zO#3<7>b`eT7UyYH1eGz z;O(rH$=QR*L%%ZcBpc=eGua?N55nD^K(8<#gl2+pN_j~b2MHs4#mcLmv%DkspS-3< zpI1F=^9siI0s-;IN_IrA;5xm~3?3!StX}pUv0vkxMaqm+zxrg7X7(I&*N~&dEd0kD z-FRV|g=|QuUsuh>-xCI}vD2imzYIOIdcCVV=$Bz@*u0+Bs<|L^)32nN*=wu3n%Ynw z@1|eLG>!8ruU1pFXUfb`j>(=Gy~?Rn4QJ-c3%3T|(Frd!bI`9u&zAnyFYTqlG#&J7 zAkD(jpw|oZLNiA>;>hgp1KX7-wxC~31II47gc zHcehD6Uxlf%+M^^uN5Wc*G%^;>D5qT{>=uxUhX%WJu^Z*(_Wq9y}npFO{Hhb>s6<9 zNi0pHXWFaVZnb)1+RS&F)xOv6&aeILcI)`k#0YE+?e)5&#r7J#c`3Z7x!LpTc01dx zrdC3{Z;joZ^KN&))zB_i)I9fWedoN>Zl-6_Iz+^G&*ak2jpF07*qoM6N<$f;w%0(f|Me literal 0 HcmV?d00001 diff --git a/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0467bf12aa4d28f374bb26596605a46dcbb3e7c8 GIT binary patch literal 1418 zcmV;51$Fv~P)q zKfU)WzW*n(@|xWGCA9ScMt*e9`2kdxPQ&&>|-UCa7_51w+ zLUsW@ZzZSW0y$)Hp~e9%PvP|a03ks1`~K?q{u;6NC8*{AOqIUq{CL&;p56Lf$oQGq z^={4hPQv)y=I|4n+?>7Fim=dxt1 z2H+Dm+1+fh+IF>G0SjJMkQQre1x4|G*Z==(Ot&kCnUrL4I(rf(ucITwmuHf^hXiJT zkdTm&kdTm&kdTm&kdP`esgWG0BcWCVkVZ&2dUwN`cgM8QJb`Z7Z~e<&Yj2(}>Tmf` zm1{eLgw!b{bXkjWbF%dTkTZEJWyWOb##Lfw4EK2}<0d6%>AGS{po>WCOy&f$Tay_> z?NBlkpo@s-O;0V%Y_Xa-G#_O08q5LR*~F%&)}{}r&L%Sbs8AS4t7Y0NEx*{soY=0MZExqA5XHQkqi#4gW3 zqODM^iyZl;dvf)-bOXtOru(s)Uc7~BFx{w-FK;2{`VA?(g&@3z&bfLFyctOH!cVsF z7IL=fo-qBndRUm;kAdXR4e6>k-z|21AaN%ubeVrHl*<|s&Ax@W-t?LR(P-24A5=>a z*R9#QvjzF8n%@1Nw@?CG@6(%>+-0ASK~jEmCV|&a*7-GKT72W<(TbSjf)&Eme6nGE z>Gkj4Sq&2e+-G%|+NM8OOm5zVl9{Z8Dd8A5z3y8mZ=4Bv4%>as_{9cN#bm~;h>62( zdqY93Zy}v&c4n($Vv!UybR8ocs7#zbfX1IY-*w~)p}XyZ-SFC~4w>BvMVr`dFbelV{lLL0bx7@*ZZdebr3`sP;? zVImji)kG)(6Juv0lz@q`F!k1FE;CQ(D0iG$wchPbKZQELlsZ#~rt8#90Y_Xh&3U-< z{s<&cCV_1`^TD^ia9!*mQDq& zn2{r`j};V|uV%_wsP!zB?m%;FeaRe+X47K0e+KE!8C{gAWF8)lCd1u1%~|M!XNRvw zvtqy3iz0WSpWdhn6$hP8PaRBmp)q`#PCA`Vd#Tc$@f1tAcM>f_I@bC)hkI9|o(Iqv zo}Piadq!j76}004RBio<`)70k^`K1NK)q>w?p^C6J2ZC!+UppiK6&y3Kmbv&O!oYF z34$0Z;QO!JOY#!`qyGH<3Pd}Pt@q*A0V=3SVtWKRR8d8Z&@)3qLPA19LPA19LPEUC YUoZo%k(ykuW&i*H07*qoM6N<$f+CH{y8r+H literal 0 HcmV?d00001 diff --git a/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 000000000..0bedcf2fd --- /dev/null +++ b/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 000000000..89c2725b7 --- /dev/null +++ b/packages/deriv_passkeys/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/deriv_passkeys/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/deriv_passkeys/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000..f2e259c7c --- /dev/null +++ b/packages/deriv_passkeys/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/deriv_passkeys/example/ios/Runner/Base.lproj/Main.storyboard b/packages/deriv_passkeys/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 000000000..f3c28516f --- /dev/null +++ b/packages/deriv_passkeys/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/deriv_passkeys/example/ios/Runner/Info.plist b/packages/deriv_passkeys/example/ios/Runner/Info.plist new file mode 100644 index 000000000..5458fc418 --- /dev/null +++ b/packages/deriv_passkeys/example/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Example + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/packages/deriv_passkeys/example/ios/Runner/Runner-Bridging-Header.h b/packages/deriv_passkeys/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 000000000..308a2a560 --- /dev/null +++ b/packages/deriv_passkeys/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/packages/deriv_passkeys/example/ios/Runner/Runner.entitlements b/packages/deriv_passkeys/example/ios/Runner/Runner.entitlements new file mode 100644 index 000000000..d96c26284 --- /dev/null +++ b/packages/deriv_passkeys/example/ios/Runner/Runner.entitlements @@ -0,0 +1,15 @@ + + + + + com.apple.developer.associated-domains + + webcredentials:qa163.deriv.dev + applinks:qa163.deriv.dev + applinks:pro-7837426045311437779.frontendapi.corbado.io + webcredentials:pro-7837426045311437779.frontendapi.corbado.io + webcredentials:24be-94-207-97-237.ngrok-free.app + applinks:24be-94-207-97-237.ngrok-free.app + + + diff --git a/packages/deriv_passkeys/example/ios/RunnerTests/RunnerTests.swift b/packages/deriv_passkeys/example/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 000000000..86a7c3b1b --- /dev/null +++ b/packages/deriv_passkeys/example/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/packages/deriv_passkeys/example/lib/main.dart b/packages/deriv_passkeys/example/lib/main.dart new file mode 100644 index 000000000..4baf1c3f8 --- /dev/null +++ b/packages/deriv_passkeys/example/lib/main.dart @@ -0,0 +1,111 @@ +import 'package:deriv_http_client/deriv_http_client.dart'; +import 'package:deriv_localizations/l10n/generated/deriv_passkeys/deriv_passkeys_localizations.dart'; +import 'package:deriv_passkeys/deriv_passkeys.dart'; +import 'package:deriv_theme/deriv_theme.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'dart:async'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => DerivPasskeysBloc( + derivPasskeysService: DerivPasskeysService( + DerivPasskeysRepository( + DerivPasskeysDataSource( + mapper: DerivPasskeysMapper(), client: HttpClient()), + ), + ), + connectionInfo: PasskeysConnectionInfoEntity( + appId: '', + endpoint: '', + ), + getJwtToken: () { + return Future.value(''); + }, + ), + child: MaterialApp( + localizationsDelegates: const >[ + DerivPasskeysLocalizations.delegate, + ], + theme: ThemeData( + primaryColor: context.theme.colors.secondary, + fontFamily: context.theme.fontFamily, + brightness: Brightness.dark, + bottomSheetTheme: BottomSheetThemeData( + backgroundColor: Colors.black.withOpacity(0), + modalBarrierColor: Colors.black.withOpacity(0.72), + ), + unselectedWidgetColor: context.theme.colors.lessProminent, + toggleButtonsTheme: ToggleButtonsThemeData( + textStyle: context.theme.textStyle( + textStyle: TextStyles.body2, + ), + ), + colorScheme: const ColorScheme.dark().copyWith( + primary: context.theme.colors.prominent, + secondary: context.theme.colors.coral, + ), + textSelectionTheme: TextSelectionThemeData( + cursorColor: context.theme.colors.lessProminent, + selectionHandleColor: context.theme.colors.lessProminent, + selectionColor: context.theme.colors.lessProminent, + ), + appBarTheme: AppBarTheme.of(context).copyWith( + backgroundColor: context.theme.colors.secondary, + centerTitle: false, + ), + ), + home: const MyPage(), + ), + ); + } +} + +class MyPage extends StatelessWidget { + const MyPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: context.theme.colors.primary, + appBar: AppBar( + title: const Text('Deriv Passkeys example app'), + ), + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const ContinueWithPasskeyButton(), + SizedBox( + width: double.infinity, + child: Padding( + padding: const EdgeInsets.all(16), + child: TextButton( + onPressed: () { + context + .read() + .add(DerivPasskeysCreateCredentialEvent()); + }, + child: const Text('Create Passkey'), + ), + ), + ) + ], + ), + ); + } +} diff --git a/packages/deriv_passkeys/example/pubspec.yaml b/packages/deriv_passkeys/example/pubspec.yaml new file mode 100644 index 000000000..9b22b0acb --- /dev/null +++ b/packages/deriv_passkeys/example/pubspec.yaml @@ -0,0 +1,77 @@ +name: passkeys_poc +description: A new Flutter project. +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + deriv_theme: + git: + url: git@github.com:regentmarkets/flutter-deriv-packages.git + path: packages/deriv_theme + ref: deriv_theme-v2.4.0 + deriv_http_client: + git: + url: git@github.com:regentmarkets/flutter-deriv-packages.git + path: packages/deriv_http_client + ref: deriv_http_client-v2.0.1 + deriv_localizations: + git: + url: git@github.com:regentmarkets/flutter-deriv-packages.git + path: packages/deriv_localizations + ref: deriv_localizations-v1.3.2 + deriv_passkeys: + path: ../ + + cupertino_icons: ^1.0.2 + dartz: ^0.10.1 + json_annotation: ^4.8.1 + http: ^1.1.0 + flutter_platform_widgets: ^3.3.5 + flutter_bloc: ^8.1.3 + equatable: ^2.0.5 + +dependency_overrides: + flutter_svg: ^2.0.9 + http: ^1.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + + + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^2.0.0 + + + +flutter: + uses-material-design: true diff --git a/packages/deriv_passkeys/example/test/widget_test.dart b/packages/deriv_passkeys/example/test/widget_test.dart new file mode 100644 index 000000000..9468a3906 --- /dev/null +++ b/packages/deriv_passkeys/example/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:passkeys_poc/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/packages/deriv_passkeys/ios/.gitignore b/packages/deriv_passkeys/ios/.gitignore new file mode 100644 index 000000000..0c885071e --- /dev/null +++ b/packages/deriv_passkeys/ios/.gitignore @@ -0,0 +1,38 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/ephemeral/ +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/packages/deriv_passkeys/ios/Classes/DerivPasskeysManager.swift b/packages/deriv_passkeys/ios/Classes/DerivPasskeysManager.swift new file mode 100644 index 000000000..cef52b6f8 --- /dev/null +++ b/packages/deriv_passkeys/ios/Classes/DerivPasskeysManager.swift @@ -0,0 +1,388 @@ +import Flutter +import UIKit +import AuthenticationServices + +@available(iOS 15.0, *) +public class DerivPasskeysManager{ + enum PluginError: Error { + case unknownError + case notFound(String) + case unknownCredentialType(AnyObject) + case excludedCredentialExists + } + + class AuthCtrlDelegate: NSObject, ASAuthorizationControllerDelegate { + private let semaphore = DispatchSemaphore(value: 0) + private var result: (controller: ASAuthorizationController, authorization: ASAuthorization?, error: Error?)? + + func getResult() throws -> (controller: ASAuthorizationController, authorization: ASAuthorization?, error: Error?) { + semaphore.wait() + guard let ret = result else { + throw PluginError.unknownError + } + return ret + } + + func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { + result = (controller, authorization, nil) + semaphore.signal() + } + + func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { + result = (controller, nil, error) + semaphore.signal() + } + } + + @available(iOS 15.0, *) + private func getPlatformPublicKeyCredentials() -> [String] { + return (UserDefaults.standard.object(forKey: "platformPublicKeyCredentials") as? [String]) ?? [] + } + + @available(iOS 15.0, *) + private func savePlatformPublicKeyCredential(_ credential: ASAuthorizationCredential) { + var credentialID = "" + if let credential = credential as? ASAuthorizationPlatformPublicKeyCredentialRegistration { + credentialID = credential.credentialID.base64url + } + else if let credential = credential as? ASAuthorizationPlatformPublicKeyCredentialAssertion { + credentialID = credential.credentialID.base64url + } + if credentialID.isEmpty { return } + + var credentials = getPlatformPublicKeyCredentials() + if !credentials.contains(credentialID) { + credentials.append(credentialID) + UserDefaults.standard.set(credentials, forKey: "platformPublicKeyCredentials") + } + } + + + @available(iOS 15.0, *) + private func handlePlatformExcludedCredentials(_ options: PublicKeyCredentialRequestOptions) throws { + let excludeCredentials = options.excludeCredentials + guard excludeCredentials.count > 0 else { return } + let credentials = getPlatformPublicKeyCredentials() + for credential in credentials { + for excludeCredential in excludeCredentials { + if excludeCredential.base64url == credential { + throw PluginError.excludedCredentialExists + } + } + } + } + + @available(iOS 15.0, *) + private func findPlatformCredentials(_ credentials: [Data]) -> [Data] { + guard credentials.count > 0 else { return [] } + var foundCredentials: [Data] = [] + let platformCredentials = getPlatformPublicKeyCredentials() + for credential in credentials { + for platformCredential in platformCredentials { + if credential.base64url == platformCredential { + foundCredentials.append(credential) + break + } + } + } + return foundCredentials + } + + @available(iOS 15.0, *) + private func createPlatformPublicKeyCredentialRegistrationRequest(_ options: PublicKeyCredentialRequestOptions) throws -> ASAuthorizationPlatformPublicKeyCredentialRegistrationRequest { + let rpId = try options.rpId + let challenge = try options.challenge + let userName = try options.userName + let userId = try options.userId + let platformProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: rpId) + let registrationRequest = platformProvider.createCredentialRegistrationRequest(challenge: challenge, name: userName, userID: userId) + /*// Passkeys do not support attestation. + if let attestationPreference = options.attestation { + registrationRequest.attestationPreference = attestationPreference + } + */ + if let userVerificationPreference = options.userVerification { + registrationRequest.userVerificationPreference = userVerificationPreference + } + // ISSUE: Currently ASAuthorizationPlatformPublicKeyCredentialRegistrationRequest doesn't support excludedCredentials. + // WORKAROUND: Handling excludeCredentials by plugin self. + try handlePlatformExcludedCredentials(options) + return registrationRequest + } + + @available(iOS 15.0, *) + private func createSecurityKeyPublicKeyCredentialRegistrationRequest(_ options: PublicKeyCredentialRequestOptions) throws -> ASAuthorizationSecurityKeyPublicKeyCredentialRegistrationRequest { + let rpId = try options.rpId + let challenge = try options.challenge + let userName = try options.userName + let userId = try options.userId + let securityKeyProvider = ASAuthorizationSecurityKeyPublicKeyCredentialProvider(relyingPartyIdentifier: rpId) + let registrationRequest = securityKeyProvider.createCredentialRegistrationRequest(challenge: challenge, displayName: userName, name: userName, userID: userId) + registrationRequest.credentialParameters = options.pubKeyCredParams + if let attestationPreference = options.attestation { + registrationRequest.attestationPreference = attestationPreference + } + if let userVerificationPreference = options.userVerification { + registrationRequest.userVerificationPreference = userVerificationPreference + } + if let residentKeyPreference = options.residentKey { + registrationRequest.residentKeyPreference = residentKeyPreference + } + let excludedCredentials = options.excludeCredentials + if excludedCredentials.count > 0 { + registrationRequest.excludedCredentials = excludedCredentials.map { ASAuthorizationSecurityKeyPublicKeyCredentialDescriptor(credentialID: $0, transports: ASAuthorizationSecurityKeyPublicKeyCredentialDescriptor.Transport.allSupported) } + } + return registrationRequest + } + + @available(iOS 15.0, *) + private func createPlatformPublicKeyCredentialAssertionRequest(_ options: PublicKeyCredentialRequestOptions) throws -> ASAuthorizationPlatformPublicKeyCredentialAssertionRequest { + let rpId = try options.rpId + let challenge = try options.challenge + let platformProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: rpId) + let assertionRequest = platformProvider.createCredentialAssertionRequest(challenge: challenge) + if let userVerificationPreference = options.userVerification { + assertionRequest.userVerificationPreference = userVerificationPreference + } + let allowedCredentials = options.allowCredentials + // ISSUE: if allowedCredentials are different from SecurityKeyPublicKeyCredentialAssertionRequest, iOS Passkeys will hang on processing in background. + if allowedCredentials.count > 0 { + assertionRequest.allowedCredentials = allowedCredentials.map { ASAuthorizationPlatformPublicKeyCredentialDescriptor(credentialID: $0) } + } + return assertionRequest + } + + @available(iOS 15.0, *) + private func createSecurityKeyPublicKeyCredentialAssertionRequest(_ options: PublicKeyCredentialRequestOptions) throws -> ASAuthorizationSecurityKeyPublicKeyCredentialAssertionRequest { + let rpId = try options.rpId + let challenge = try options.challenge + let securityKeyProvider = ASAuthorizationSecurityKeyPublicKeyCredentialProvider(relyingPartyIdentifier: rpId) + let assertionRequest = securityKeyProvider.createCredentialAssertionRequest(challenge: challenge) + if let userVerificationPreference = options.userVerification { + assertionRequest.userVerificationPreference = userVerificationPreference + } + let allowedCredentials = options.allowCredentials + if allowedCredentials.count > 0 { + assertionRequest.allowedCredentials = allowedCredentials.map { ASAuthorizationSecurityKeyPublicKeyCredentialDescriptor(credentialID: $0, transports: ASAuthorizationSecurityKeyPublicKeyCredentialDescriptor.Transport.allSupported) } + } + return assertionRequest + } + + @available(iOS 15.0, *) + private func getPresentationContextProvider() throws -> ASAuthorizationControllerPresentationContextProviding { + let keyWindow = UIApplication.shared.connectedScenes + .filter({$0.activationState == .foregroundActive}) + .map({$0 as? UIWindowScene}) + .compactMap({$0}) + .first?.windows + .filter({$0.isKeyWindow}).first + guard var topController = keyWindow?.rootViewController else { + throw PluginError.notFound("Root view controller") + } + while let presentedViewController = topController.presentedViewController { + topController = presentedViewController + } + if let nav = topController as? UINavigationController { + topController = nav.visibleViewController ?? topController + } + guard let contextProvider = topController as? ASAuthorizationControllerPresentationContextProviding else { + throw PluginError.notFound("Presentation context provider") + } + return contextProvider + } + + @available(iOS 15.0, *) + private func requestCredential(_ authorizationRequests: [ASAuthorizationRequest]) throws -> ASAuthorizationCredential { + let authController = ASAuthorizationController(authorizationRequests: authorizationRequests) + let authCtrlDelete = AuthCtrlDelegate() + let contextProvider = try getPresentationContextProvider() + authController.delegate = authCtrlDelete + authController.presentationContextProvider = contextProvider + authController.performRequests() + + let result = try authCtrlDelete.getResult() + guard result.error == nil else { + throw result.error! + } + guard let authorization = result.authorization else { + throw PluginError.notFound("Credential") + } + // ISSUE: Currently ASAuthorizationPlatformPublicKeyCredentialRegistrationRequest doesn't support excludedCredentials. + // WORKAROUND: Handling excludeCredentials by plugin self. + // SIDE-EFFECT: Assertions may come from another device by scanning QR code, in that case, the credential SHALL NOT be saved, but there is no way to know the assertion source. This leads that credential registration fails if excludedCredentials includes the credential. + savePlatformPublicKeyCredential(authorization.credential) + return authorization.credential + } + + @available(iOS 15.0, *) + private func generateCredentialRegistrationResponse(_ credentialID: Data, _ rawClientDataJSON: Data, _ rawAttestationObject: Data? = nil) throws -> [String: Any] { + let response = [ + "id": credentialID.base64url, + "rawId": credentialID.base64url, + "type": "public-key", + "response": [ + "clientDataJSON": rawClientDataJSON.base64url, + "attestationObject": rawAttestationObject?.base64url ?? "null", + "transports": ASAuthorizationSecurityKeyPublicKeyCredentialDescriptor.Transport.allSupported.map { $0.rawValue }, + ] as [String : Any], + "clientExtensionResults": [:] as [String : Any] + ] as [String : Any] + return response + } + + @available(iOS 15.0, *) + private func generateCredentialAssertionResponse(_ credentialID: Data, _ userID: Data, _ signature: Data, _ rawAuthenticatorData: Data, _ rawClientDataJSON: Data) throws -> [String: Any] { + let response = [ + "id": credentialID.base64url, + "rawId": credentialID.base64url, + "type": "public-key", + "response": [ + "authenticatorData": rawAuthenticatorData.base64url, + "signature": signature.base64url, + "userHandle": userID.base64url, + "clientDataJSON": rawClientDataJSON.base64url, + ] as [String : Any], + "clientExtensionResults": [:] as [String : Any] + ] as [String : Any] + return response + } + + @available(iOS 15.0, *) + private func generateCredentialResponse(_ credential: ASAuthorizationCredential) throws -> [String: Any] { + var response: [String: Any] = [:] + switch (credential) { + case is ASAuthorizationPlatformPublicKeyCredentialRegistration: + let credential = credential as! ASAuthorizationPlatformPublicKeyCredentialRegistration + response = try generateCredentialRegistrationResponse(credential.credentialID, credential.rawClientDataJSON, credential.rawAttestationObject) + case is ASAuthorizationSecurityKeyPublicKeyCredentialRegistration: + let credential = credential as! ASAuthorizationSecurityKeyPublicKeyCredentialRegistration + response = try generateCredentialRegistrationResponse(credential.credentialID, credential.rawClientDataJSON, credential.rawAttestationObject) + case is ASAuthorizationPlatformPublicKeyCredentialAssertion: + let credential = credential as! ASAuthorizationPlatformPublicKeyCredentialAssertion + response = try generateCredentialAssertionResponse(credential.credentialID, credential.userID, credential.signature, credential.rawAuthenticatorData, credential.rawClientDataJSON) + case is ASAuthorizationSecurityKeyPublicKeyCredentialAssertion: + let credential = credential as! ASAuthorizationSecurityKeyPublicKeyCredentialAssertion + response = try generateCredentialAssertionResponse(credential.credentialID, credential.userID, credential.signature, credential.rawAuthenticatorData, credential.rawClientDataJSON) + default: + throw PluginError.unknownCredentialType(credential) + } + return response + } + + @available(iOS 15.0, *) + private func requestCredentialRegistration(_ options: PublicKeyCredentialRequestOptions) throws -> String { + var authorizationRequests: [ASAuthorizationRequest] = [] + switch options.authenticatorAttachment { + case .platform: + authorizationRequests.append(try createPlatformPublicKeyCredentialRegistrationRequest(options)) + case .crossPlatform: + authorizationRequests.append(try createSecurityKeyPublicKeyCredentialRegistrationRequest(options)) + default: + authorizationRequests.append(try createPlatformPublicKeyCredentialRegistrationRequest(options)) + authorizationRequests.append(try createSecurityKeyPublicKeyCredentialRegistrationRequest(options)) + } + let credential = try requestCredential(authorizationRequests) + let response = try generateCredentialResponse(credential) + return try response.jsonString + } + + @available(iOS 15.0, *) + private func requestCredentialAssertion(_ options: PublicKeyCredentialRequestOptions) throws -> String { + var authorizationRequests: [ASAuthorizationRequest] = [] + switch options.authenticatorAttachment { + case .platform: + authorizationRequests.append(try createPlatformPublicKeyCredentialAssertionRequest(options)) + case .crossPlatform: + authorizationRequests.append(try createSecurityKeyPublicKeyCredentialAssertionRequest(options)) + default: + authorizationRequests.append(try createPlatformPublicKeyCredentialAssertionRequest(options)) + /* ISSUE: + * iOS(16.4.1) will show QR code for Passkeys if adding both PlatformPublicKeyCredentialAssertionRequest and + * SecurityKeyPublicKeyCredentialAssertionRequest into authorizationRequests and allowCredentials is not empty. + * It seems that allowCredentials cannot be found in the platform credeintials, but it will work if removing + * SecurityKeyPublicKeyCredentialAssertionRequest from authorizationRequests, that means allowCredentials can be found. + */ + // WORKAROUND: Adding SecurityKeyPublicKeyCredentialAssertionRequest if there is no any platform credeintial can be found in allowCredentials. But the saved platform credentials are not all on the device, due to there is no way can get all platform credentials from iOS. + if findPlatformCredentials(options.allowCredentials).count == 0 { + authorizationRequests.append(try createSecurityKeyPublicKeyCredentialAssertionRequest(options)) + } + } + let credential = try requestCredential(authorizationRequests) + let response = try generateCredentialResponse(credential) + return try response.jsonString + } + + @available(iOS 15.0, *) + public func createCredential(_ options: String, _ callback: @escaping (_ credential: String?, _ error: Error?) -> Void) { + Task { + do { + let credential = try requestCredentialRegistration(PublicKeyCredentialRequestOptions(options)) + callback(credential, nil) + } catch { + callback(nil, error) + } + } + } + + @available(iOS 15.0, *) + public func getCredential(_ options: String, _ callback: @escaping (_ credential: String?, _ error: Error?) -> Void) { + Task { + do { + let credential = try requestCredentialAssertion(PublicKeyCredentialRequestOptions(options)) + callback(credential, nil) + } catch { + callback(nil, error) + } + } + } +} + +@available(iOS 15.0, *) +extension DerivPasskeysManager.PluginError: CustomStringConvertible { + var description: String { + switch self { + case .unknownError: + return "Unknown error." + case .notFound(let name): + return "\(name) not found." + case .unknownCredentialType(let object): + return "Unknown credential type \(String(describing: type(of: object)))." + case .excludedCredentialExists: + return "One of the excluded credentials exists on the local device." + } + } +} + +@available(iOS 15.0, *) +extension DerivPasskeysManager.PluginError: LocalizedError { + private var errorDescription: String { + return self.description + } +} + +@available(iOS 15.0, *) +extension FlutterViewController: ASAuthorizationControllerPresentationContextProviding { + public func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { + return self.view.window! + } +} + +@available(iOS 15.0, *) +extension Data { + var base64url: String { + self.base64EncodedString().replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "=", with: "") + } +} + +@available(iOS 15.0, *) +extension Dictionary where Key == String { + var jsonString: String { + get throws { + let data = try JSONSerialization.data(withJSONObject: self) + guard let jsonString = String(data: data, encoding: .utf8) else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Invalid JSON data.", underlyingError: nil)) + } + return jsonString + } + } +} diff --git a/packages/deriv_passkeys/ios/Classes/DerivPasskeysPlugin.swift b/packages/deriv_passkeys/ios/Classes/DerivPasskeysPlugin.swift new file mode 100644 index 000000000..73b1c5af4 --- /dev/null +++ b/packages/deriv_passkeys/ios/Classes/DerivPasskeysPlugin.swift @@ -0,0 +1,72 @@ +import Flutter +import UIKit + +public class DerivPasskeysPlugin: NSObject, FlutterPlugin { + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel( + name: "deriv_passkeys", binaryMessenger: registrar.messenger()) + let instance = DerivPasskeysPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + + switch call.method { + case "isPlatformSupported": + if #available(iOS 15.0, *) { + result(true) + } else { + result(false) + } + case "createCredential": + if #available(iOS 15.0, *) { + let derivPasskeysManager = DerivPasskeysManager() + if let args = call.arguments as? [String: Any], let options = args["options"] as? String { + derivPasskeysManager.createCredential(options) { credential, error in + if let err = error { + let errorCode = (err as NSError).code + result(FlutterError(code: String(errorCode), message: "\(err)", details: nil)) + return + } + result(credential!) + } + } else { + result( + FlutterError(code: "CreateCredentialError", message: "Options not found", details: nil)) + } + } else { + result( + FlutterError( + code: "UNAVAILABLE", + message: "Passkey creation not available on this iOS version", + details: nil)) + } + case "getCredential": + if #available(iOS 15.0, *) { + let derivPasskeysManager = DerivPasskeysManager() + if let args = call.arguments as? [String: Any], let options = args["options"] as? String { + derivPasskeysManager.getCredential(options) { credential, error in + if let err = error { + let errorCode = (err as NSError).code + result(FlutterError(code: String(errorCode), message: "\(err)", details: nil)) + return + } + result(credential!) + } + } else { + result( + FlutterError(code: "GetCredentialError", message: "Options not found", details: nil)) + } + } else { + result( + FlutterError( + code: "UNAVAILABLE", + message: "Passkey retrieval not available on this iOS version", + details: nil)) + } + default: + result(FlutterMethodNotImplemented) + } + } +} diff --git a/packages/deriv_passkeys/ios/Classes/PublicKeyCredentialRequestOptions.swift b/packages/deriv_passkeys/ios/Classes/PublicKeyCredentialRequestOptions.swift new file mode 100644 index 000000000..5cc387906 --- /dev/null +++ b/packages/deriv_passkeys/ios/Classes/PublicKeyCredentialRequestOptions.swift @@ -0,0 +1,215 @@ +import AuthenticationServices + +@available(iOS 15.0, *) +public class PublicKeyCredentialRequestOptions: NSObject { + + enum OptionError: Error { + case notFound(String) + case invalidValue(String) + } + + enum AuthenticatorAttachment: String { + case platform = "platform" + case crossPlatform = "cross-platform" + } + + let options: [String: Any] + + init(_ jsonObject: [String: Any]) { + self.options = jsonObject + } + + convenience init (_ jsonString: String) throws { + self.init(try jsonString.jsonObject) + } + + var challenge: Data { + get throws { + guard let challengeStr = self.options["challenge"] as? String, let challenge = challengeStr.base64urlDecoded else { + throw OptionError.notFound("challenge") + } + return challenge + } + } + + var rpId: String { + get throws { + var rpId = "" + if let rpObj = self.options["rp"] as? [String: Any], let id = rpObj["id"] as? String { rpId = id } + if let id = self.options["rpId"] as? String { rpId = id } + if rpId.isEmpty { + throw OptionError.notFound("rpId") + } + return rpId + } + } + + var userName: String { + get throws { + guard let userObj = self.options["user"] as? [String: Any], let userName = userObj["name"] as? String else { + throw OptionError.notFound("userName") + } + return userName + } + } + + var userId: Data { + get throws { + guard let userObj = self.options["user"] as? [String: Any], let id = userObj["id"] as? String, let userId = id.data(using: .utf8) else { + throw OptionError.notFound("userId") + } + return userId + } + } + + var pubKeyCredParams: [ASAuthorizationPublicKeyCredentialParameters] { + var _pubKeyCredParams: [ASAuthorizationPublicKeyCredentialParameters] = [] + if let pubKeyCredParamsObj = self.options["pubKeyCredParams"] as? [[String: Any]] { + for paramObj in pubKeyCredParamsObj { + if let alg = paramObj["alg"] as? Int { + _pubKeyCredParams.append(ASAuthorizationPublicKeyCredentialParameters(algorithm: ASCOSEAlgorithmIdentifier(alg))) + } + } + } + return _pubKeyCredParams + } + + var authenticatorAttachment: AuthenticatorAttachment? { + guard let authenticatorSelectionObj = self.options["authenticatorSelection"] as? [String: Any] else { return nil } + guard let _authenticatorAttachment = authenticatorSelectionObj["authenticatorAttachment"] as? String else { return nil } + switch _authenticatorAttachment { + case AuthenticatorAttachment.platform.rawValue: + return AuthenticatorAttachment.platform + case AuthenticatorAttachment.crossPlatform.rawValue: + return AuthenticatorAttachment.crossPlatform + default: + return nil + } + } + + var attestation: ASAuthorizationPublicKeyCredentialAttestationKind? { + switch self.options["attestation"] as? String { + case "none": + return ASAuthorizationPublicKeyCredentialAttestationKind.none + case "indirect": + return ASAuthorizationPublicKeyCredentialAttestationKind.indirect + case "direct": + return ASAuthorizationPublicKeyCredentialAttestationKind.direct + case "enterprise": + return ASAuthorizationPublicKeyCredentialAttestationKind.enterprise + default: + return nil + } + } + + var userVerification: ASAuthorizationPublicKeyCredentialUserVerificationPreference? { + var _userVerification = "" + if let authenticatorSelectionObj = self.options["authenticatorSelection"] as? [String: Any], let uv = authenticatorSelectionObj["userVerification"] as? String { + _userVerification = uv + } + else if let uv = self.options["authenticatorSelection"] as? String { + _userVerification = uv + } + switch _userVerification { + case "discouraged": + return ASAuthorizationPublicKeyCredentialUserVerificationPreference.discouraged + case "preferred": + return ASAuthorizationPublicKeyCredentialUserVerificationPreference.preferred + case "required": + return ASAuthorizationPublicKeyCredentialUserVerificationPreference.required + default: + return nil + } + } + + var residentKey: ASAuthorizationPublicKeyCredentialResidentKeyPreference? { + var _residentKey = "" + if let authenticatorSelectionObj = self.options["authenticatorSelection"] as? [String: Any] { + if let rk = authenticatorSelectionObj["residentKey"] as? String { + _residentKey = rk + } + else if (authenticatorSelectionObj["requireResidentKey"] as? Bool) == true { + _residentKey = "required" + } + } + switch _residentKey { + case "discouraged": + return ASAuthorizationPublicKeyCredentialResidentKeyPreference.discouraged + case "preferred": + return ASAuthorizationPublicKeyCredentialResidentKeyPreference.preferred + case "required": + return ASAuthorizationPublicKeyCredentialResidentKeyPreference.required + default: + return nil + } + } + + var excludeCredentials: [Data] { + get { + var _excludeCredentials: [Data] = [] + if let excludeCredentialsObj = self.options["excludeCredentials"] as? [[String: Any]] { + for credentialObj in excludeCredentialsObj { + if let id = credentialObj["id"] as? String, let data = id.base64urlDecoded { + _excludeCredentials.append(data) + } + } + } + return _excludeCredentials + } + } + + var allowCredentials: [Data] { + get { + var _allowCredentials: [Data] = [] + if let allowCredentialsObj = self.options["allowCredentials"] as? [[String: Any]] { + for credentialObj in allowCredentialsObj { + if let id = credentialObj["id"] as? String, let data = id.base64urlDecoded { + _allowCredentials.append(data) + } + } + } + return _allowCredentials + } + } +} + +@available(iOS 15.0, *) +extension PublicKeyCredentialRequestOptions.OptionError: CustomStringConvertible { + var description: String { + switch self { + case .notFound(let name): + return "\(name) not found." + case .invalidValue(let value): + return "Invalid value '\(value)'." + } + } +} + +@available(iOS 15.0, *) +extension PublicKeyCredentialRequestOptions.OptionError: LocalizedError { + private var errorDescription: String { + return self.description + } +} + +@available(iOS 15.0, *) +extension String { + var base64urlDecoded: Data? { + var base64 = self.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/") + if base64.count % 4 != 0 { base64.append(String(repeating: "=", count: 4 - base64.count % 4)) } + return Data(base64Encoded: base64) + } + + var jsonObject: [String: Any] { + get throws { + guard let data = self.data(using: .utf8) else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Invalid JSON string.", underlyingError: nil)) + } + let object = try JSONSerialization.jsonObject(with: data) + guard let jsonObject = object as? [String: Any] else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Invalid JSON string.", underlyingError: nil)) + } + return jsonObject + } + } +} diff --git a/packages/deriv_passkeys/ios/deriv_passkeys.podspec b/packages/deriv_passkeys/ios/deriv_passkeys.podspec new file mode 100644 index 000000000..57554f425 --- /dev/null +++ b/packages/deriv_passkeys/ios/deriv_passkeys.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint deriv_passkeys.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'deriv_passkeys' + s.version = '0.0.1' + s.summary = 'A new Flutter project.' + s.description = <<-DESC +A new Flutter project. + DESC + s.homepage = 'https://deriv.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Deriv' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '13.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' +end diff --git a/packages/deriv_passkeys/lib/deriv_passkeys.dart b/packages/deriv_passkeys/lib/deriv_passkeys.dart new file mode 100644 index 000000000..ad3c95f9e --- /dev/null +++ b/packages/deriv_passkeys/lib/deriv_passkeys.dart @@ -0,0 +1,6 @@ +library deriv_passkeys; + +export './src/presentation/presentation.dart'; +export './src/domain/domain.dart'; +export './src/interactor/interactor.dart'; +export './src/data/data.dart'; diff --git a/packages/deriv_passkeys/lib/src/data/data.dart b/packages/deriv_passkeys/lib/src/data/data.dart new file mode 100644 index 000000000..450421bc6 --- /dev/null +++ b/packages/deriv_passkeys/lib/src/data/data.dart @@ -0,0 +1,5 @@ +library deriv_passkeys; + +export './repositories/deriv_passkeys_repository.dart'; +export './data_sources/deriv_passkeys_data_source.dart'; +export './mappers/deriv_passkeys_mapper.dart'; diff --git a/packages/deriv_passkeys/lib/src/data/data_sources/base_deriv_passkeys_data_source.dart b/packages/deriv_passkeys/lib/src/data/data_sources/base_deriv_passkeys_data_source.dart new file mode 100644 index 000000000..b1d63b90e --- /dev/null +++ b/packages/deriv_passkeys/lib/src/data/data_sources/base_deriv_passkeys_data_source.dart @@ -0,0 +1,46 @@ +import 'package:deriv_http_client/deriv_http_client.dart'; +import 'package:deriv_passkeys/src/data/mappers/deriv_passkeys_mapper.dart'; +import 'package:deriv_passkeys/src/data/models/passkeys_connection_info_model.dart'; +import 'package:deriv_passkeys/src/data/models/deriv_passkey_model.dart'; +import 'package:deriv_passkeys/src/data/models/deriv_passkeys_options_model.dart'; +import 'package:deriv_passkeys/src/data/models/deriv_passkeys_register_options_model.dart'; +import 'package:deriv_passkeys/src/data/models/deriv_passkeys_verify_credentials_request_body_model.dart'; +import 'package:deriv_passkeys/src/data/models/deriv_passkeys_verify_credentials_response_model.dart'; +import 'package:flutter_deriv_api/basic_api/generated/passkeys_register_send.dart'; + +/// [BaseDerivPasskeysDataSource] defines a contract for all DerivPasskeys data sources. +/// +/// All DerivPasskeys data sources should extend this class, and implement its abstract methods. +abstract class BaseDerivPasskeysDataSource { + /// Creates a [BaseDerivPasskeysDataSource]. + BaseDerivPasskeysDataSource({required this.mapper, required this.client}); + + /// The mapper used to map the data source to the domain. + final DerivPasskeysMapper mapper; + + /// The http client used to make http requests. + BaseHttpClient client; + + /// Get options for authentication with DerivPasskeys. + Future getOptions({ + required PasskeysConnectionInfoModel passkeysConnectionInfoModel, + }); + + /// Verify the user's DerivPasskeys credentials. + Future verifyCredentials({ + required DerivPasskeysVerifyCredentialsRequest requestBodyModel, + required String jwtToken, + required PasskeysConnectionInfoModel passkeysConnectionInfoModel, + String? userAgent, + }); + + /// Get options for registration with DerivPasskeys. + Future getRegisterOptions(); + + /// Register credentials with DerivPasskeys. + Future registerCredentials( + PasskeysRegisterRequest request); + + /// Get passkeys list. + Future> getPasskeysList(); +} diff --git a/packages/deriv_passkeys/lib/src/data/data_sources/deriv_passkeys_data_source.dart b/packages/deriv_passkeys/lib/src/data/data_sources/deriv_passkeys_data_source.dart new file mode 100644 index 000000000..45d1950a2 --- /dev/null +++ b/packages/deriv_passkeys/lib/src/data/data_sources/deriv_passkeys_data_source.dart @@ -0,0 +1,164 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:deriv_http_client/deriv_http_client.dart'; +import 'package:flutter_deriv_api/api/exceptions/base_api_exception.dart'; +import 'package:flutter_deriv_api/api/response/passkeys_list_response_extended.dart'; +import 'package:flutter_deriv_api/api/response/passkeys_register_options_response_extended.dart'; +import 'package:flutter_deriv_api/api/response/passkeys_register_response_extended.dart'; +import 'package:flutter_deriv_api/basic_api/generated/passkeys_list_receive.dart'; +import 'package:flutter_deriv_api/basic_api/generated/passkeys_list_send.dart'; +import 'package:flutter_deriv_api/basic_api/generated/passkeys_register_options_receive.dart'; +import 'package:flutter_deriv_api/basic_api/generated/passkeys_register_options_send.dart'; +import 'package:flutter_deriv_api/basic_api/generated/passkeys_register_receive.dart'; +import 'package:flutter_deriv_api/basic_api/generated/passkeys_register_send.dart'; + +import 'package:http/http.dart' as http; + +import '../data_sources/base_deriv_passkeys_data_source.dart'; +import '../models/passkeys_connection_info_model.dart'; +import '../models/deriv_passkey_model.dart'; +import '../models/deriv_passkeys_options_model.dart'; +import '../models/deriv_passkeys_register_options_model.dart'; +import '../models/deriv_passkeys_verify_credentials_request_body_model.dart'; +import '../models/deriv_passkeys_verify_credentials_response_model.dart'; +import '../../exceptions/server_exceptions.dart'; + +/// [DerivPasskeysDataSource] extends and implements [BaseDerivPasskeysDataSource]. +class DerivPasskeysDataSource extends BaseDerivPasskeysDataSource { + /// Creates a [DerivPasskeysDataSource]. + DerivPasskeysDataSource({required super.mapper, required super.client}); + + @override + Future getOptions({ + required PasskeysConnectionInfoModel passkeysConnectionInfoModel, + }) async { + final String url = + 'https://${passkeysConnectionInfoModel.endpoint}/oauth2/api/v1/passkeys/login/options'; + final http.Response response = await client.get( + url, + ); + + if (response.statusCode == 200) { + return DerivPasskeysOptionsModel.fromJson(jsonDecode(response.body)); + } else { + final Map jsonDecodedResponse = + jsonDecode(response.body); + if (jsonDecodedResponse.containsKey('error_code')) { + throw ServerException( + errorCode: jsonDecodedResponse['error_code'], + message: jsonDecodedResponse['message'], + ); + } + throw Exception('Failed to load options!'); + } + } + + @override + Future verifyCredentials({ + required DerivPasskeysVerifyCredentialsRequest requestBodyModel, + required String jwtToken, + required PasskeysConnectionInfoModel passkeysConnectionInfoModel, + String? userAgent, + }) async { + try { + final String url = + 'https://${passkeysConnectionInfoModel.endpoint}/oauth2/api/v1/passkeys/login/verify'; + + final Map headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $jwtToken', + 'User-Agent': userAgent ?? 'Dart/3.0 (dart:io)', + 'accept': 'application/json' + }; + final Map jsonDecodedResponse = await client.post( + url: url, + headers: headers, + jsonBody: requestBodyModel.toJson(), + ); + + if (jsonDecodedResponse.containsKey('error_code')) { + throw ServerException( + errorCode: jsonDecodedResponse['error_code'], + message: jsonDecodedResponse['message'], + ); + } + + return DerivPasskeysVerifyCredentialsResponseModel( + response: jsonDecodedResponse, + ); + } on HTTPClientException catch (e) { + throw ServerException( + errorCode: e.errorCode ?? '', + message: e.message, + ); + } + } + + @override + Future getRegisterOptions() async { + try { + final PasskeysRegisterOptionsReceive response = + await PasskeysRegisterOptionsResponseExtended.fetchRaw( + const PasskeysRegisterOptionsRequest()); + + if (response.passkeysRegisterOptions == null) { + throw Exception('Failed to load register options!'); + } + + return DerivPasskeysRegisterOptionsModel( + options: response.passkeysRegisterOptions!['publicKey']); + } on BaseAPIException catch (e) { + throw ServerException( + errorCode: e.code ?? '', + message: e.message ?? '', + ); + } + } + + @override + Future registerCredentials( + PasskeysRegisterRequest request) async { + try { + final PasskeysRegisterReceive response = + await PasskeysRegisterResponseExtended.fetchRaw(request); + + if (response.passkeysRegister == null) { + throw Exception('Failed to register credentials!'); + } + + return DerivPasskeyModel.fromJson( + response.passkeysRegister!['properties']); + } on BaseAPIException catch (e) { + throw ServerException( + errorCode: e.code ?? '', + message: e.message ?? '', + ); + } + } + + @override + Future> getPasskeysList() async { + try { + final PasskeysListReceive response = + await PasskeysListResponseExtended.fetchRaw( + const PasskeysListRequest()); + + if (response.passkeysList == null) { + throw Exception('Failed to load passkeys list!'); + } + + final List passkeys = []; + for (final Map passkey in response.passkeysList!) { + passkeys.add(DerivPasskeyModel.fromJson(passkey)); + } + + return passkeys; + } on BaseAPIException catch (e) { + throw ServerException( + errorCode: e.code ?? '', + message: e.message ?? '', + ); + } + } +} diff --git a/packages/deriv_passkeys/lib/src/data/mappers/deriv_passkeys_mapper.dart b/packages/deriv_passkeys/lib/src/data/mappers/deriv_passkeys_mapper.dart new file mode 100644 index 000000000..9404c3987 --- /dev/null +++ b/packages/deriv_passkeys/lib/src/data/mappers/deriv_passkeys_mapper.dart @@ -0,0 +1,63 @@ +import 'package:deriv_passkeys/src/data/models/passkeys_connection_info_model.dart'; +import 'package:deriv_passkeys/src/data/models/deriv_passkey_model.dart'; +import 'package:deriv_passkeys/src/data/models/deriv_passkeys_options_model.dart'; +import 'package:deriv_passkeys/src/data/models/deriv_passkeys_register_options_model.dart'; +import 'package:deriv_passkeys/src/data/models/deriv_passkeys_verify_credentials_request_body_model.dart'; +import 'package:deriv_passkeys/src/data/models/deriv_passkeys_verify_credentials_response_model.dart'; +import 'package:deriv_passkeys/src/domain/entities/passkeys_connection_info_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkey_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkeys_options_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkeys_register_credentials_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkeys_register_options_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkeys_verify_credentials_request_body_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkeys_verify_credentials_response_entity.dart'; +import 'package:flutter_deriv_api/basic_api/generated/passkeys_register_send.dart'; + +/// [DerivPasskeysMapper] maps [DerivPasskeysOptionsModel] to [DerivPasskeysOptionsEntity]. +class DerivPasskeysMapper { + /// Maps [DerivPasskeysOptionsModel] to [DerivPasskeysOptionsEntity]. + DerivPasskeysOptionsEntity mapDerivPasskeysOptionsModel( + DerivPasskeysOptionsModel model) => + DerivPasskeysOptionsEntity.fromJson(model.toJson()); + + /// Maps [DerivPasskeysVerifyCredentialsRequest] to [DerivPasskeysVerifyCredentialsRequestBodyEntity]. + DerivPasskeysVerifyCredentialsRequest + mapDerivPasskeysVerifyCredentialsRequestBodyEntity( + DerivPasskeysVerifyCredentialsRequestBodyEntity entity) => + DerivPasskeysVerifyCredentialsRequest.fromJson(entity.toJson()); + + /// Maps [DerivPasskeysRegisterOptionsModel] to [DerivPasskeysRegisterOptionsEntity]. + DerivPasskeysRegisterOptionsEntity mapDerivPasskeysRegisterOptionsModel( + DerivPasskeysRegisterOptionsModel model) => + DerivPasskeysRegisterOptionsEntity.fromJson(model.toJson()); + + /// Maps [DerivPasskeyModel] to [DerivPasskeyEntity]. + DerivPasskeyEntity mapDerivPasskeyModel(DerivPasskeyModel model) => + DerivPasskeyEntity.fromJson( + model.toJson(), + ); + + /// Maps [DerivPasskeysRegisterCredentialsEntity] to [PasskeysRegisterRequest]. + PasskeysRegisterRequest mapDerivPasskeysRegisterCredentialsEntity( + DerivPasskeysRegisterCredentialsEntity entity) => + PasskeysRegisterRequest( + name: entity.name, + publicKeyCredential: entity.publicKeyCredential, + ); + + /// Maps [DerivPasskeysVerifyCredentialsResponseModel] to [DerivPasskeysVerifyCredentialsResponseEntity]. + DerivPasskeysVerifyCredentialsResponseEntity + mapDerivPasskeysVerifyCredentialsResponseModel( + DerivPasskeysVerifyCredentialsResponseModel model) => + DerivPasskeysVerifyCredentialsResponseEntity( + token: (model.response['tokens'] as List).first['token'], + ); + + /// Maps [ConnectionInfoEntity] to [PasskeysConnectionInfoModel]. + PasskeysConnectionInfoModel mapConnectionInfoEntity( + PasskeysConnectionInfoEntity entity) => + PasskeysConnectionInfoModel( + endpoint: entity.endpoint, + appId: entity.appId, + ); +} diff --git a/packages/deriv_passkeys/lib/src/data/models/deriv_passkey_model.dart b/packages/deriv_passkeys/lib/src/data/models/deriv_passkey_model.dart new file mode 100644 index 000000000..c0fc61836 --- /dev/null +++ b/packages/deriv_passkeys/lib/src/data/models/deriv_passkey_model.dart @@ -0,0 +1,58 @@ +import 'package:equatable/equatable.dart'; + +/// [DerivPasskeyModel] DTO for DerivPasskey data coming from the WebsocketAPI. +class DerivPasskeyModel extends Equatable { + /// Creates a [DerivPasskeyModel]. + const DerivPasskeyModel({ + required this.createdAt, + required this.id, + required this.lastUsed, + required this.name, + required this.passkeyId, + required this.storedOn, + }); + + /// Creates a [DerivPasskeyModel] from a JSON object. + factory DerivPasskeyModel.fromJson(Map json) => + DerivPasskeyModel( + createdAt: json['created_at'] as int, + id: json['id'].toString(), + lastUsed: + (json['last_used'] as int?) == 0 ? null : json['last_used'] as int?, + name: json['name'] as String, + passkeyId: json['passkey_id'] as String, + storedOn: json['stored_on'] as String, + ); + + /// Creates a JSON object from a [DerivPasskeyModel]. + Map toJson() => { + 'created_at': createdAt, + 'id': id, + 'last_used': lastUsed, + 'name': name, + 'passkey_id': passkeyId, + 'stored_on': storedOn, + }; + + /// [createdAt] is the creation date. + final int createdAt; + + /// [id] is the passkey ID. + final String id; + + /// [lastUsed] is the last used date. + final int? lastUsed; + + /// [name] is the passkey name. + final String name; + + /// [passkeyId] is the passkey ID. + final String passkeyId; + + /// [storedOn] is the device the passkey is stored on. + final String storedOn; + + @override + List get props => + [createdAt, id, lastUsed, name, passkeyId, storedOn]; +} diff --git a/packages/deriv_passkeys/lib/src/data/models/deriv_passkeys_options_model.dart b/packages/deriv_passkeys/lib/src/data/models/deriv_passkeys_options_model.dart new file mode 100644 index 000000000..55994a2ca --- /dev/null +++ b/packages/deriv_passkeys/lib/src/data/models/deriv_passkeys_options_model.dart @@ -0,0 +1,51 @@ +/// [DerivPasskeysOptionsModel] DTO for DerivPasskeysOptions data coming from the WebsocketAPI. +final class DerivPasskeysOptionsModel { + /// Creates a [DerivPasskeysOptionsModel]. + /// [challenge] is the challenge string. + /// [rpId] is the relying party ID. + /// [timeout] is the timeout in seconds. + /// [userVerification] is the user verification method. + DerivPasskeysOptionsModel({ + required this.challenge, + required this.rpId, + required this.timeout, + required this.userVerification, + required this.allowCredentials, + }); + + /// Creates a [DerivPasskeysOptionsModel] from a JSON object. + factory DerivPasskeysOptionsModel.fromJson(Map json) => + DerivPasskeysOptionsModel( + challenge: json['publicKey']['challenge'] as String, + rpId: json['publicKey']['rpId'] as String, + timeout: json['publicKey']['timeout'] as int, + userVerification: json['publicKey']['userVerification'] as String, + allowCredentials: + json['publicKey']['allowCredentials'] as List? ?? + [], + ); + + /// [challenge] is the challenge string. + final String challenge; + + /// [rpId] is the relying party ID. + final String rpId; + + /// [timeout] is the timeout in seconds. + final int timeout; + + /// [userVerification] is the user verification method. + final String userVerification; + + /// [allowCredentials] is the allowed credentials. + final List allowCredentials; + + /// Creates a JSON object from a [DerivPasskeysOptionsModel]. + Map toJson() => { + 'challenge': challenge, + 'rpId': rpId, + 'timeout': timeout, + 'userVerification': userVerification, + 'allowCredentials': allowCredentials, + }; +} diff --git a/packages/deriv_passkeys/lib/src/data/models/deriv_passkeys_register_options_model.dart b/packages/deriv_passkeys/lib/src/data/models/deriv_passkeys_register_options_model.dart new file mode 100644 index 000000000..3e2cf2028 --- /dev/null +++ b/packages/deriv_passkeys/lib/src/data/models/deriv_passkeys_register_options_model.dart @@ -0,0 +1,18 @@ +/// [DerivPasskeysRegisterOptionsModel] DTO for DerivPasskeysOptions data coming from the WebsocketAPI. +final class DerivPasskeysRegisterOptionsModel { + /// Creates a [DerivPasskeysRegisterOptionsModel]. + DerivPasskeysRegisterOptionsModel({ + required this.options, + }); + + /// Creates a [DerivPasskeysRegisterOptionsModel] from a JSON object. + factory DerivPasskeysRegisterOptionsModel.fromJson( + Map json) => + DerivPasskeysRegisterOptionsModel(options: json); + + /// [options] is the challenge string. + Map options; + + /// Creates a JSON object from a [DerivPasskeysRegisterOptionsModel]. + Map toJson() => options; +} diff --git a/packages/deriv_passkeys/lib/src/data/models/deriv_passkeys_verify_credentials_request_body_model.dart b/packages/deriv_passkeys/lib/src/data/models/deriv_passkeys_verify_credentials_request_body_model.dart new file mode 100644 index 000000000..f5cdaa133 --- /dev/null +++ b/packages/deriv_passkeys/lib/src/data/models/deriv_passkeys_verify_credentials_request_body_model.dart @@ -0,0 +1,48 @@ +/// A dart class that serves as a request body for the `DerivPasskeysVerifyCredentials` request. +final class DerivPasskeysVerifyCredentialsRequest { + /// Creates a [DerivPasskeysVerifyCredentialsRequest]. + DerivPasskeysVerifyCredentialsRequest({ + required this.appId, + required this.publicKeyCredential, + required this.type, + }); + + /// Creates a [DerivPasskeysVerifyCredentialsRequest] from a JSON object. + factory DerivPasskeysVerifyCredentialsRequest.fromJson( + Map json, + ) => + DerivPasskeysVerifyCredentialsRequest( + appId: json['app_id'] as String, + publicKeyCredential: + json['publicKeyCredential'] as Map, + type: json['type'] as String, + ); + + /// The app ID. + final String appId; + + /// The public key credential returned by the authenticator. + final Map publicKeyCredential; + + /// The type. + final String type; + + /// Converts the [DerivPasskeysVerifyCredentialsRequest] to a JSON object. + Map toJson() => { + 'app_id': appId, + 'publicKeyCredential': publicKeyCredential, + 'type': type, + }; + + /// Copies the [DerivPasskeysVerifyCredentialsRequest] with some new values. + DerivPasskeysVerifyCredentialsRequest copyWith({ + String? appId, + Map? publicKeyCredential, + String? type, + }) => + DerivPasskeysVerifyCredentialsRequest( + appId: appId ?? this.appId, + publicKeyCredential: publicKeyCredential ?? this.publicKeyCredential, + type: type ?? this.type, + ); +} diff --git a/packages/deriv_passkeys/lib/src/data/models/deriv_passkeys_verify_credentials_response_model.dart b/packages/deriv_passkeys/lib/src/data/models/deriv_passkeys_verify_credentials_response_model.dart new file mode 100644 index 000000000..3b6c29e2e --- /dev/null +++ b/packages/deriv_passkeys/lib/src/data/models/deriv_passkeys_verify_credentials_response_model.dart @@ -0,0 +1,15 @@ +import 'package:equatable/equatable.dart'; + +/// A Dart class that contains the response to verify credentials. +final class DerivPasskeysVerifyCredentialsResponseModel extends Equatable { + /// Creates a [DerivPasskeysVerifyCredentialsResponseModel]. + const DerivPasskeysVerifyCredentialsResponseModel({ + required this.response, + }); + + /// contains the response from the REST API to verify credentials. + final Map response; + + @override + List get props => [response]; +} diff --git a/packages/deriv_passkeys/lib/src/data/models/passkeys_connection_info_model.dart b/packages/deriv_passkeys/lib/src/data/models/passkeys_connection_info_model.dart new file mode 100644 index 000000000..238548ff0 --- /dev/null +++ b/packages/deriv_passkeys/lib/src/data/models/passkeys_connection_info_model.dart @@ -0,0 +1,24 @@ +/// Model to store connection info. +class PasskeysConnectionInfoModel { + /// Creates a new [PasskeysConnectionInfoModel] instance. + PasskeysConnectionInfoModel({ + required this.endpoint, + required this.appId, + }); + + /// Connection endpoint. + String endpoint; + + /// Deriv client app ID. + String appId; + + /// Copies the current instance with some new values. + PasskeysConnectionInfoModel copyWith({ + String? endpoint, + String? appId, + }) => + PasskeysConnectionInfoModel( + endpoint: endpoint ?? this.endpoint, + appId: appId ?? this.appId, + ); +} diff --git a/packages/deriv_passkeys/lib/src/data/platform/deriv_passkeys_method_channel.dart b/packages/deriv_passkeys/lib/src/data/platform/deriv_passkeys_method_channel.dart new file mode 100644 index 000000000..3742d9b9d --- /dev/null +++ b/packages/deriv_passkeys/lib/src/data/platform/deriv_passkeys_method_channel.dart @@ -0,0 +1,71 @@ +import 'package:deriv_passkeys/src/exceptions/platform_exceptions.dart'; +import 'package:deriv_passkeys/src/domain/platform/base_deriv_passkeys_method_channel.dart'; +import 'package:flutter/services.dart'; + +/// An implementation of [BaseDerivPasskeysMethodChannel] that uses method channels. +class MethodChannelDerivPasskeys extends BaseDerivPasskeysMethodChannel { + /// Constructs a [MethodChannelDerivPasskeys] with an optional [MethodChannel]; + MethodChannelDerivPasskeys({ + MethodChannel? channel, + }) : methodChannel = channel ?? const MethodChannel('deriv_passkeys'); + + /// The method channel used to interact with the native platform. + MethodChannel methodChannel; + + @override + Future isPlatformSupported() async => + methodChannel.invokeMethod('isPlatformSupported'); + + @override + Future createCredential(String options) => + methodChannel.invokeMethod( + 'createCredential', {'options': options}).catchError( + (Object error) { + if (error is PlatformException) { + if (error.code == '1001' || + error.code == 'CreateCredentialCancellationException') { + throw CanceledPlatformException( + code: error.code, + message: error.message ?? 'Error creating passkey', + details: error.details ?? error.toString(), + ); + } + } + throw PlatformException( + code: 'unhandled_error', + message: 'Unhandled error creating passkey', + details: error.toString(), + ); + }, + ); + + @override + Future getCredential(String options) => + methodChannel.invokeMethod( + 'getCredential', {'options': options}).catchError( + (Object error) { + if (error is PlatformException) { + if (error.code == '1001' || + error.code == 'GetCredentialCancellationException') { + throw CanceledPlatformException( + code: error.code, + message: error.message ?? 'Error creating passkey', + details: error.details ?? error.toString(), + ); + } + if (error.code == 'NoCredentialException') { + throw NoCredentialPlatformException( + code: error.code, + message: error.message ?? 'Error creating passkey', + details: error.details ?? error.toString(), + ); + } + } + throw PlatformException( + code: 'unhandled_error', + message: 'Unhandled error creating passkey', + details: error.toString(), + ); + }, + ); +} diff --git a/packages/deriv_passkeys/lib/src/data/repositories/deriv_passkeys_repository.dart b/packages/deriv_passkeys/lib/src/data/repositories/deriv_passkeys_repository.dart new file mode 100644 index 000000000..0e0a472a1 --- /dev/null +++ b/packages/deriv_passkeys/lib/src/data/repositories/deriv_passkeys_repository.dart @@ -0,0 +1,71 @@ +import 'package:deriv_passkeys/src/data/data_sources/base_deriv_passkeys_data_source.dart'; +import 'package:deriv_passkeys/src/data/models/deriv_passkey_model.dart'; +import 'package:deriv_passkeys/src/domain/base_repositories/base_deriv_passkeys_repository.dart'; +import 'package:deriv_passkeys/src/domain/entities/passkeys_connection_info_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkey_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkeys_options_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkeys_register_credentials_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkeys_register_options_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkeys_verify_credentials_request_body_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkeys_verify_credentials_response_entity.dart'; + +/// [DerivPasskeysRepository] extends and implements [BaseDerivPasskeysRepository]. +/// +/// It is responsible for handling all DerivPasskeys data sources, and forwarding relavant data +/// to the services using it. +final class DerivPasskeysRepository extends BaseDerivPasskeysRepository { + /// Creates a [DerivPasskeysRepository]. + DerivPasskeysRepository(this.dataSource); + + /// The data source used to get data for Passkeys functionalities. + final BaseDerivPasskeysDataSource dataSource; + + @override + Future getOptions({ + required PasskeysConnectionInfoEntity passkeysConnectionInfoEntity, + }) => + dataSource + .getOptions( + passkeysConnectionInfoModel: dataSource.mapper + .mapConnectionInfoEntity(passkeysConnectionInfoEntity), + ) + .then(dataSource.mapper.mapDerivPasskeysOptionsModel); + + @override + Future verifyCredentials({ + required DerivPasskeysVerifyCredentialsRequestBodyEntity requestBodyEntity, + required String jwtToken, + required PasskeysConnectionInfoEntity passkeysConnectionInfoEntity, + String? userAgent, + }) => + dataSource + .verifyCredentials( + requestBodyModel: dataSource.mapper + .mapDerivPasskeysVerifyCredentialsRequestBodyEntity( + requestBodyEntity), + jwtToken: jwtToken, + passkeysConnectionInfoModel: dataSource.mapper + .mapConnectionInfoEntity(passkeysConnectionInfoEntity), + userAgent: userAgent, + ) + .then( + dataSource.mapper.mapDerivPasskeysVerifyCredentialsResponseModel); + + @override + Future getRegisterOptions() => dataSource + .getRegisterOptions() + .then(dataSource.mapper.mapDerivPasskeysRegisterOptionsModel); + + @override + Future registerCredentials( + DerivPasskeysRegisterCredentialsEntity entity) => + dataSource + .registerCredentials(dataSource.mapper + .mapDerivPasskeysRegisterCredentialsEntity(entity)) + .then(dataSource.mapper.mapDerivPasskeyModel); + + @override + Future> getPasskeysList() => + dataSource.getPasskeysList().then((List models) => + models.map(dataSource.mapper.mapDerivPasskeyModel).toList()); +} diff --git a/packages/deriv_passkeys/lib/src/domain/base_repositories/base_deriv_passkeys_repository.dart b/packages/deriv_passkeys/lib/src/domain/base_repositories/base_deriv_passkeys_repository.dart new file mode 100644 index 000000000..63a1f366b --- /dev/null +++ b/packages/deriv_passkeys/lib/src/domain/base_repositories/base_deriv_passkeys_repository.dart @@ -0,0 +1,35 @@ +import 'package:deriv_passkeys/src/domain/entities/passkeys_connection_info_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkey_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkeys_options_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkeys_register_credentials_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkeys_register_options_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkeys_verify_credentials_request_body_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkeys_verify_credentials_response_entity.dart'; + +/// [BaseDerivPasskeysRepository] defines a contract for all DerivPasskeys repositories. +/// +/// All DerivPasskeys repositories should extend this class, and implement its abstract methods. +abstract base class BaseDerivPasskeysRepository { + /// Get options for authentication with DerivPasskeys. + Future getOptions({ + required PasskeysConnectionInfoEntity passkeysConnectionInfoEntity, + }); + + /// Verify the user's DerivPasskeys credentials. + Future verifyCredentials({ + required DerivPasskeysVerifyCredentialsRequestBodyEntity requestBodyEntity, + required String jwtToken, + required PasskeysConnectionInfoEntity passkeysConnectionInfoEntity, + String? userAgent, + }); + + /// Get options for registration with DerivPasskeys. + Future getRegisterOptions(); + + /// Register credentials with DerivPasskeys. + Future registerCredentials( + DerivPasskeysRegisterCredentialsEntity entity); + + /// Get passkeys list. + Future> getPasskeysList(); +} diff --git a/packages/deriv_passkeys/lib/src/domain/domain.dart b/packages/deriv_passkeys/lib/src/domain/domain.dart new file mode 100644 index 000000000..9fbf5dc74 --- /dev/null +++ b/packages/deriv_passkeys/lib/src/domain/domain.dart @@ -0,0 +1,5 @@ +library deriv_passkeys; + +export './entities/passkeys_connection_info_entity.dart'; +export './entities/deriv_passkeys_verify_credentials_response_entity.dart'; +export './entities/deriv_passkey_entity.dart'; diff --git a/packages/deriv_passkeys/lib/src/domain/entities/deriv_passkey_entity.dart b/packages/deriv_passkeys/lib/src/domain/entities/deriv_passkey_entity.dart new file mode 100644 index 000000000..e2f1f29a1 --- /dev/null +++ b/packages/deriv_passkeys/lib/src/domain/entities/deriv_passkey_entity.dart @@ -0,0 +1,53 @@ +import 'package:equatable/equatable.dart'; + +/// [DerivPasskeyEntity] DTO for DerivPasskey data coming from the WebsocketAPI. +class DerivPasskeyEntity extends Equatable { + /// Creates a [DerivPasskeyEntity]. + const DerivPasskeyEntity({ + required this.createdAt, + required this.id, + required this.lastUsed, + required this.name, + required this.passkeyId, + required this.storedOn, + }); + + /// Creates a [DerivPasskeyEntity] from a JSON object. + factory DerivPasskeyEntity.fromJson(Map json) => + DerivPasskeyEntity( + createdAt: json['created_at'] as int, + id: json['id'] as String, + lastUsed: json['last_used'] as int?, + name: json['name'] as String, + passkeyId: json['passkey_id'] as String, + storedOn: json['stored_on'] as String, + ); + + /// [createdAt] is the creation date. + final int createdAt; + + /// [id] is the passkey ID. + final String id; + + /// [lastUsed] is the last used date. + final int? lastUsed; + + /// [name] is the passkey name. + final String name; + + /// [passkeyId] is the passkey ID. + final String passkeyId; + + /// [storedOn] is the device the passkey is stored on. + final String storedOn; + + @override + List get props => [ + createdAt, + id, + lastUsed, + name, + passkeyId, + storedOn, + ]; +} diff --git a/packages/deriv_passkeys/lib/src/domain/entities/deriv_passkeys_options_entity.dart b/packages/deriv_passkeys/lib/src/domain/entities/deriv_passkeys_options_entity.dart new file mode 100644 index 000000000..6532faf99 --- /dev/null +++ b/packages/deriv_passkeys/lib/src/domain/entities/deriv_passkeys_options_entity.dart @@ -0,0 +1,60 @@ +import 'package:equatable/equatable.dart'; + +/// [DerivPasskeysOptionsEntity] represents a DerivPasskeysOptions entity. +final class DerivPasskeysOptionsEntity extends Equatable { + /// Creates a [DerivPasskeysOptionsEntity]. + /// [challenge] is the challenge string. + /// [rpId] is the relying party ID. + /// [timeout] is the timeout in seconds. + /// [userVerification] is the user verification method. + const DerivPasskeysOptionsEntity({ + required this.challenge, + required this.rpId, + required this.timeout, + required this.userVerification, + required this.allowCredentials, + }); + + /// Creates a [DerivPasskeysOptionsEntity] from a JSON object. + factory DerivPasskeysOptionsEntity.fromJson(Map json) => + DerivPasskeysOptionsEntity( + challenge: json['challenge'] as String, + rpId: json['rpId'] as String, + timeout: json['timeout'] as int, + userVerification: json['userVerification'] as String, + allowCredentials: json['allowCredentials'] as List, + ); + + /// [challenge] is the challenge string. + final String challenge; + + /// [rpId] is the relying party ID. + final String rpId; + + /// [timeout] is the timeout in seconds. + final int timeout; + + /// [userVerification] is the user verification method. + final String userVerification; + + /// [allowCredentials] is the allowed credentials. + final List allowCredentials; + + /// Creates a JSON object from a [DerivPasskeysOptionsEntity]. + Map toJson() => { + 'challenge': challenge, + 'rpId': rpId, + 'timeout': timeout, + 'userVerification': userVerification, + 'allowCredentials': allowCredentials, + }; + + @override + List get props => [ + challenge, + rpId, + timeout, + userVerification, + allowCredentials, + ]; +} diff --git a/packages/deriv_passkeys/lib/src/domain/entities/deriv_passkeys_register_credentials_entity.dart b/packages/deriv_passkeys/lib/src/domain/entities/deriv_passkeys_register_credentials_entity.dart new file mode 100644 index 000000000..5a95b4c9f --- /dev/null +++ b/packages/deriv_passkeys/lib/src/domain/entities/deriv_passkeys_register_credentials_entity.dart @@ -0,0 +1,25 @@ +import 'package:equatable/equatable.dart'; + +/// [DerivPasskeysRegisterCredentialsEntity] is the entity class for the register credentials. +final class DerivPasskeysRegisterCredentialsEntity extends Equatable { + /// Creates a [DerivPasskeysRegisterCredentialsEntity]. + const DerivPasskeysRegisterCredentialsEntity({ + required this.publicKeyCredential, + required this.name, + }); + + /// [publicKeyCredential] is the challenge string. + final Map publicKeyCredential; + + /// [name] is the name of the passkey. + final String name; + + /// Creates a JSON object from a [DerivPasskeysRegisterCredentialsEntity]. + Map toJson() => { + 'publicKeyCredential': publicKeyCredential, + 'name': name, + }; + + @override + List get props => [publicKeyCredential, name]; +} diff --git a/packages/deriv_passkeys/lib/src/domain/entities/deriv_passkeys_register_options_entity.dart b/packages/deriv_passkeys/lib/src/domain/entities/deriv_passkeys_register_options_entity.dart new file mode 100644 index 000000000..00b7fccb6 --- /dev/null +++ b/packages/deriv_passkeys/lib/src/domain/entities/deriv_passkeys_register_options_entity.dart @@ -0,0 +1,23 @@ +import 'package:equatable/equatable.dart'; + +/// [DerivPasskeysRegisterOptionsEntity] DTO for DerivPasskeysOptions data coming from the WebsocketAPI. +final class DerivPasskeysRegisterOptionsEntity extends Equatable { + /// Creates a [DerivPasskeysRegisterOptionsEntity]. + const DerivPasskeysRegisterOptionsEntity({ + required this.options, + }); + + /// Creates a [DerivPasskeysRegisterOptionsEntity] from a JSON object. + factory DerivPasskeysRegisterOptionsEntity.fromJson( + Map json) => + DerivPasskeysRegisterOptionsEntity(options: json); + + /// [options] is the challenge string. + final Map options; + + /// Creates a JSON object from a [DerivPasskeysRegisterOptionsEntity]. + Map toJson() => options; + + @override + List get props => [options]; +} diff --git a/packages/deriv_passkeys/lib/src/domain/entities/deriv_passkeys_verify_credentials_request_body_entity.dart b/packages/deriv_passkeys/lib/src/domain/entities/deriv_passkeys_verify_credentials_request_body_entity.dart new file mode 100644 index 000000000..4e6b06be8 --- /dev/null +++ b/packages/deriv_passkeys/lib/src/domain/entities/deriv_passkeys_verify_credentials_request_body_entity.dart @@ -0,0 +1,40 @@ +import 'package:equatable/equatable.dart'; + +/// A dart class that serves as a request body for the `DerivPasskeysVerifyCredentials` request. +final class DerivPasskeysVerifyCredentialsRequestBodyEntity extends Equatable { + /// Creates a [DerivPasskeysVerifyCredentialsRequestBodyEntity]. + const DerivPasskeysVerifyCredentialsRequestBodyEntity({ + required this.appId, + required this.publicKeyCredential, + required this.type, + }); + + /// Creates a [DerivPasskeysVerifyCredentialsRequestBodyEntity] from a JSON object. + factory DerivPasskeysVerifyCredentialsRequestBodyEntity.fromJson( + Map json) => + DerivPasskeysVerifyCredentialsRequestBodyEntity( + appId: json['app_id'] as String, + publicKeyCredential: + json['publicKeyCredential'] as Map, + type: json['type'] as String, + ); + + /// The app ID. + final String appId; + + /// The public key credential returned by the authenticator. + final Map publicKeyCredential; + + /// The type. + final String type; + + /// Converts the [DerivPasskeysVerifyCredentialsRequestBodyEntity] to a JSON object. + Map toJson() => { + 'app_id': appId, + 'publicKeyCredential': publicKeyCredential, + 'type': type, + }; + + @override + List get props => [appId, publicKeyCredential, type]; +} diff --git a/packages/deriv_passkeys/lib/src/domain/entities/deriv_passkeys_verify_credentials_response_entity.dart b/packages/deriv_passkeys/lib/src/domain/entities/deriv_passkeys_verify_credentials_response_entity.dart new file mode 100644 index 000000000..1e249d40a --- /dev/null +++ b/packages/deriv_passkeys/lib/src/domain/entities/deriv_passkeys_verify_credentials_response_entity.dart @@ -0,0 +1,15 @@ +import 'package:equatable/equatable.dart'; + +/// A Dart class that contains the response to verify credentials. +final class DerivPasskeysVerifyCredentialsResponseEntity extends Equatable { + /// Creates a [DerivPasskeysVerifyCredentialsResponseEntity]. + const DerivPasskeysVerifyCredentialsResponseEntity({ + required this.token, + }); + + /// contains the token from the REST API to verify credentials. + final String token; + + @override + List get props => [token]; +} diff --git a/packages/deriv_passkeys/lib/src/domain/entities/passkeys_connection_info_entity.dart b/packages/deriv_passkeys/lib/src/domain/entities/passkeys_connection_info_entity.dart new file mode 100644 index 000000000..213d42943 --- /dev/null +++ b/packages/deriv_passkeys/lib/src/domain/entities/passkeys_connection_info_entity.dart @@ -0,0 +1,14 @@ +/// Entity to store connection info. +class PasskeysConnectionInfoEntity { + /// Creates a new [PasskeysConnectionInfoEntity] instance. + PasskeysConnectionInfoEntity({ + required this.endpoint, + required this.appId, + }); + + /// Connection endpoint. + String endpoint; + + /// Deriv client app ID. + String appId; +} diff --git a/packages/deriv_passkeys/lib/src/domain/platform/base_deriv_passkeys_method_channel.dart b/packages/deriv_passkeys/lib/src/domain/platform/base_deriv_passkeys_method_channel.dart new file mode 100644 index 000000000..c145e6540 --- /dev/null +++ b/packages/deriv_passkeys/lib/src/domain/platform/base_deriv_passkeys_method_channel.dart @@ -0,0 +1,35 @@ +import 'package:deriv_passkeys/src/data/platform/deriv_passkeys_method_channel.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +/// The interface that implementations of deriv_passkeys must implement. +abstract class BaseDerivPasskeysMethodChannel extends PlatformInterface { + /// Constructs a BaseDerivPasskeysMethodChannel. + BaseDerivPasskeysMethodChannel() : super(token: _token); + + static final Object _token = Object(); + + static BaseDerivPasskeysMethodChannel _instance = + MethodChannelDerivPasskeys(); + + /// The default instance of [BaseDerivPasskeysMethodChannel] to use. + /// + /// Defaults to [MethodChannelDerivPasskeys]. + static BaseDerivPasskeysMethodChannel get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [BaseDerivPasskeysMethodChannel] when + /// they register themselves. + static set instance(BaseDerivPasskeysMethodChannel instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + /// Returns the platform version. + Future isPlatformSupported(); + + /// Creates a passkey credential. + Future createCredential(String options); + + /// Gets a passkey credential. + Future getCredential(String options); +} diff --git a/packages/deriv_passkeys/lib/src/exceptions/platform_exceptions.dart b/packages/deriv_passkeys/lib/src/exceptions/platform_exceptions.dart new file mode 100644 index 000000000..6db77cf7c --- /dev/null +++ b/packages/deriv_passkeys/lib/src/exceptions/platform_exceptions.dart @@ -0,0 +1,29 @@ +import 'package:flutter/services.dart'; + +/// An exception that is thrown when the platform fails to create a passkey. +class CanceledPlatformException extends PlatformException { + /// Creates a [CanceledPlatformException]. + CanceledPlatformException({ + required String code, + required String message, + required String details, + }) : super( + code: code, + message: message, + details: details, + ); +} + +/// An exception that is thrown when the platform cannot find a matching credential. +class NoCredentialPlatformException extends PlatformException { + /// Creates a [NoCredentialPlatformException]. + NoCredentialPlatformException({ + required String code, + required String message, + required String details, + }) : super( + code: code, + message: message, + details: details, + ); +} diff --git a/packages/deriv_passkeys/lib/src/exceptions/server_exceptions.dart b/packages/deriv_passkeys/lib/src/exceptions/server_exceptions.dart new file mode 100644 index 000000000..b85a27380 --- /dev/null +++ b/packages/deriv_passkeys/lib/src/exceptions/server_exceptions.dart @@ -0,0 +1,14 @@ +/// Server exception +class ServerException implements Exception { + /// Creates a [ServerException]. + ServerException({ + required this.errorCode, + required this.message, + }); + + /// The error code. + String errorCode; + + /// The error message. + String message; +} diff --git a/packages/deriv_passkeys/lib/src/extensions/context_extensions.dart b/packages/deriv_passkeys/lib/src/extensions/context_extensions.dart new file mode 100644 index 000000000..767cb4060 --- /dev/null +++ b/packages/deriv_passkeys/lib/src/extensions/context_extensions.dart @@ -0,0 +1,9 @@ +import 'package:deriv_localizations/l10n/generated/deriv_passkeys/deriv_passkeys_localizations.dart'; +import 'package:flutter/widgets.dart'; + +/// Extension for [BuildContext] +extension ContextExtension on BuildContext { + /// Get derivPasskeysLocalizations. + DerivPasskeysLocalizations get derivPasskeysLocalizations => + DerivPasskeysLocalizations.of(this)!; +} diff --git a/packages/deriv_passkeys/lib/src/interactor/interactor.dart b/packages/deriv_passkeys/lib/src/interactor/interactor.dart new file mode 100644 index 000000000..c0db4ce94 --- /dev/null +++ b/packages/deriv_passkeys/lib/src/interactor/interactor.dart @@ -0,0 +1,3 @@ +library deriv_passkeys; + +export './services/deriv_passkeys_service.dart'; diff --git a/packages/deriv_passkeys/lib/src/interactor/services/deriv_passkeys_service.dart b/packages/deriv_passkeys/lib/src/interactor/services/deriv_passkeys_service.dart new file mode 100644 index 000000000..ec4809fc6 --- /dev/null +++ b/packages/deriv_passkeys/lib/src/interactor/services/deriv_passkeys_service.dart @@ -0,0 +1,111 @@ +import 'dart:convert'; + +import 'package:deriv_passkeys/src/domain/base_repositories/base_deriv_passkeys_repository.dart'; +import 'package:deriv_passkeys/src/domain/entities/passkeys_connection_info_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkey_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkeys_register_credentials_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkeys_verify_credentials_request_body_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkeys_verify_credentials_response_entity.dart'; +import 'package:deriv_passkeys/src/domain/platform/base_deriv_passkeys_method_channel.dart'; +import 'package:flutter/services.dart'; + +/// A wrapper class that contains methods to interact with the native platform for passkey. +class DerivPasskeysService { + /// Constructs a [DerivPasskeysService] with [BaseDerivPasskeysRepository]. + DerivPasskeysService(this.repository); + + /// The repository used to get data for Passkeys functionalities. + final BaseDerivPasskeysRepository repository; + + /// Returns true if the device supports passkey. + Future isSupported() async { + final bool? isPlatformSupportedResult = + await BaseDerivPasskeysMethodChannel.instance.isPlatformSupported(); + return isPlatformSupportedResult ?? false; + } + + /// Creates a passkey credential. + Future createCredential() async { + final Map getRegisterOptionsResult = + (await repository.getRegisterOptions()).options; + final Map publicKeyCredentialUserEntityJson = + { + 'id': _base64UrlEncodeString( + getRegisterOptionsResult['user']['id'].toString()), + 'name': getRegisterOptionsResult['user']['name'], + 'displayName': getRegisterOptionsResult['user']['displayName'] + }; + + getRegisterOptionsResult['user'] = publicKeyCredentialUserEntityJson; + + final String options = jsonEncode(getRegisterOptionsResult); + + final String? credentials = + await BaseDerivPasskeysMethodChannel.instance.createCredential(options); + if (credentials == null) { + throw PlatformException( + code: 'null-response', + message: 'Unable to get response from Passkey.'); + } + final Map decodedCredentials = jsonDecode(credentials); + final DerivPasskeyEntity getRegisterPasskeysResult = + await repository.registerCredentials( + DerivPasskeysRegisterCredentialsEntity( + publicKeyCredential: decodedCredentials, + name: 'Passkey', + ), + ); + return getRegisterPasskeysResult; + } + + /// Gets a list of passkeys. + Future> getPasskeysList() async { + final List getPasskeysListResult = + await repository.getPasskeysList(); + return getPasskeysListResult; + } + + /// Gets a passkey credential. + Future verifyCredential({ + required String jwtToken, + required PasskeysConnectionInfoEntity passkeysConnectionInfoEntity, + String? userAgent, + }) async { + final Map getOptionsResult = (await repository.getOptions( + passkeysConnectionInfoEntity: passkeysConnectionInfoEntity, + )) + .toJson(); + final String options = jsonEncode(getOptionsResult); + + final String? response = + await BaseDerivPasskeysMethodChannel.instance.getCredential(options); + + if (response == null) { + throw PlatformException( + code: 'null-response', + message: 'Unable to get response from Passkey.'); + } + + final Map decodedResponse = jsonDecode(response); + + final DerivPasskeysVerifyCredentialsResponseEntity + getVerifyCredentialsResult = await repository.verifyCredentials( + requestBodyEntity: DerivPasskeysVerifyCredentialsRequestBodyEntity( + appId: passkeysConnectionInfoEntity.appId, + publicKeyCredential: decodedResponse, + type: 'passkeys', + ), + jwtToken: jwtToken, + passkeysConnectionInfoEntity: passkeysConnectionInfoEntity, + userAgent: userAgent, + ); + + return getVerifyCredentialsResult; + } +} + +String _base64UrlEncodeString(String input) { + final List bytes = utf8.encode(input); + final String base64Str = base64Url.encode(bytes); + return base64Str; +} diff --git a/packages/deriv_passkeys/lib/src/presentation/constants/assets.dart b/packages/deriv_passkeys/lib/src/presentation/constants/assets.dart new file mode 100644 index 000000000..9e7fd6cb3 --- /dev/null +++ b/packages/deriv_passkeys/lib/src/presentation/constants/assets.dart @@ -0,0 +1,32 @@ +/// [Assets] contains all the assets used in the deriv_passkeys plugin. +class Assets { + /// [passkeySvgIcon] is the svg icon used for passkey. + static const String passkeySvgIcon = 'assets/svg/passkey_icon.svg'; + + /// [effortlessPasskeysIcon] is the svg icon used for passkey. + static const String effortlessPasskeysIcon = + 'assets/svg/effortless_login_passkey_icon.svg'; + + /// [passkeyCreatedSuccessIcon] is the svg icon used for passkey. + static const String passkeyCreatedSuccessIcon = + 'assets/svg/passkey_created_success_icon.svg'; + + /// [fingerPrintIcon] is the svg icon used for passkey. + static const String fingerPrintIcon = 'assets/svg/fingerprint_icon.svg'; + + /// [syncIcon] is the svg icon used for passkey. + static const String syncIcon = 'assets/svg/sync_icon.svg'; + + /// [lockIcon] is the svg icon used for passkey. + static const String lockIcon = 'assets/svg/lock_icon.svg'; + + /// [lightBulbIcon] is the svg icon used for passkey. + static const String lightBulbIcon = 'assets/svg/light_bulb_icon.svg'; + + /// [learnMorePasskeysIcon] is the svg icon used for passkey. + static const String learnMorePasskeysIcon = + 'assets/svg/learn_more_passkeys_icon.svg'; + + /// [addPasskeyIcon] is the svg icon used for passkey. + static const String addPasskeyIcon = 'assets/svg/add_passkey_icon.svg'; +} diff --git a/packages/deriv_passkeys/lib/src/presentation/pages/effortless_passkeys_login_page.dart b/packages/deriv_passkeys/lib/src/presentation/pages/effortless_passkeys_login_page.dart new file mode 100644 index 000000000..3d9961d38 --- /dev/null +++ b/packages/deriv_passkeys/lib/src/presentation/pages/effortless_passkeys_login_page.dart @@ -0,0 +1,213 @@ +import 'package:deriv_passkeys/src/extensions/context_extensions.dart'; +import 'package:deriv_passkeys/src/presentation/constants/assets.dart'; +import 'package:deriv_passkeys/src/presentation/pages/learn_more_passkeys_page.dart'; +import 'package:deriv_passkeys/src/presentation/pages/passkey_created_page.dart'; +import 'package:deriv_passkeys/src/presentation/states/bloc/deriv_passkeys_bloc.dart'; +import 'package:deriv_passkeys/src/presentation/utils/handle_errors_utils.dart'; +import 'package:deriv_passkeys/src/presentation/widgets/icon_text_row_widget.dart'; +import 'package:deriv_passkeys/src/presentation/widgets/passkey_created_call_to_action.dart'; +import 'package:deriv_theme/deriv_theme.dart'; +import 'package:deriv_ui/deriv_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; + +/// A stateless widget to build the Effortless Passkeys page. +class EffortlessPasskeysPage extends StatelessWidget { + /// Creates a [EffortlessPasskeysPage]. + const EffortlessPasskeysPage({ + required this.onPageClosed, + required this.addMorePasskeysNavigationCallback, + required this.continueTradingNavigationCallback, + super.key, + }); + + /// The route name for the effortless passkeys page. + static const String routeName = 'effortless_passkeys_page'; + + /// Callback to be called when the user wants to add more passkeys. + final void Function(BuildContext context) addMorePasskeysNavigationCallback; + + /// Callback to be called when the user wants to continue trading. + final void Function(BuildContext context) continueTradingNavigationCallback; + + /// Callback to be called when the flow is complete. + final void Function(BuildContext context) onPageClosed; + + /// + + @override + Widget build(BuildContext context) => + BlocListener( + listener: (BuildContext context, DerivPasskeysState state) { + if (state is DerivPasskeysCreatedSuccessfullyState) { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (BuildContext context) => PasskeyCreatedPage( + onPageClose: onPageClosed, + bottomCallToAction: PasskeysCreatedCallToAction( + addMorePasskeysNavigationCallback: + addMorePasskeysNavigationCallback, + continueTradingNavigationCallback: + continueTradingNavigationCallback, + ), + )), + ); + } else if (state is DerivPasskeysErrorState) { + handlePasskeysError(context, state); + } + }, + child: Scaffold( + body: SafeArea( + child: LayoutBuilder( + builder: (_, constraints) => SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minWidth: constraints.maxWidth, + minHeight: constraints.maxHeight), + child: IntrinsicHeight( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + alignment: Alignment.topRight, + child: Padding( + padding: const EdgeInsets.all(16), + child: TextButton( + onPressed: () => onPageClosed(context), + child: Text( + context + .derivPasskeysLocalizations.maybeLater + .toUpperCase(), + style: TextStyle( + color: context.theme.colors.coral, + ), + ), + ), + ), + ), + Expanded( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 24, vertical: 96), + child: Column( + mainAxisAlignment: + MainAxisAlignment.spaceEvenly, + children: [ + SvgPicture.asset( + Assets.effortlessPasskeysIcon, + package: 'deriv_passkeys', + ), + Text( + context.derivPasskeysLocalizations + .effortlessLoginWithPasskeys, + style: const TextStyle(fontSize: 20), + ), + const SizedBox( + height: 24, + ), + IconTextRowWidget( + assetName: Assets.fingerPrintIcon, + text: context.derivPasskeysLocalizations + .noNeedToRememberPassword, + ), + Divider( + color: context.theme.colors.hover, + ), + IconTextRowWidget( + assetName: Assets.syncIcon, + text: context.derivPasskeysLocalizations + .syncAcrossDevices, + ), + Divider( + color: context.theme.colors.hover, + ), + IconTextRowWidget( + assetName: Assets.lockIcon, + text: context.derivPasskeysLocalizations + .useYourBiometrics, + ), + Divider( + color: context.theme.colors.hover, + ), + SizedBox( + width: double.infinity, + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: + '${context.derivPasskeysLocalizations.learnMoreAboutPasskeys} ', + style: TextStyle( + color: context + .theme.colors.general, + )), + WidgetSpan( + alignment: + PlaceholderAlignment.middle, + child: InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => + LearnMorePasskeysPage( + onPageClosed: + (BuildContext + context) { + Navigator.pop( + context); + }, + addMorePasskeysNavigationCallback: + addMorePasskeysNavigationCallback, + continueTradingNavigationCallback: + continueTradingNavigationCallback, + ), + ), + ); + }, + child: Text( + '${context.derivPasskeysLocalizations.here}.', + style: TextStyle( + color: context.theme + .colors.coral), + ), + ), + ), + ], + ), + ), + ) + ], + ), + ), + ), + Container( + width: double.infinity, + child: Padding( + padding: const EdgeInsets.all(16), + child: PrimaryButton( + onPressed: () { + context.read().add( + DerivPasskeysCreateCredentialEvent()); + }, + child: Text( + context.derivPasskeysLocalizations + .createPasskey, + style: TextStyle( + color: context.theme.colors.prominent, + ), + ), + ), + ), + ) + ], + ), + ), + ), + )), + ), + ), + ); +} diff --git a/packages/deriv_passkeys/lib/src/presentation/pages/learn_more_passkeys_page.dart b/packages/deriv_passkeys/lib/src/presentation/pages/learn_more_passkeys_page.dart new file mode 100644 index 000000000..cc308d638 --- /dev/null +++ b/packages/deriv_passkeys/lib/src/presentation/pages/learn_more_passkeys_page.dart @@ -0,0 +1,253 @@ +import 'package:deriv_passkeys/deriv_passkeys.dart'; +import 'package:deriv_passkeys/src/extensions/context_extensions.dart'; +import 'package:deriv_passkeys/src/presentation/constants/assets.dart'; +import 'package:deriv_passkeys/src/presentation/pages/passkey_created_page.dart'; +import 'package:deriv_passkeys/src/presentation/widgets/passkey_created_call_to_action.dart'; +import 'package:deriv_passkeys/src/presentation/widgets/section_title_and_content.dart'; +import 'package:deriv_passkeys/src/presentation/widgets/unordered_list_widget.dart'; +import 'package:deriv_theme/deriv_theme.dart'; +import 'package:deriv_ui/widgets/primary_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +/// A stateless widget to build the Learn More Passkeys page. +class LearnMorePasskeysPage extends StatelessWidget { + /// Creates a [LearnMorePasskeysPage]. + const LearnMorePasskeysPage({ + required this.onPageClosed, + required this.addMorePasskeysNavigationCallback, + required this.continueTradingNavigationCallback, + super.key, + }); + + /// Callback to be called when the flow is complete. + final void Function(BuildContext context) onPageClosed; + + /// Callback to be called when the user wants to add more passkeys. + final void Function(BuildContext context) addMorePasskeysNavigationCallback; + + /// Callback to be called when the user wants to continue trading. + final void Function(BuildContext context) continueTradingNavigationCallback; + + @override + Widget build(BuildContext context) => + BlocListener( + listener: (BuildContext context, DerivPasskeysState state) { + if (state is DerivPasskeysCreatedSuccessfullyState) { + Navigator.pop(context); + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (BuildContext context) => PasskeyCreatedPage( + onPageClose: onPageClosed, + bottomCallToAction: PasskeysCreatedCallToAction( + addMorePasskeysNavigationCallback: + addMorePasskeysNavigationCallback, + continueTradingNavigationCallback: + continueTradingNavigationCallback, + ), + )), + ); + } + }, + child: Scaffold( + appBar: AppBar(), + body: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: SafeArea( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 14), + child: SvgPicture.asset( + Assets.effortlessPasskeysIcon, + package: 'deriv_passkeys', + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 24), + child: Text( + context + .derivPasskeysLocalizations.effortlessLogin, + style: const TextStyle(fontSize: 20), + ), + ), + SectionTitleAndContent( + title: context + .derivPasskeysLocalizations.whatArePasskeys, + texts: [ + context.derivPasskeysLocalizations + .whatArePasskeysDescriptionPoint1, + context.derivPasskeysLocalizations + .whatArePasskeysDescriptionPoint2 + ], + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Divider( + color: context.theme.colors.hover, + ), + ), + SectionTitleAndContent( + title: + context.derivPasskeysLocalizations.whyPasskeys, + texts: [ + context.derivPasskeysLocalizations + .whyPasskeysDescription1, + context.derivPasskeysLocalizations + .whyPasskeysDescription2 + ], + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Divider( + color: context.theme.colors.hover, + ), + ), + SectionTitleAndContent( + title: context + .derivPasskeysLocalizations.howToCreatePasskey, + texts: [ + context.derivPasskeysLocalizations + .howToCreatePasskeyDescription1, + context.derivPasskeysLocalizations + .howToCreatePasskeyDescription2 + ], + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Divider( + color: context.theme.colors.hover, + ), + ), + SectionTitleAndContent( + title: context.derivPasskeysLocalizations + .whereArePasskeysSaved, + texts: [ + context.derivPasskeysLocalizations + .whereArePasskeysSavedDescriptionAndroid, + context.derivPasskeysLocalizations + .whereArePasskeysSavedDescriptionIOS + ], + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Divider( + color: context.theme.colors.hover, + ), + ), + SectionTitleAndContent( + title: context.derivPasskeysLocalizations + .whatHappensIfEmailChanged, + texts: [ + context.derivPasskeysLocalizations + .whatHappensIfEmailChangedDescription1, + context.derivPasskeysLocalizations + .whatHappensIfEmailChangedDescription2 + ], + ), + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + margin: const EdgeInsets.only(top: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: context.theme.colors.active, + ), + ), + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset( + Assets.lightBulbIcon, + package: 'deriv_passkeys', + ), + const SizedBox( + width: 8, + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + '${context.derivPasskeysLocalizations.tips}:', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: context.theme.colors.prominent, + ), + ), + Text( + '${context.derivPasskeysLocalizations.beforeUsingPasskeys}:', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: context.theme.colors.general, + ), + ), + const SizedBox( + height: 4, + ), + UnorderedList( + texts: [ + context.derivPasskeysLocalizations + .enableScreenLock, + context.derivPasskeysLocalizations + .signInGoogleOrIcloud, + context.derivPasskeysLocalizations + .enableBluetooth + ], + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: context.theme.colors.general, + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ), + Container( + color: context.theme.colors.secondary, + width: double.infinity, + child: Padding( + padding: const EdgeInsets.only( + left: 24, right: 24, bottom: 24, top: 16), + child: PrimaryButton( + onPressed: () { + context + .read() + .add(DerivPasskeysCreateCredentialEvent()); + }, + child: Text( + context.derivPasskeysLocalizations.createPasskey, + style: TextStyle( + color: context.theme.colors.prominent, + ), + ), + ), + ), + ), + ], + ), + ), + ); +} diff --git a/packages/deriv_passkeys/lib/src/presentation/pages/manage_passkeys_page.dart b/packages/deriv_passkeys/lib/src/presentation/pages/manage_passkeys_page.dart new file mode 100644 index 000000000..fa28503bb --- /dev/null +++ b/packages/deriv_passkeys/lib/src/presentation/pages/manage_passkeys_page.dart @@ -0,0 +1,195 @@ +import 'package:deriv_passkeys/src/extensions/context_extensions.dart'; +import 'package:deriv_passkeys/src/presentation/constants/assets.dart'; +import 'package:deriv_passkeys/src/presentation/pages/learn_more_passkeys_page.dart'; +import 'package:deriv_passkeys/src/presentation/pages/passkey_created_page.dart'; +import 'package:deriv_passkeys/src/presentation/states/bloc/deriv_passkeys_bloc.dart'; +import 'package:deriv_passkeys/src/presentation/utils/handle_errors_utils.dart'; +import 'package:deriv_passkeys/src/presentation/widgets/passkey_created_call_to_action.dart'; +import 'package:deriv_passkeys/src/presentation/widgets/passkey_widget.dart'; +import 'package:deriv_theme/deriv_theme.dart'; +import 'package:deriv_ui/deriv_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; + +/// A stateless widget to build the Manage Passkeys page. +class ManagePasskeysPage extends StatefulWidget { + /// Creates a [ManagePasskeysPage]. + const ManagePasskeysPage({ + required this.addMorePasskeysNavigationCallback, + required this.continueTradingNavigationCallback, + super.key, + }); + + /// The route name for the manage passkeys page. + static const String routeName = 'manage_passkeys_page'; + + /// Callback to be called when the user wants to add more passkeys. + final void Function(BuildContext context) addMorePasskeysNavigationCallback; + + /// Callback to be called when the user wants to continue trading. + final void Function(BuildContext context) continueTradingNavigationCallback; + + @override + State createState() => _ManagePasskeysPageState(); +} + +class _ManagePasskeysPageState extends State { + @override + void initState() { + super.initState(); + context + .read() + .add(const DerivPasskeysGetPasskeysListEvent()); + } + + @override + Widget build(BuildContext context) => Scaffold( + backgroundColor: context.theme.colors.primary, + appBar: AppBar( + title: const Text('Passkeys', style: TextStyle(fontSize: 20)), + actions: [ + InkWell( + child: Padding( + padding: const EdgeInsets.all(16), + child: SvgPicture.asset( + Assets.learnMorePasskeysIcon, + package: 'deriv_passkeys', + ), + ), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => LearnMorePasskeysPage( + onPageClosed: (BuildContext context) { + Navigator.pop(context); + }, + addMorePasskeysNavigationCallback: + widget.addMorePasskeysNavigationCallback, + continueTradingNavigationCallback: + widget.continueTradingNavigationCallback, + )), + ); + }, + ), + ], + ), + body: BlocConsumer( + listener: (BuildContext context, DerivPasskeysState state) { + if (state is DerivPasskeysCreatedSuccessfullyState) { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => PasskeyCreatedPage( + onPageClose: (BuildContext context) { + Navigator.pop(context); + }, + bottomCallToAction: PasskeysCreatedCallToAction( + addMorePasskeysNavigationCallback: + widget.addMorePasskeysNavigationCallback, + continueTradingNavigationCallback: + widget.continueTradingNavigationCallback, + ), + )), + ); + } else if (state is DerivPasskeysErrorState) { + handlePasskeysError(context, state); + } + }, builder: (BuildContext context, DerivPasskeysState state) { + if (state is DerivPasskeysLoadedState) { + return SafeArea( + child: SizedBox( + width: double.infinity, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildDerivPasskeysLoadedContent( + context, + state, + ), + Container( + width: double.infinity, + child: Padding( + padding: const EdgeInsets.all(16), + child: PrimaryButton( + onPressed: () { + context + .read() + .add(DerivPasskeysCreateCredentialEvent()); + }, + child: Text( + context.derivPasskeysLocalizations.createPasskey, + style: TextStyle( + color: context.theme.colors.prominent, + ), + ), + ), + ), + ) + ], + ), + ), + ); + } + return const SizedBox(); + }), + ); + + Widget _buildDerivPasskeysListContent( + BuildContext context, DerivPasskeysLoadedState state) => + Expanded( + child: Padding( + padding: const EdgeInsets.all(24), + child: ListView.separated( + separatorBuilder: (BuildContext context, int index) => + const Divider(), + itemCount: state.passkeysList.length, + itemBuilder: (BuildContext context, int index) => Padding( + padding: const EdgeInsets.only(bottom: 16), + child: PasskeyWidget(passkey: state.passkeysList[index]), + ), + ), + ), + ); + + Widget _buildDerivPasskeysEmptyContent(BuildContext context) => Expanded( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 14), + child: SvgPicture.asset( + Assets.addPasskeyIcon, + package: 'deriv_passkeys', + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 24), + child: Text( + context.derivPasskeysLocalizations.experienceSaferLogins, + style: const TextStyle(fontSize: 20), + ), + ), + SizedBox( + width: double.infinity, + child: Text( + context.derivPasskeysLocalizations.enhanceSecurity, + textAlign: TextAlign.center, + ), + ) + ], + ), + ), + ); + + Widget _buildDerivPasskeysLoadedContent( + BuildContext context, DerivPasskeysLoadedState state) { + if (state.passkeysList.isEmpty) { + return _buildDerivPasskeysEmptyContent(context); + } + return _buildDerivPasskeysListContent(context, state); + } +} diff --git a/packages/deriv_passkeys/lib/src/presentation/pages/passkey_created_page.dart b/packages/deriv_passkeys/lib/src/presentation/pages/passkey_created_page.dart new file mode 100644 index 000000000..b9000812e --- /dev/null +++ b/packages/deriv_passkeys/lib/src/presentation/pages/passkey_created_page.dart @@ -0,0 +1,78 @@ +import 'package:deriv_passkeys/src/extensions/context_extensions.dart'; +import 'package:deriv_passkeys/src/presentation/constants/assets.dart'; +import 'package:deriv_passkeys/src/presentation/utils/platform_utils.dart'; +import 'package:deriv_theme/deriv_theme.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +/// A stateless widget to build the passkey created successfully page. +class PasskeyCreatedPage extends StatelessWidget { + /// Creates a [PasskeyCreatedPage]. + const PasskeyCreatedPage({ + required this.onPageClose, + required this.bottomCallToAction, + super.key, + }); + + /// A callback function that will be called when the user clicks on the 'Continue' button. + final void Function(BuildContext context) onPageClose; + + /// The widget to be displayed at the bottom of the page. + final Widget bottomCallToAction; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: () => onPageClose(context), + ), + ), + body: SafeArea( + child: SizedBox( + width: double.infinity, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 14), + child: SvgPicture.asset( + Assets.passkeyCreatedSuccessIcon, + package: 'deriv_passkeys', + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 24), + child: Text( + context.derivPasskeysLocalizations + .passkeyCreatedSuccessTitle, + style: const TextStyle(fontSize: 20), + ), + ), + Text( + context.derivPasskeysLocalizations + .passkeyCreatedSuccessMessage( + platformName(context)), + style: TextStyle( + fontSize: 14, + color: context.theme.colors.general, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + bottomCallToAction, + ], + ), + ), + ), + ); +} diff --git a/packages/deriv_passkeys/lib/src/presentation/presentation.dart b/packages/deriv_passkeys/lib/src/presentation/presentation.dart new file mode 100644 index 000000000..d533211de --- /dev/null +++ b/packages/deriv_passkeys/lib/src/presentation/presentation.dart @@ -0,0 +1,6 @@ +library deriv_passkeys; + +export './pages/manage_passkeys_page.dart'; +export './pages/effortless_passkeys_login_page.dart'; +export './widgets/continue_with_passkey_button.dart'; +export './states/bloc/deriv_passkeys_bloc.dart'; diff --git a/packages/deriv_passkeys/lib/src/presentation/states/bloc/deriv_passkeys_bloc.dart b/packages/deriv_passkeys/lib/src/presentation/states/bloc/deriv_passkeys_bloc.dart new file mode 100644 index 000000000..a3dfe172d --- /dev/null +++ b/packages/deriv_passkeys/lib/src/presentation/states/bloc/deriv_passkeys_bloc.dart @@ -0,0 +1,151 @@ +import 'dart:io'; + +import 'package:deriv_passkeys/src/exceptions/platform_exceptions.dart'; +import 'package:deriv_passkeys/src/domain/entities/passkeys_connection_info_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkey_entity.dart'; +import 'package:deriv_passkeys/src/exceptions/server_exceptions.dart'; +import 'package:deriv_passkeys/src/interactor/services/deriv_passkeys_service.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkeys_verify_credentials_response_entity.dart'; + +part 'deriv_passkeys_state.dart'; +part 'deriv_passkeys_event.dart'; + +/// [DerivPasskeysBloc] handles the state within the DerivPasskeys flow. +class DerivPasskeysBloc extends Bloc { + /// Creates a [DerivPasskeysBloc]. + DerivPasskeysBloc({ + required DerivPasskeysService derivPasskeysService, + required PasskeysConnectionInfoEntity connectionInfo, + required Future Function() getJwtToken, + }) : super(DerivPasskeysLoadingState()) { + on( + (SetDerivPasskeysInitializedEvent event, + Emitter emit) async { + emit(DerivPasskeysInitializedState()); + }); + + on( + (SetDerivPasskeysNotSupportedEvent event, + Emitter emit) async { + emit(DerivPasskeysNotSupportedState()); + }); + + on( + (DerivPasskeysVerifyCredentialEvent event, + Emitter emit) async { + if (state is DerivPasskeysLoadingState) { + return; + } + + emit(DerivPasskeysLoadingState()); + + final String jwtToken = await getJwtToken(); + + await derivPasskeysService + .verifyCredential( + jwtToken: jwtToken, + passkeysConnectionInfoEntity: connectionInfo, + userAgent: WebSocket.userAgent, + ) + .then((DerivPasskeysVerifyCredentialsResponseEntity + derivPasskeysVerifyCredentialsResponseEntity) { + emit( + DerivPasskeysCredentialVerifiedState( + token: derivPasskeysVerifyCredentialsResponseEntity.token, + ), + ); + }).catchError((Object error) { + if (error is CanceledPlatformException) { + emit(DerivPasskeysLoadedState(passkeysList)); + } else if (error is NoCredentialPlatformException) { + emit(const NoCredentialErrorState()); + } else if (error is ServerException) { + emit( + DerivPasskeysErrorState( + error.message, + errorCode: error.errorCode, + ), + ); + } else { + emit(const DerivPasskeysErrorState('Error verifying passkey')); + } + }); + }); + + on( + (DerivPasskeysCreateCredentialEvent event, + Emitter emit) async { + if (state is DerivPasskeysLoadingState) { + return; + } + emit(DerivPasskeysLoadingState()); + await derivPasskeysService + .createCredential() + .then((DerivPasskeyEntity credential) async { + emit(DerivPasskeysCreatedSuccessfullyState()); + final DerivPasskeyEntity derivPasskeyEntity = credential; + passkeysList.add(derivPasskeyEntity); + emit(DerivPasskeysLoadedState(passkeysList)); + }).catchError((Object error) { + if (error is ServerException) { + emit( + DerivPasskeysErrorState( + error.message, + errorCode: error.errorCode, + ), + ); + } else { + emit(const DerivPasskeysErrorState('Error creating passkey')); + } + emit(DerivPasskeysLoadedState(passkeysList)); + }); + }); + + on( + (DerivPasskeysGetPasskeysListEvent event, + Emitter emit) async { + emit(DerivPasskeysLoadingState()); + + await derivPasskeysService + .getPasskeysList() + .then((List _passkeysList) { + passkeysList = _passkeysList; + emit(DerivPasskeysLoadedState(passkeysList)); + }).catchError((Object error) { + if (error is ServerException) { + emit( + DerivPasskeysErrorState( + error.message, + errorCode: error.errorCode, + ), + ); + } else { + emit(DerivPasskeysErrorState(error.toString())); + } + }); + }); + + on( + (DerivPasskeysLogoutEvent event, Emitter emit) { + passkeysList.clear(); + if (state is DerivPasskeysNotSupportedState) { + emit(DerivPasskeysNotSupportedState()); + } else { + emit(DerivPasskeysInitializedState()); + } + }); + + derivPasskeysService.isSupported().then((bool isSupported) { + if (isSupported) { + add(const SetDerivPasskeysInitializedEvent()); + } else { + add(const SetDerivPasskeysNotSupportedEvent()); + } + }); + } + + /// The list of passkeys. + List passkeysList = []; +} diff --git a/packages/deriv_passkeys/lib/src/presentation/states/bloc/deriv_passkeys_event.dart b/packages/deriv_passkeys/lib/src/presentation/states/bloc/deriv_passkeys_event.dart new file mode 100644 index 000000000..ea2a56fbf --- /dev/null +++ b/packages/deriv_passkeys/lib/src/presentation/states/bloc/deriv_passkeys_event.dart @@ -0,0 +1,70 @@ +part of 'deriv_passkeys_bloc.dart'; + +/// [DerivPasskeysEvent] represents the events within the DerivPasskeys flow. +class DerivPasskeysEvent extends Equatable { + /// Creates a [DerivPasskeysEvent]. + const DerivPasskeysEvent(); + + @override + List get props => []; +} + +/// [DerivPasskeysCreateCredentialEvent] represents the create credential event within the DerivPasskeys flow. +class DerivPasskeysCreateCredentialEvent extends DerivPasskeysEvent {} + +/// [DerivPasskeysVerifyCredentialEvent] represents the get credential event within the DerivPasskeys flow. +class DerivPasskeysVerifyCredentialEvent extends DerivPasskeysEvent {} + +/// [DerivPasskeysGetPasskeysListEvent] represents the get passkeys list event within the DerivPasskeys flow. +class DerivPasskeysGetPasskeysListEvent extends DerivPasskeysEvent { + /// Creates a [DerivPasskeysGetPasskeysListEvent]. + const DerivPasskeysGetPasskeysListEvent(); + + @override + List get props => []; +} + +/// [SetDerivPasskeysInitializedEvent] represents the set initialized event within the DerivPasskeys flow. +class SetDerivPasskeysInitializedEvent extends DerivPasskeysEvent { + /// Creates a [SetDerivPasskeysInitializedEvent]. + const SetDerivPasskeysInitializedEvent(); + + @override + List get props => []; +} + +/// [SetDerivPasskeysNotSupportedEvent] represents the set not supported event within the DerivPasskeys flow. +class SetDerivPasskeysNotSupportedEvent extends DerivPasskeysEvent { + /// Creates a [SetDerivPasskeysNotSupportedEvent]. + const SetDerivPasskeysNotSupportedEvent(); + + @override + List get props => []; +} + +/// [DerivPasskeysRevokeCredentialEvent] represents the revoke credential event within the DerivPasskeys flow. +class DerivPasskeysRevokeCredentialEvent extends DerivPasskeysEvent { + /// Creates a [DerivPasskeysRevokeCredentialEvent]. + const DerivPasskeysRevokeCredentialEvent(this.options); + + /// The options to revoke a credential. + final String options; + + @override + List get props => [options]; +} + +/// [DerivPasskeysEditCredentialEvent] represents the edit credential event within the DerivPasskeys flow. +class DerivPasskeysEditCredentialEvent extends DerivPasskeysEvent { + /// Creates a [DerivPasskeysEditCredentialEvent]. + const DerivPasskeysEditCredentialEvent(this.options); + + /// The options to edit a credential. + final String options; + + @override + List get props => [options]; +} + +/// [DerivPasskeysLogoutEvent] represents the logout event within the DerivPasskeys flow. +class DerivPasskeysLogoutEvent extends DerivPasskeysEvent {} diff --git a/packages/deriv_passkeys/lib/src/presentation/states/bloc/deriv_passkeys_state.dart b/packages/deriv_passkeys/lib/src/presentation/states/bloc/deriv_passkeys_state.dart new file mode 100644 index 000000000..e50ae555c --- /dev/null +++ b/packages/deriv_passkeys/lib/src/presentation/states/bloc/deriv_passkeys_state.dart @@ -0,0 +1,67 @@ +part of 'deriv_passkeys_bloc.dart'; + +/// [DerivPasskeysState] represents the state within the DerivPasskeys flow. +abstract class DerivPasskeysState extends Equatable { + /// Creates a [DerivPasskeysState]. + const DerivPasskeysState(); + + @override + List get props => []; +} + +/// [DerivPasskeysLoadedState] represents the loaded state within the DerivPasskeys flow. +class DerivPasskeysLoadedState extends DerivPasskeysState { + /// Creates a [DerivPasskeysLoadedState]. + const DerivPasskeysLoadedState(this.passkeysList); + + /// The passkeys list. + final List passkeysList; + + @override + List get props => [passkeysList]; +} + +/// [DerivPasskeysInitializedState] represents the initialized state within the DerivPasskeys flow. +class DerivPasskeysInitializedState extends DerivPasskeysState {} + +/// [DerivPasskeysLoadingState] represents the loading state within the DerivPasskeys flow. +class DerivPasskeysLoadingState extends DerivPasskeysState {} + +/// [DerivPasskeysCreatedSuccessfullyState] represents the created successfully state within the DerivPasskeys flow. +class DerivPasskeysCreatedSuccessfullyState extends DerivPasskeysState {} + +/// [DerivPasskeysCredentialVerifiedState] represents the credential verified state within the DerivPasskeys flow. +class DerivPasskeysCredentialVerifiedState extends DerivPasskeysState { + /// Creates a [DerivPasskeysCredentialVerifiedState]. + const DerivPasskeysCredentialVerifiedState({required this.token}); + + /// The response. + final String token; + + @override + List get props => [token]; +} + +/// [DerivPasskeysNotSupportedState] represents the not supported state within the DerivPasskeys flow. +class DerivPasskeysNotSupportedState extends DerivPasskeysState {} + +/// [DerivPasskeysErrorState] represents the error state within the DerivPasskeys flow. +class DerivPasskeysErrorState extends DerivPasskeysState { + /// Creates a [DerivPasskeysErrorState]. + const DerivPasskeysErrorState(this.message, {this.errorCode = ''}); + + /// The error message. + final String message; + + /// The error code + final String errorCode; + + @override + List get props => [message, errorCode]; +} + +/// [NoCredentialErrorState] represents the no credential error state within the DerivPasskeys flow. +class NoCredentialErrorState extends DerivPasskeysErrorState { + /// Creates a [NoCredentialErrorState]. + const NoCredentialErrorState() : super('No credential found'); +} diff --git a/packages/deriv_passkeys/lib/src/presentation/utils/date_time_utils.dart b/packages/deriv_passkeys/lib/src/presentation/utils/date_time_utils.dart new file mode 100644 index 000000000..f7949588d --- /dev/null +++ b/packages/deriv_passkeys/lib/src/presentation/utils/date_time_utils.dart @@ -0,0 +1,10 @@ +import 'package:jiffy/jiffy.dart'; + +///returns DateTime from timestamp +DateTime dateTimeFromTimestamp(int timestamp) => + DateTime.fromMillisecondsSinceEpoch(timestamp); + +///returns formatted date +///e.g. April 20th, 2023 +String formattedDate(DateTime date) => + Jiffy.parseFromDateTime(date).format(pattern: 'MMMM do, yyyy'); diff --git a/packages/deriv_passkeys/lib/src/presentation/utils/handle_errors_utils.dart b/packages/deriv_passkeys/lib/src/presentation/utils/handle_errors_utils.dart new file mode 100644 index 000000000..192db00e2 --- /dev/null +++ b/packages/deriv_passkeys/lib/src/presentation/utils/handle_errors_utils.dart @@ -0,0 +1,58 @@ +//handlePasskeysError +import 'package:deriv_passkeys/deriv_passkeys.dart'; +import 'package:deriv_passkeys/src/extensions/context_extensions.dart'; +import 'package:deriv_ui/widgets/popup_alert_dialog.dart'; +import 'package:flutter/material.dart'; + +/// Handles the passkeys error state. +void handlePasskeysError(BuildContext context, DerivPasskeysErrorState state) { + if (state is NoCredentialErrorState) { + showDialog( + context: context, + builder: (BuildContext context) => PopupAlertDialog( + title: context.derivPasskeysLocalizations.noPasskeyFound, + content: + Text(context.derivPasskeysLocalizations.noPasskeyFoundDescription), + positiveButtonLabel: context.derivPasskeysLocalizations.ok, + onPositiveActionPressed: () { + Navigator.pop(context, context.derivPasskeysLocalizations.ok); + }, + ), + ); + } else { + bool showErrorDialogue = false; + String title = context.derivPasskeysLocalizations.unexpectedError; + String content = state.message; + + if (state.errorCode == 'PASSKEYS_SERVICE_ERROR') { + content = state.message; + showErrorDialogue = true; + } + if (state.errorCode == 'PASSKEYS_NOT_FOUND') { + title = context.derivPasskeysLocalizations.noPasskeyFound; + content = state.message; + showErrorDialogue = true; + } + + if (state.errorCode == 'PASSKEYS_OFF' || state.errorCode == 'PasskeysOff') { + title = context.derivPasskeysLocalizations.unable_to_process_your_request; + content = context.derivPasskeysLocalizations + .unable_to_process_your_request_description; + showErrorDialogue = true; + } + + if (showErrorDialogue) { + showDialog( + context: context, + builder: (BuildContext context) => PopupAlertDialog( + title: title, + content: Text(content), + positiveButtonLabel: context.derivPasskeysLocalizations.ok, + onPositiveActionPressed: () { + Navigator.pop(context, context.derivPasskeysLocalizations.ok); + }, + ), + ); + } + } +} diff --git a/packages/deriv_passkeys/lib/src/presentation/utils/platform_utils.dart b/packages/deriv_passkeys/lib/src/presentation/utils/platform_utils.dart new file mode 100644 index 000000000..4dbd2c265 --- /dev/null +++ b/packages/deriv_passkeys/lib/src/presentation/utils/platform_utils.dart @@ -0,0 +1,15 @@ +import 'dart:io'; + +import 'package:deriv_passkeys/src/extensions/context_extensions.dart'; +import 'package:flutter/widgets.dart'; + +/// Returns the platform name. +String platformName(BuildContext context) { + if (Platform.isIOS) { + return 'IOS'; + } + if (Platform.isAndroid) { + return 'Android'; + } + return context.derivPasskeysLocalizations.unsupportedPlatform; +} diff --git a/packages/deriv_passkeys/lib/src/presentation/widgets/continue_with_passkey_button.dart b/packages/deriv_passkeys/lib/src/presentation/widgets/continue_with_passkey_button.dart new file mode 100644 index 000000000..c739f35f9 --- /dev/null +++ b/packages/deriv_passkeys/lib/src/presentation/widgets/continue_with_passkey_button.dart @@ -0,0 +1,80 @@ +import 'package:deriv_passkeys/src/presentation/constants/assets.dart'; +import 'package:deriv_passkeys/src/presentation/states/bloc/deriv_passkeys_bloc.dart'; +import 'package:deriv_passkeys/src/presentation/utils/handle_errors_utils.dart'; +import 'package:deriv_theme/deriv_theme.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:deriv_ui/deriv_ui.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +/// A button that allows users to continue with passkey +class ContinueWithPasskeyButton extends StatelessWidget { + /// constructs a [ContinueWithPasskeyButton] + const ContinueWithPasskeyButton({ + super.key, + }); + + @override + Widget build(BuildContext context) => + BlocConsumer( + listener: (BuildContext context, DerivPasskeysState state) { + if (state is DerivPasskeysErrorState) { + handlePasskeysError(context, state); + } + }, + builder: (BuildContext context, DerivPasskeysState state) { + if (state is DerivPasskeysNotSupportedState) { + return const SizedBox(); + } + + return InkWell( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: context.theme.colors.active, + ), + ), + child: state is DerivPasskeysLoadingState + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + LoadingIndicator( + valueColor: context.theme.colors.prominent, + strokeWidth: ThemeProvider.margin02, + height: ThemeProvider.iconSize16, + width: ThemeProvider.iconSize16, + ) + ], + ) + : Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + Assets.passkeySvgIcon, + package: 'deriv_passkeys', + ), + const SizedBox(width: 8), + Text( + 'Passkey', + style: context.theme.textStyle( + textStyle: TextStyles.body2, + color: context.theme.colors.prominent, + ), + ), + ], + ), + ), + onTap: () async { + context + .read() + .add(DerivPasskeysVerifyCredentialEvent()); + }, + ); + }, + ); +} diff --git a/packages/deriv_passkeys/lib/src/presentation/widgets/icon_text_row_widget.dart b/packages/deriv_passkeys/lib/src/presentation/widgets/icon_text_row_widget.dart new file mode 100644 index 000000000..b412b7d23 --- /dev/null +++ b/packages/deriv_passkeys/lib/src/presentation/widgets/icon_text_row_widget.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +/// A widget that displays an icon and text in a row. +class IconTextRowWidget extends StatelessWidget { + /// Creates an [IconTextRowWidget]. + const IconTextRowWidget({ + required this.assetName, + required this.text, + }); + + /// The name of the asset to display. + final String assetName; + + /// The text to display. + final String text; + + @override + Widget build(BuildContext context) => Row( + children: [ + SvgPicture.asset( + assetName, + package: 'deriv_passkeys', + ), + const SizedBox(width: 8), + Expanded(child: Text(text)), + ], + ); +} diff --git a/packages/deriv_passkeys/lib/src/presentation/widgets/passkey_created_call_to_action.dart b/packages/deriv_passkeys/lib/src/presentation/widgets/passkey_created_call_to_action.dart new file mode 100644 index 000000000..50591285c --- /dev/null +++ b/packages/deriv_passkeys/lib/src/presentation/widgets/passkey_created_call_to_action.dart @@ -0,0 +1,52 @@ +import 'package:deriv_passkeys/src/extensions/context_extensions.dart'; +import 'package:deriv_theme/deriv_theme.dart'; +import 'package:deriv_ui/deriv_ui.dart'; +import 'package:flutter/material.dart'; + +/// A stateless widget to build the passkeys created call to action. +class PasskeysCreatedCallToAction extends StatelessWidget { + /// Creates a [PasskeysCreatedCallToAction]. + const PasskeysCreatedCallToAction({ + required this.addMorePasskeysNavigationCallback, + required this.continueTradingNavigationCallback, + super.key, + }); + + /// Callback to be called when the user wants to add more passkeys. + final void Function(BuildContext context) addMorePasskeysNavigationCallback; + + /// Callback to be called when the user wants to continue trading. + final void Function(BuildContext context) continueTradingNavigationCallback; + + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + onPressed: () => addMorePasskeysNavigationCallback(context), + child: Text( + context.derivPasskeysLocalizations.addMorePasskeysButtonText, + style: TextStyle( + color: context.theme.colors.prominent, + ), + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: PrimaryButton( + onPressed: () => continueTradingNavigationCallback(context), + child: Text( + context.derivPasskeysLocalizations.continueTradingButtonText, + style: TextStyle( + color: context.theme.colors.prominent, + ), + ), + ), + ), + ], + ), + ); +} diff --git a/packages/deriv_passkeys/lib/src/presentation/widgets/passkey_widget.dart b/packages/deriv_passkeys/lib/src/presentation/widgets/passkey_widget.dart new file mode 100644 index 000000000..f6d1a7340 --- /dev/null +++ b/packages/deriv_passkeys/lib/src/presentation/widgets/passkey_widget.dart @@ -0,0 +1,110 @@ +import 'package:deriv_passkeys/src/domain/entities/deriv_passkey_entity.dart'; +import 'package:deriv_passkeys/src/extensions/context_extensions.dart'; +import 'package:deriv_passkeys/src/presentation/constants/assets.dart'; +import 'package:deriv_passkeys/src/presentation/utils/date_time_utils.dart'; +import 'package:deriv_theme/deriv_theme.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +/// [PasskeyWidget] is a widget that displays the passkey information. +class PasskeyWidget extends StatefulWidget { + /// Creates a [PasskeyWidget]. + const PasskeyWidget({ + required this.passkey, + super.key, + }); + + /// The passkey entity. + final DerivPasskeyEntity passkey; + + @override + State createState() => _PasskeyWidgetState(); +} + +class _PasskeyWidgetState extends State { + String _lastUsed() { + String lastUsed = context.derivPasskeysLocalizations.never; + if (widget.passkey.lastUsed != null) { + lastUsed = formattedDate(dateTimeFromTimestamp(widget.passkey.lastUsed!)); + } + return lastUsed; + } + + @override + Widget build(BuildContext context) => Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: SvgPicture.asset( + Assets.passkeySvgIcon, + package: 'deriv_passkeys', + ), + ), + const SizedBox(width: 8), + Expanded( + flex: 10, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(widget.passkey.name, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: context.theme.colors.prominent, + )), + Text( + '${context.derivPasskeysLocalizations.storedOn}: ${widget.passkey.storedOn}', + style: TextStyle( + fontSize: 12, + color: context.theme.colors.general, + ), + ), + Text( + '${context.derivPasskeysLocalizations.lastUsed}: ${_lastUsed()}', + style: TextStyle( + fontSize: 12, + color: context.theme.colors.general, + ), + ), + ], + ), + ), + // TODO(bassam): uncomment and implement once phase 2 starts + // DropdownButtonHideUnderline( + // child: DropdownButton( + // icon: Icon(Icons.more_vert), + // items: [ + // TextButton( + // onPressed: () { + // }, + // child: Text( + // context.derivPasskeysLocalizations.rename, + // style: TextStyle( + // fontSize: 14, + // color: context.theme.colors.general, + // ), + // ), + // ), + // TextButton( + // onPressed: () { + // }, + // child: Text( + // context.derivPasskeysLocalizations.revoke, + // style: TextStyle( + // fontSize: 14, + // color: context.theme.colors.general, + // ), + // ), + // ), + // ] + // .map((Widget item) => DropdownMenuItem( + // value: item, + // child: item, + // )) + // .toList(), + // onChanged: (Widget? value) {}, + // ), + // ), + ], + ); +} diff --git a/packages/deriv_passkeys/lib/src/presentation/widgets/section_title_and_content.dart b/packages/deriv_passkeys/lib/src/presentation/widgets/section_title_and_content.dart new file mode 100644 index 000000000..a70aa359a --- /dev/null +++ b/packages/deriv_passkeys/lib/src/presentation/widgets/section_title_and_content.dart @@ -0,0 +1,42 @@ +import 'package:deriv_passkeys/src/presentation/widgets/unordered_list_widget.dart'; +import 'package:deriv_theme/deriv_theme.dart'; +import 'package:flutter/material.dart'; + +/// A widget that displays a section title and content. +class SectionTitleAndContent extends StatelessWidget { + /// Creates a [SectionTitleAndContent]. + const SectionTitleAndContent({ + required this.title, + required this.texts, + }); + + /// The title of the section. + final String title; + + /// The content of the section. + final List texts; + + @override + Widget build(BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 14, + color: context.theme.colors.prominent, + ), + ), + const SizedBox(height: 8), + UnorderedList( + texts: texts, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: context.theme.colors.general, + ), + expandedContent: true, + ), + ], + ); +} diff --git a/packages/deriv_passkeys/lib/src/presentation/widgets/unordered_list_widget.dart b/packages/deriv_passkeys/lib/src/presentation/widgets/unordered_list_widget.dart new file mode 100644 index 000000000..76fc68465 --- /dev/null +++ b/packages/deriv_passkeys/lib/src/presentation/widgets/unordered_list_widget.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; + +/// A widget that displays a list of items with bullet points. +class UnorderedList extends StatelessWidget { + /// Creates an [UnorderedList]. + const UnorderedList({ + required this.texts, + this.style, + this.expandedContent = false, + }); + + /// The list of texts to display. + final List texts; + + /// The style to use for the text. + final TextStyle? style; + + /// Whether the content should be expanded. + final bool expandedContent; + + @override + Widget build(BuildContext context) { + final List widgetList = texts + .map((String text) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + UnorderedListItem( + text: text, + textStyle: style, + expandedContent: expandedContent, + ), + const SizedBox(height: 5), + ], + )) + .toList(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: widgetList, + ); + } +} + +/// A widget that displays an unordered list item. +@visibleForTesting +class UnorderedListItem extends StatelessWidget { + /// Creates an [UnorderedListItem]. + const UnorderedListItem({ + required this.text, + this.textStyle, + this.expandedContent = false, + }); + + /// The text to display. + final String text; + + /// The style to use for the text. + final TextStyle? textStyle; + + /// Whether the content should be expanded. + final bool expandedContent; + + @override + Widget build(BuildContext context) { + Widget content = Text( + text, + style: textStyle, + ); + if (expandedContent) { + content = Expanded( + child: content, + ); + } + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + width: 5, + ), + Text('\u2022', style: textStyle), + const SizedBox( + width: 5, + ), + content, + ], + ); + } +} diff --git a/packages/deriv_passkeys/pubspec.yaml b/packages/deriv_passkeys/pubspec.yaml new file mode 100644 index 000000000..aebf18665 --- /dev/null +++ b/packages/deriv_passkeys/pubspec.yaml @@ -0,0 +1,111 @@ +name: deriv_passkeys +description: Deriv Passkeys Flutter Plugin +version: 0.0.1 +publish_to: 'none' + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + + deriv_theme: + git: + url: git@github.com:regentmarkets/flutter-deriv-packages.git + path: packages/deriv_theme + ref: deriv_theme-v2.4.0 + deriv_localizations: + git: + url: git@github.com:regentmarkets/flutter-deriv-packages.git + path: packages/deriv_localizations + ref: deriv_localizations-v1.3.2 + deriv_ui: + git: + url: git@github.com:regentmarkets/flutter-deriv-packages.git + path: packages/deriv_ui + ref: deriv_ui-v0.0.7+4 + deriv_http_client: + git: + url: git@github.com:regentmarkets/flutter-deriv-packages.git + path: packages/deriv_http_client + ref: deriv_http_client-v2.0.1 + flutter_deriv_api: + git: + url: git@github.com:deriv-com/flutter-deriv-api.git + ref: dev + + + flutter_svg: ^2.0.9 + plugin_platform_interface: ^2.0.2 + http: ^0.13.4 + jiffy: ^6.2.1 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + mocktail: ^1.0.3 + bloc_test: ^9.1.7 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + uses-material-design: true + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) + # which should be registered in the plugin registry. This is required for + # using method channels. + # The Android 'package' specifies package in which the registered class is. + # This is required for using method channels on Android. + # The 'ffiPlugin' specifies that native code should be built and bundled. + # This is required for using `dart:ffi`. + # All these are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + android: + package: com.deriv.passkeys.deriv_passkeys + pluginClass: DerivPasskeysPlugin + ios: + pluginClass: DerivPasskeysPlugin + + assets: + - assets/svg/passkey_icon.svg + - assets/svg/effortless_login_passkey_icon.svg + - assets/svg/passkey_created_success_icon.svg + - assets/svg/fingerprint_icon.svg + - assets/svg/sync_icon.svg + - assets/svg/lock_icon.svg + - assets/svg/light_bulb_icon.svg + - assets/svg/learn_more_passkeys_icon.svg + - assets/svg/add_passkey_icon.svg + - assets/svg/face_id.svg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/deriv_passkeys/test/data/deriv_passkeys_data_source_mock_setup.dart b/packages/deriv_passkeys/test/data/deriv_passkeys_data_source_mock_setup.dart new file mode 100644 index 000000000..221657dae --- /dev/null +++ b/packages/deriv_passkeys/test/data/deriv_passkeys_data_source_mock_setup.dart @@ -0,0 +1,123 @@ +import 'package:deriv_passkeys/src/data/data_sources/deriv_passkeys_data_source.dart'; +import 'package:deriv_passkeys/src/data/mappers/deriv_passkeys_mapper.dart'; +import 'package:deriv_passkeys/src/data/models/deriv_passkey_model.dart'; +import 'package:deriv_passkeys/src/data/models/deriv_passkeys_options_model.dart'; +import 'package:deriv_passkeys/src/data/models/deriv_passkeys_register_options_model.dart'; +import 'package:deriv_passkeys/src/data/models/deriv_passkeys_verify_credentials_request_body_model.dart'; +import 'package:deriv_passkeys/src/data/models/deriv_passkeys_verify_credentials_response_model.dart'; +import 'package:deriv_passkeys/src/data/models/passkeys_connection_info_model.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkeys_register_credentials_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkeys_verify_credentials_request_body_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/passkeys_connection_info_entity.dart'; +import 'package:flutter_deriv_api/basic_api/generated/passkeys_register_send.dart'; +import 'package:mocktail/mocktail.dart'; + +/// Mock class for [DerivPasskeysDataSource]. +class MockDerivPasskeysDataSource extends Mock + implements DerivPasskeysDataSource { + @override + final DerivPasskeysMapper mapper = DerivPasskeysMapper(); +} + +final PasskeysConnectionInfoEntity passkeysConnectionInfoEntity = + PasskeysConnectionInfoEntity(endpoint: 'deriv.com', appId: '1234'); + +final PasskeysConnectionInfoModel passkeysConnectionInfoModel = + PasskeysConnectionInfoModel( + endpoint: 'deriv.com', + appId: '1234', +); + +const DerivPasskeysVerifyCredentialsRequestBodyEntity + derivPasskeysVerifyCredentialsRequestBodyEntity = + DerivPasskeysVerifyCredentialsRequestBodyEntity( + appId: '1234', + publicKeyCredential: {}, + type: '', +); + +final DerivPasskeysVerifyCredentialsRequest + derivPasskeysVerifyCredentialsRequest = + DerivPasskeysVerifyCredentialsRequest( + appId: '1234', + publicKeyCredential: {}, + type: '', +); + +const String jwtToken = 'jwtToken'; +const String userAgent = 'Dart/3.0 (dart:io)'; + +const PasskeysRegisterRequest passkeysRegisterRequest = PasskeysRegisterRequest( + name: '', + publicKeyCredential: {}, +); + +const DerivPasskeysRegisterCredentialsEntity registerCredentialsEntity = + DerivPasskeysRegisterCredentialsEntity( + publicKeyCredential: {}, + name: '', +); +const DerivPasskeyModel derivPasskeyModel = DerivPasskeyModel( + createdAt: 1234, + id: '', + lastUsed: null, + name: '', + passkeyId: '', + storedOn: '', +); + +DerivPasskeysRegisterOptionsModel derivPasskeysRegisterOptionsModel = + DerivPasskeysRegisterOptionsModel( + options: { + 'rp': { + 'id': 'id', + 'name': 'name', + }, + 'user': { + 'id': 'id', + 'name': 'name', + 'displayName': 'displayName', + }, + 'challenge': 'challenge', + 'pubKeyCredParams': [ + { + 'type': 'type', + 'alg': -1, + } + ], + 'timeout': 18000, + 'attestation': 'attestation', + 'extensions': {}, + }, +); + +const DerivPasskeysVerifyCredentialsResponseModel + derivPasskeysVerifyCredentialsResponseModel = + DerivPasskeysVerifyCredentialsResponseModel( + response: { + 'tokens': >[ + { + 'token': 'token', + }, + ] + }, +); + +final DerivPasskeysOptionsModel derivPasskeysOptionsModel = + DerivPasskeysOptionsModel( + challenge: '', + rpId: '', + timeout: 1234, + userVerification: '', + allowCredentials: [], +); + +/// Sets up the mock data for [DerivPasskeysDataSource]. +void derivPasskeysDataSourceMockSetup() { + registerFallbackValue(derivPasskeysVerifyCredentialsRequest); + registerFallbackValue(derivPasskeysVerifyCredentialsRequestBodyEntity); + registerFallbackValue(passkeysConnectionInfoModel); + registerFallbackValue(jwtToken); + registerFallbackValue(userAgent); + registerFallbackValue(passkeysRegisterRequest); +} diff --git a/packages/deriv_passkeys/test/data/deriv_passkeys_data_source_test.dart b/packages/deriv_passkeys/test/data/deriv_passkeys_data_source_test.dart new file mode 100644 index 000000000..2aaf2657b --- /dev/null +++ b/packages/deriv_passkeys/test/data/deriv_passkeys_data_source_test.dart @@ -0,0 +1,211 @@ +import 'dart:convert'; + +import 'package:deriv_http_client/deriv_http_client.dart'; +import 'package:deriv_passkeys/deriv_passkeys.dart'; +import 'package:deriv_passkeys/src/data/models/deriv_passkey_model.dart'; +import 'package:deriv_passkeys/src/data/models/deriv_passkeys_options_model.dart'; +import 'package:deriv_passkeys/src/data/models/deriv_passkeys_register_options_model.dart'; +import 'package:deriv_passkeys/src/data/models/deriv_passkeys_verify_credentials_request_body_model.dart'; +import 'package:deriv_passkeys/src/data/models/deriv_passkeys_verify_credentials_response_model.dart'; +import 'package:deriv_passkeys/src/data/models/passkeys_connection_info_model.dart'; +import 'package:deriv_passkeys/src/exceptions/server_exceptions.dart'; +import 'package:flutter_deriv_api/api/api_initializer.dart'; +import 'package:flutter_deriv_api/basic_api/generated/passkeys_register_send.dart'; +import 'package:flutter_deriv_api/services/connection/api_manager/mock_api.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:http/http.dart'; + +Future _mockHTTP(Request request) async { + if (request.url.host.contains('error')) { + if (request.url.host == 'error_code_exists') { + return Response( + jsonEncode({ + 'error_code': 'error_code_exists', + 'message': 'message', + }), + 500, + ); + } + if (request.url.host == 'no_error_code') { + return Response( + jsonEncode({ + 'message': 'message', + }), + 500, + ); + } + } + switch (request.url.path) { + case '/oauth2/api/v1/passkeys/login/options': + if (request.method == 'GET') { + return Response( + jsonEncode({ + 'publicKey': { + 'challenge': 'jV6lvuj1d-iVSPiQQg9iwXhiSTCm3CueJ7aLcv2GfVc', + 'rpId': 'deriv.com', + 'timeout': 60000, + 'userVerification': 'preferred' + } + }), + 200, + ); + } + break; + case '/oauth2/api/v1/passkeys/login/verify': + if (request.method == 'POST') { + return Response( + jsonEncode({}), + 200, + ); + } + break; + } + return Response( + jsonEncode({ + 'error_code': 'error_code', + 'message': 'message', + }), + 400, + ); +} + +class FakeHttpClient extends Fake implements HttpClient { + @override + Future get(String url, {String? basicAuthToken}) async => + _mockHTTP(Request('GET', Uri.parse(url))); + + @override + Future> post({ + required String url, + required Map jsonBody, + Map? headers, + }) async { + final Response response = await _mockHTTP(Request('POST', Uri.parse(url))); + return jsonDecode(response.body) as Map; + } +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late DerivPasskeysDataSource dataSource; + final HttpClient mockClient = FakeHttpClient(); + final PasskeysConnectionInfoModel passkeysConnectionInfoModel = + PasskeysConnectionInfoModel( + endpoint: 'deriv.com', + appId: 'appId', + ); + + setUpAll(() { + APIInitializer().initialize(api: MockAPI()); + dataSource = DerivPasskeysDataSource( + mapper: DerivPasskeysMapper(), + client: mockClient, + ); + }); + + group('getOptions', () { + test('should return DerivPasskeysOptionsModel when request is successful', + () async { + expect( + dataSource.getOptions( + passkeysConnectionInfoModel: passkeysConnectionInfoModel, + ), + completion(isA())); + }); + + test( + 'should throw ServerException when request is unsuccessful and response body contains "error_code".', + () async { + expect( + () => dataSource.getOptions( + passkeysConnectionInfoModel: passkeysConnectionInfoModel.copyWith( + endpoint: 'error_code_exists', + ), + ), + throwsA(isA()), + ); + }); + + test( + 'should throw Exception when request is unsuccessful and response body does not contain "error_code".', + () async { + expect( + () => dataSource.getOptions( + passkeysConnectionInfoModel: passkeysConnectionInfoModel.copyWith( + endpoint: 'no_error_code', + ), + ), + throwsA(isA()), + ); + }); + }); + + group('verifyCredentials', () { + final DerivPasskeysVerifyCredentialsRequest + derivPasskeysVerifyCredentialsRequest = + DerivPasskeysVerifyCredentialsRequest( + appId: 'appId', + publicKeyCredential: {}, + type: 'type', + ); + test( + 'should return DerivPasskeysVerifyCredentialsResponseModel when request is successful', + () async { + expect( + dataSource.verifyCredentials( + requestBodyModel: derivPasskeysVerifyCredentialsRequest, + jwtToken: 'jwtToken', + passkeysConnectionInfoModel: passkeysConnectionInfoModel, + ), + completion(isA())); + }); + + test( + 'should throw ServerException when request is unsuccessful and response body contains "error_code".', + () async { + expect( + () => dataSource.verifyCredentials( + requestBodyModel: derivPasskeysVerifyCredentialsRequest, + jwtToken: 'jwtToken', + passkeysConnectionInfoModel: passkeysConnectionInfoModel.copyWith( + endpoint: 'error_code_exists', + ), + ), + throwsA(isA()), + ); + }); + }); + + group('getRegisterOptions', () { + test( + 'should return DerivPasskeysRegisterOptionsModel when request is successful', + () async { + expect(dataSource.getRegisterOptions(), + completion(isA())); + }); + }); + + group('registerCredentials', () { + test('should return DerivPasskeyModel when request is successful', + () async { + expect( + dataSource.registerCredentials( + const PasskeysRegisterRequest( + publicKeyCredential: {}, + name: '', + ), + ), + completion(isA())); + }); + }); + + group('getPasskeysList', () { + test('should return List when request is successful', + () async { + expect(dataSource.getPasskeysList(), + completion(isA>())); + }); + }); +} diff --git a/packages/deriv_passkeys/test/data/deriv_passkeys_mapper_test.dart b/packages/deriv_passkeys/test/data/deriv_passkeys_mapper_test.dart new file mode 100644 index 000000000..76968b9ea --- /dev/null +++ b/packages/deriv_passkeys/test/data/deriv_passkeys_mapper_test.dart @@ -0,0 +1,132 @@ +import 'package:deriv_passkeys/src/data/models/passkeys_connection_info_model.dart'; +import 'package:deriv_passkeys/src/data/models/deriv_passkey_model.dart'; +import 'package:deriv_passkeys/src/data/models/deriv_passkeys_options_model.dart'; +import 'package:deriv_passkeys/src/data/models/deriv_passkeys_register_options_model.dart'; +import 'package:deriv_passkeys/src/data/models/deriv_passkeys_verify_credentials_request_body_model.dart'; +import 'package:deriv_passkeys/src/data/models/deriv_passkeys_verify_credentials_response_model.dart'; +import 'package:deriv_passkeys/src/domain/entities/passkeys_connection_info_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkey_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkeys_options_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkeys_register_credentials_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkeys_register_options_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkeys_verify_credentials_request_body_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkeys_verify_credentials_response_entity.dart'; +import 'package:flutter_deriv_api/basic_api/generated/passkeys_register_send.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:deriv_passkeys/src/data/mappers/deriv_passkeys_mapper.dart'; + +void main() { + group('DerivPasskeysMapper', () { + late DerivPasskeysMapper mapper; + + setUp(() { + mapper = DerivPasskeysMapper(); + }); + + test( + 'mapDerivPasskeysOptionsModel should return DerivPasskeysOptionsEntity', + () { + final DerivPasskeysOptionsModel model = DerivPasskeysOptionsModel( + challenge: '', + rpId: '', + timeout: 12345, + userVerification: '', + allowCredentials: []); // Provide necessary data for the model + + final DerivPasskeysOptionsEntity result = + mapper.mapDerivPasskeysOptionsModel(model); + + expect(result, isA()); + }); + + test( + 'mapDerivPasskeysVerifyCredentialsRequestBodyEntity should return DerivPasskeysVerifyCredentialsRequest', + () { + const DerivPasskeysVerifyCredentialsRequestBodyEntity entity = + DerivPasskeysVerifyCredentialsRequestBodyEntity( + appId: '', publicKeyCredential: {}, type: ''); + + final DerivPasskeysVerifyCredentialsRequest result = + mapper.mapDerivPasskeysVerifyCredentialsRequestBodyEntity(entity); + + expect(result, isA()); + }); + + test( + 'mapDerivPasskeysRegisterOptionsModel should return DerivPasskeysRegisterOptionsEntity', + () { + final DerivPasskeysRegisterOptionsModel model = + DerivPasskeysRegisterOptionsModel(options: {}); // Provide necessary data for the model + + final DerivPasskeysRegisterOptionsEntity result = + mapper.mapDerivPasskeysRegisterOptionsModel(model); + + expect(result, isA()); + }); + + test('mapDerivPasskeyModel should return DerivPasskeyEntity', () { + const DerivPasskeyModel model = DerivPasskeyModel( + createdAt: 1234, + id: '', + lastUsed: null, + name: '', + passkeyId: '', + storedOn: '', + ); + + final DerivPasskeyEntity result = mapper.mapDerivPasskeyModel(model); + + expect(result, isA()); + }); + + test( + 'mapDerivPasskeysRegisterCredentialsEntity should return PasskeysRegisterRequest', + () { + const DerivPasskeysRegisterCredentialsEntity entity = + DerivPasskeysRegisterCredentialsEntity( + publicKeyCredential: {}, + name: '', + ); + + final PasskeysRegisterRequest result = + mapper.mapDerivPasskeysRegisterCredentialsEntity(entity); + + expect(result, isA()); + }); + + test( + 'mapDerivPasskeysVerifyCredentialsResponseModel should return DerivPasskeysVerifyCredentialsResponseEntity', + () { + const DerivPasskeysVerifyCredentialsResponseModel model = + DerivPasskeysVerifyCredentialsResponseModel( + response: { + 'tokens': [ + { + 'token': '', + } + ] + }, + ); + + final DerivPasskeysVerifyCredentialsResponseEntity result = + mapper.mapDerivPasskeysVerifyCredentialsResponseModel(model); + + expect(result, isA()); + }); + + test('mapConnectionInfoEntity should return PasskeysConnectionInfoModel', + () { + final PasskeysConnectionInfoEntity entity = PasskeysConnectionInfoEntity( + endpoint: '', + appId: '', + ); + + final PasskeysConnectionInfoModel result = + mapper.mapConnectionInfoEntity(entity); + + expect(result, isA()); + }); + }); +} diff --git a/packages/deriv_passkeys/test/data/deriv_passkeys_method_channel_test.dart b/packages/deriv_passkeys/test/data/deriv_passkeys_method_channel_test.dart new file mode 100644 index 000000000..df6e20fb1 --- /dev/null +++ b/packages/deriv_passkeys/test/data/deriv_passkeys_method_channel_test.dart @@ -0,0 +1,147 @@ +import 'package:deriv_passkeys/src/data/platform/deriv_passkeys_method_channel.dart'; +import 'package:deriv_passkeys/src/exceptions/platform_exceptions.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class MockMethodChannel extends MethodChannel { + MockMethodChannel() : super('deriv_passkeys'); + + Future handler(MethodCall methodCall) async => + invokeMethod(methodCall.method, methodCall.arguments); + + @override + Future invokeMethod(String method, [dynamic arguments]) async { + switch (method) { + case 'isPlatformSupported': + return true as T?; + case 'createCredential': + final String options = arguments['options']; + if (options == 'valid_options') { + return 'credential created' as T?; + } else if (options == 'user_cancelled') { + throw PlatformException( + code: 'CreateCredentialCancellationException', + message: 'CreateCredentialCancellationException', + ); + } else { + throw PlatformException( + code: 'invalid_argument', + message: 'Invalid options provided', + ); + } + case 'getCredential': + final String options = arguments['options']; + if (options == 'valid_options') { + return 'credential retrieved' as T?; + } else if (options == 'user_cancelled') { + throw PlatformException( + code: 'GetCredentialCancellationException', + message: 'GetCredentialCancellationException', + ); + } else if (options == 'no_credential') { + throw PlatformException( + code: 'NoCredentialException', + message: 'GetCredentialCancellationException', + ); + } else { + throw PlatformException( + code: 'invalid_argument', + message: 'Invalid options provided', + ); + } + default: + throw PlatformException( + code: 'method_not_implemented', + message: 'Method not implemented on platform', + ); + } + } +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late MethodChannelDerivPasskeys platform; + late MockMethodChannel mockChannel; + + setUp(() { + mockChannel = MockMethodChannel(); + platform = MethodChannelDerivPasskeys(); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + mockChannel, + mockChannel.handler, + ); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(mockChannel, null); + }); + + test('isSupported', () async { + expect(await platform.isPlatformSupported(), isTrue); + }); + + test('createCredential with valid options', () async { + const String options = 'valid_options'; + final String? response = await platform.createCredential(options); + expect(response, 'credential created'); + }); + + test('createCredential with user cancelled throws CanceledPlatformException', + () { + const String options = 'user_cancelled'; + expect( + () => platform.createCredential(options), + throwsA(isA()), + ); + }); + + test('createCredential with invalid options throws PlatformException', () { + const String options = 'invalid options'; + expect( + () => platform.createCredential(options), + throwsA(isA()), + ); + }); + + test('getCredential with valid options', () async { + const String options = 'valid_options'; + final String? response = await platform.getCredential(options); + expect(response, 'credential retrieved'); + }); + + test('getCredential with user cancelled throws CanceledPlatformException', + () { + const String options = 'user_cancelled'; + expect( + () => platform.getCredential(options), + throwsA(isA()), + ); + }); + + test('getCredential with no credential throws NoCredentialPlatformException', + () { + const String options = 'no_credential'; + expect( + () => platform.getCredential(options), + throwsA(isA()), + ); + }); + + test('getCredential with invalid options throws PlatformException', () { + const String options = 'invalid options'; + expect( + () => platform.getCredential(options), + throwsA(isA()), + ); + }); + + test('Method not implemented throws PlatformException', () async { + expect( + () => platform.methodChannel.invokeMethod('nonExistentMethod'), + throwsA(isA()), + ); + }); +} diff --git a/packages/deriv_passkeys/test/data/deriv_passkeys_repository_test.dart b/packages/deriv_passkeys/test/data/deriv_passkeys_repository_test.dart new file mode 100644 index 000000000..2375042f9 --- /dev/null +++ b/packages/deriv_passkeys/test/data/deriv_passkeys_repository_test.dart @@ -0,0 +1,189 @@ +import 'package:deriv_passkeys/deriv_passkeys.dart'; +import 'package:deriv_passkeys/src/data/models/deriv_passkey_model.dart'; +import 'package:deriv_passkeys/src/data/models/deriv_passkeys_register_options_model.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkey_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkeys_options_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkeys_register_options_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkeys_verify_credentials_response_entity.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import 'deriv_passkeys_data_source_mock_setup.dart'; + +void main() { + late DerivPasskeysRepository repository; + late MockDerivPasskeysDataSource mockDerivPasskeysDataSource; + + setUpAll(() { + derivPasskeysDataSourceMockSetup(); + mockDerivPasskeysDataSource = MockDerivPasskeysDataSource(); + repository = DerivPasskeysRepository(mockDerivPasskeysDataSource); + }); + + group('getOptions', () { + test('should return DerivPasskeysOptionsEntity', () async { + when(() => mockDerivPasskeysDataSource.getOptions( + passkeysConnectionInfoModel: + any(named: 'passkeysConnectionInfoModel'), + )).thenAnswer( + (_) async => derivPasskeysOptionsModel, + ); + + final DerivPasskeysOptionsEntity result = await repository.getOptions( + passkeysConnectionInfoEntity: passkeysConnectionInfoEntity, + ); + + expect( + result, + equals( + mockDerivPasskeysDataSource.mapper.mapDerivPasskeysOptionsModel( + derivPasskeysOptionsModel, + ), + ), + ); + verify(() => mockDerivPasskeysDataSource.getOptions( + passkeysConnectionInfoModel: + any(named: 'passkeysConnectionInfoModel'), + )).called(1); + }); + }); + + group('verifyCredentials', () { + test('should return DerivPasskeysVerifyCredentialsResponseEntity', + () async { + when(() => mockDerivPasskeysDataSource.verifyCredentials( + requestBodyModel: any(named: 'requestBodyModel'), + jwtToken: any(named: 'jwtToken'), + passkeysConnectionInfoModel: + any(named: 'passkeysConnectionInfoModel'), + userAgent: any(named: 'userAgent'), + )) + .thenAnswer((_) async => derivPasskeysVerifyCredentialsResponseModel); + + final DerivPasskeysVerifyCredentialsResponseEntity result = + await repository.verifyCredentials( + requestBodyEntity: derivPasskeysVerifyCredentialsRequestBodyEntity, + jwtToken: jwtToken, + passkeysConnectionInfoEntity: passkeysConnectionInfoEntity, + ); + + expect( + result, + equals( + mockDerivPasskeysDataSource.mapper + .mapDerivPasskeysVerifyCredentialsResponseModel( + derivPasskeysVerifyCredentialsResponseModel, + ), + ), + ); + verify(() => mockDerivPasskeysDataSource.verifyCredentials( + requestBodyModel: any(named: 'requestBodyModel'), + jwtToken: any(named: 'jwtToken'), + passkeysConnectionInfoModel: + any(named: 'passkeysConnectionInfoModel'), + userAgent: any(named: 'userAgent'), + )).called(1); + }); + }); + + group('getRegisterOptions', () { + test('should return DerivPasskeysRegisterOptionsEntity', () async { + final DerivPasskeysRegisterOptionsModel + derivPasskeysRegisterOptionsModel = DerivPasskeysRegisterOptionsModel( + options: {}, + ); + when(() => mockDerivPasskeysDataSource.getRegisterOptions()) + .thenAnswer((_) async => derivPasskeysRegisterOptionsModel); + + final DerivPasskeysRegisterOptionsEntity result = + await repository.getRegisterOptions(); + + expect( + result, + equals( + mockDerivPasskeysDataSource.mapper + .mapDerivPasskeysRegisterOptionsModel( + derivPasskeysRegisterOptionsModel, + ), + ), + ); + verify(() => mockDerivPasskeysDataSource.getRegisterOptions()).called(1); + }); + }); + + group('registerCredentials', () { + test('should return DerivPasskeyEntity', () async { + when(() => mockDerivPasskeysDataSource.registerCredentials( + any(), + )).thenAnswer((_) async => derivPasskeyModel); + + final DerivPasskeyEntity result = + await repository.registerCredentials(registerCredentialsEntity); + + expect( + result, + equals( + mockDerivPasskeysDataSource.mapper.mapDerivPasskeyModel( + derivPasskeyModel, + ), + ), + ); + verify(() => mockDerivPasskeysDataSource.registerCredentials( + any(), + )).called(1); + }); + }); + + group('getPasskeysList', () { + test('should return List', () async { + const DerivPasskeyModel passkeyModel1 = DerivPasskeyModel( + createdAt: 1234, + id: '1', + lastUsed: null, + name: '', + passkeyId: '', + storedOn: '', + ); + const DerivPasskeyModel passkeyModel2 = DerivPasskeyModel( + createdAt: 1234, + id: '2', + lastUsed: null, + name: '', + passkeyId: '', + storedOn: '', + ); + const DerivPasskeyEntity passkeyEntity1 = DerivPasskeyEntity( + createdAt: 1234, + id: '1', + lastUsed: null, + name: '', + passkeyId: '', + storedOn: '', + ); + const DerivPasskeyEntity passkeyEntity2 = DerivPasskeyEntity( + createdAt: 1234, + id: '2', + lastUsed: null, + name: '', + passkeyId: '', + storedOn: '', + ); + final List passkeyModels = [ + passkeyModel1, + passkeyModel2 + ]; + final List passkeyEntities = [ + passkeyEntity1, + passkeyEntity2 + ]; + when(() => mockDerivPasskeysDataSource.getPasskeysList()) + .thenAnswer((_) async => passkeyModels); + + final List result = + await repository.getPasskeysList(); + + expect(result, equals(passkeyEntities)); + verify(() => mockDerivPasskeysDataSource.getPasskeysList()).called(1); + }); + }); +} diff --git a/packages/deriv_passkeys/test/interactor/deriv_passkeys_service_test.dart b/packages/deriv_passkeys/test/interactor/deriv_passkeys_service_test.dart new file mode 100644 index 000000000..43436d3cd --- /dev/null +++ b/packages/deriv_passkeys/test/interactor/deriv_passkeys_service_test.dart @@ -0,0 +1,149 @@ +import 'package:deriv_passkeys/deriv_passkeys.dart'; +import 'package:deriv_passkeys/src/data/models/deriv_passkey_model.dart'; +import 'package:deriv_passkeys/src/data/platform/deriv_passkeys_method_channel.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkey_entity.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkeys_verify_credentials_response_entity.dart'; +import 'package:deriv_passkeys/src/domain/platform/base_deriv_passkeys_method_channel.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import '../data/deriv_passkeys_data_source_mock_setup.dart'; + +class MockBaseDerivPasskeysMethodChannel + with MockPlatformInterfaceMixin + implements BaseDerivPasskeysMethodChannel { + Future Function(String options)? mockCreateCredential; + Future Function(String options)? mockGetCredential; + + @override + Future isPlatformSupported() => Future.value(true); + + @override + Future createCredential(String options) => + mockCreateCredential != null + ? mockCreateCredential!(options) + : Future.value('{}'); + + @override + Future getCredential(String options) => mockGetCredential != null + ? mockGetCredential!(options) + : Future.value('{}'); +} + +void main() { + final BaseDerivPasskeysMethodChannel initialPlatform = + BaseDerivPasskeysMethodChannel.instance; + + test('$MethodChannelDerivPasskeys is the default instance', () { + expect(initialPlatform, isInstanceOf()); + }); + + group('DerivPasskeys', () { + late DerivPasskeysService derivPasskeysService; + final MockDerivPasskeysDataSource mockDerivPasskeysDataSource = + MockDerivPasskeysDataSource(); + + setUp(() { + derivPasskeysDataSourceMockSetup(); + derivPasskeysService = DerivPasskeysService( + DerivPasskeysRepository( + mockDerivPasskeysDataSource, + ), + ); + BaseDerivPasskeysMethodChannel.instance = + MockBaseDerivPasskeysMethodChannel(); + }); + + tearDown(() { + BaseDerivPasskeysMethodChannel.instance = initialPlatform; + }); + + test('isSupported returns true', () async { + final bool isSupported = await derivPasskeysService.isSupported(); + expect(isSupported, true); + }); + + test('createCredential returns response if not null', () async { + when(() => mockDerivPasskeysDataSource.getRegisterOptions()) + .thenAnswer((_) async => derivPasskeysRegisterOptionsModel); + when(() => mockDerivPasskeysDataSource.registerCredentials( + any(), + )).thenAnswer((_) async => derivPasskeyModel); + final DerivPasskeyEntity response = + await derivPasskeysService.createCredential(); + expect(response, isNotNull); + expect(response, isA()); + }); + + test('createCredential throws PlatformException if response is null', + () async { + when(() => mockDerivPasskeysDataSource.getRegisterOptions()) + .thenAnswer((_) async => derivPasskeysRegisterOptionsModel); + + BaseDerivPasskeysMethodChannel + .instance = MockBaseDerivPasskeysMethodChannel() + ..mockCreateCredential = (String options) => Future.value(); + expect( + () => derivPasskeysService.createCredential(), + throwsA(isA()), + ); + }); + + test('verifyCredential returns response if not null', () async { + when(() => mockDerivPasskeysDataSource.getOptions( + passkeysConnectionInfoModel: + any(named: 'passkeysConnectionInfoModel'), + )).thenAnswer( + (_) async => derivPasskeysOptionsModel, + ); + when(() => mockDerivPasskeysDataSource.verifyCredentials( + requestBodyModel: any(named: 'requestBodyModel'), + jwtToken: any(named: 'jwtToken'), + passkeysConnectionInfoModel: + any(named: 'passkeysConnectionInfoModel'), + userAgent: any(named: 'userAgent'), + )) + .thenAnswer((_) async => derivPasskeysVerifyCredentialsResponseModel); + final DerivPasskeysVerifyCredentialsResponseEntity response = + await derivPasskeysService.verifyCredential( + jwtToken: jwtToken, + passkeysConnectionInfoEntity: passkeysConnectionInfoEntity, + ); + expect(response, isNotNull); + expect(response, isA()); + }); + + test('verifyCredential throws PlatformException if response is null', + () async { + when(() => mockDerivPasskeysDataSource.getOptions( + passkeysConnectionInfoModel: + any(named: 'passkeysConnectionInfoModel'), + )).thenAnswer( + (_) async => derivPasskeysOptionsModel, + ); + BaseDerivPasskeysMethodChannel.instance = + MockBaseDerivPasskeysMethodChannel() + ..mockGetCredential = (String options) => Future.value(); + + expect( + () => derivPasskeysService.verifyCredential( + jwtToken: jwtToken, + passkeysConnectionInfoEntity: passkeysConnectionInfoEntity, + ), + throwsA(isInstanceOf())); + }); + test('getPasskeysList returns response if not null', () async { + when(() => mockDerivPasskeysDataSource.getPasskeysList()) + .thenAnswer((_) async => [derivPasskeyModel]); + final List response = await DerivPasskeysService( + DerivPasskeysRepository( + mockDerivPasskeysDataSource, + ), + ).getPasskeysList(); + expect(response, isNotNull); + expect(response, isA>()); + }); + }); +} diff --git a/packages/deriv_passkeys/test/presentation/pages/learn_more_passkeys_page_test.dart b/packages/deriv_passkeys/test/presentation/pages/learn_more_passkeys_page_test.dart new file mode 100644 index 000000000..b6ae89df7 --- /dev/null +++ b/packages/deriv_passkeys/test/presentation/pages/learn_more_passkeys_page_test.dart @@ -0,0 +1,104 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:deriv_localizations/l10n/generated/deriv_passkeys/deriv_passkeys_localizations.dart'; +import 'package:deriv_localizations/l10n/generated/deriv_passkeys/deriv_passkeys_localizations_en.dart'; +import 'package:deriv_passkeys/deriv_passkeys.dart'; +import 'package:deriv_passkeys/src/presentation/widgets/section_title_and_content.dart'; +import 'package:deriv_passkeys/src/presentation/widgets/unordered_list_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:deriv_passkeys/src/presentation/pages/learn_more_passkeys_page.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockDerivPasskeysBloc + extends MockBloc + implements DerivPasskeysBloc {} + +class _TestPage extends StatelessWidget { + const _TestPage(); + + @override + Widget build(BuildContext context) => MaterialApp( + localizationsDelegates: const >[ + DerivPasskeysLocalizations.delegate, + ], + locale: const Locale('en'), + home: LearnMorePasskeysPage( + onPageClosed: (BuildContext context) { + Navigator.pop(context); + }, + addMorePasskeysNavigationCallback: (BuildContext context) {}, + continueTradingNavigationCallback: (BuildContext context) {}, + ), + ); +} + +void main() { + group('LearnMorePasskeysPage', () { + late MockDerivPasskeysBloc derivPasskeysBloc; + setUp(() { + derivPasskeysBloc = MockDerivPasskeysBloc(); + + when(() => derivPasskeysBloc.state).thenReturn( + DerivPasskeysInitializedState(), + ); + }); + + testWidgets('renders page correctly', (WidgetTester tester) async { + await tester.pumpWidget( + BlocProvider( + create: (BuildContext context) => derivPasskeysBloc, + child: const _TestPage(), + ), + ); + + expect(find.byType(AppBar), findsOneWidget); + expect(find.byType(SingleChildScrollView), findsOneWidget); + expect(find.byType(SvgPicture), findsNWidgets(2)); + expect(find.byType(SectionTitleAndContent), findsNWidgets(5)); + expect(find.byType(Divider), findsNWidgets(4)); + expect(find.byType(UnorderedList), findsNWidgets(6)); + }); + + testWidgets('displays correct text', (WidgetTester tester) async { + await tester.pumpWidget( + BlocProvider( + create: (BuildContext context) => derivPasskeysBloc, + child: const _TestPage(), + ), + ); + + // Verify that the page displays the correct text + expect(find.text(DerivPasskeysLocalizationsEn().effortlessLogin), + findsOneWidget); + expect(find.text(DerivPasskeysLocalizationsEn().whatArePasskeys), + findsOneWidget); + expect(find.text(DerivPasskeysLocalizationsEn().whyPasskeys), + findsOneWidget); + expect(find.text(DerivPasskeysLocalizationsEn().howToCreatePasskey), + findsOneWidget); + expect(find.text(DerivPasskeysLocalizationsEn().whereArePasskeysSaved), + findsOneWidget); + expect( + find.text(DerivPasskeysLocalizationsEn().whatHappensIfEmailChanged), + findsOneWidget); + expect(find.textContaining(DerivPasskeysLocalizationsEn().tips), + findsOneWidget); + expect( + find.textContaining( + DerivPasskeysLocalizationsEn().beforeUsingPasskeys), + findsOneWidget); + expect( + find.textContaining(DerivPasskeysLocalizationsEn().enableScreenLock), + findsOneWidget); + expect( + find.textContaining( + DerivPasskeysLocalizationsEn().signInGoogleOrIcloud), + findsOneWidget); + expect( + find.textContaining(DerivPasskeysLocalizationsEn().enableBluetooth), + findsOneWidget); + }); + }); +} diff --git a/packages/deriv_passkeys/test/presentation/states/bloc/deriv_passkeys_bloc_setup.dart b/packages/deriv_passkeys/test/presentation/states/bloc/deriv_passkeys_bloc_setup.dart new file mode 100644 index 000000000..f9e4c298b --- /dev/null +++ b/packages/deriv_passkeys/test/presentation/states/bloc/deriv_passkeys_bloc_setup.dart @@ -0,0 +1,38 @@ +import 'package:deriv_passkeys/deriv_passkeys.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkeys_verify_credentials_response_entity.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockDerivPasskeysService extends Mock implements DerivPasskeysService {} + +class MockPasskeysConnectionInfoEntity extends Mock + implements PasskeysConnectionInfoEntity {} + +late DerivPasskeysBloc derivPasskeysBloc; +late MockDerivPasskeysService mockDerivPasskeysService; +late MockPasskeysConnectionInfoEntity mockPasskeysConnectionInfoEntity; + +void setupDerivPasskeysBloc() { + mockDerivPasskeysService = MockDerivPasskeysService(); + mockPasskeysConnectionInfoEntity = MockPasskeysConnectionInfoEntity(); + when(() => mockDerivPasskeysService.isSupported()) + .thenAnswer((_) async => true); + derivPasskeysBloc = DerivPasskeysBloc( + derivPasskeysService: mockDerivPasskeysService, + connectionInfo: mockPasskeysConnectionInfoEntity, + getJwtToken: () async => 'jwtToken', + ); + registerFallbackValue(mockPasskeysConnectionInfoEntity); + registerFallbackValue(() async => 'userAgent'); +} + +void setupSuccessDerivPasskeysVerifyCredentialEvent() { + const DerivPasskeysVerifyCredentialsResponseEntity mockResponseEntity = + DerivPasskeysVerifyCredentialsResponseEntity(token: 'token'); + + when(() => mockDerivPasskeysService.verifyCredential( + jwtToken: any(named: 'jwtToken'), + passkeysConnectionInfoEntity: + any(named: 'passkeysConnectionInfoEntity'), + userAgent: any(named: 'userAgent'), + )).thenAnswer((_) async => mockResponseEntity); +} diff --git a/packages/deriv_passkeys/test/presentation/states/bloc/deriv_passkeys_bloc_test.dart b/packages/deriv_passkeys/test/presentation/states/bloc/deriv_passkeys_bloc_test.dart new file mode 100644 index 000000000..2b62ec867 --- /dev/null +++ b/packages/deriv_passkeys/test/presentation/states/bloc/deriv_passkeys_bloc_test.dart @@ -0,0 +1,271 @@ +import 'package:deriv_passkeys/src/domain/entities/deriv_passkey_entity.dart'; +import 'package:deriv_passkeys/src/exceptions/platform_exceptions.dart'; +import 'package:deriv_passkeys/src/exceptions/server_exceptions.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:deriv_passkeys/deriv_passkeys.dart'; + +import 'deriv_passkeys_bloc_setup.dart'; + +void main() { + setUp(() { + setupDerivPasskeysBloc(); + }); + + group('DerivPasskeysBloc', () { + test('initial state should be DerivPasskeysInitializedState', () { + expect(derivPasskeysBloc.state, isA()); + }); + + test( + 'initial state should be DerivPasskeysNotSupportedState if not supported', + () async { + when(() => mockDerivPasskeysService.isSupported()) + .thenAnswer((_) async => Future(() => false)); + final DerivPasskeysBloc unsupportedDerivPasskeysBloc = DerivPasskeysBloc( + derivPasskeysService: mockDerivPasskeysService, + connectionInfo: mockPasskeysConnectionInfoEntity, + getJwtToken: () async => 'jwtToken', + ); + await expectLater( + unsupportedDerivPasskeysBloc.stream, + emits( + isA(), + ), + ); + }); + + test( + 'SetDerivPasskeysNotSupportedEvent should emit DerivPasskeysNotSupportedState', + () { + expectLater( + derivPasskeysBloc.stream, + emits(isA()), + ); + + derivPasskeysBloc.add(const SetDerivPasskeysNotSupportedEvent()); + }); + + test( + 'DerivPasskeysVerifyCredentialEvent should emit DerivPasskeysCredentialVerifiedState', + () { + setupSuccessDerivPasskeysVerifyCredentialEvent(); + + expectLater( + derivPasskeysBloc.stream, + emitsInOrder(>[ + isA(), + isA(), + ]), + ); + + derivPasskeysBloc.add(DerivPasskeysVerifyCredentialEvent()); + }); + + test( + 'DerivPasskeysVerifyCredentialEvent should emit DerivPasskeysLoadedState if CanceledPlatformException is thrown', + () { + when(() => mockDerivPasskeysService.verifyCredential( + jwtToken: any(named: 'jwtToken'), + passkeysConnectionInfoEntity: + any(named: 'passkeysConnectionInfoEntity'), + userAgent: any(named: 'userAgent'), + )).thenAnswer((_) async => throw CanceledPlatformException( + code: '', + message: '', + details: '', + )); + + expectLater( + derivPasskeysBloc.stream, + emitsInOrder(>[ + isA(), + isA(), + ]), + ); + + derivPasskeysBloc.add(DerivPasskeysVerifyCredentialEvent()); + }); + + test( + 'DerivPasskeysVerifyCredentialEvent should emit NoCredentialErrorState if NoCredentialPlatformException is thrown', + () { + when(() => mockDerivPasskeysService.verifyCredential( + jwtToken: any(named: 'jwtToken'), + passkeysConnectionInfoEntity: + any(named: 'passkeysConnectionInfoEntity'), + userAgent: any(named: 'userAgent'), + )).thenAnswer((_) async => throw NoCredentialPlatformException( + code: '', + message: '', + details: '', + )); + + expectLater( + derivPasskeysBloc.stream, + emitsInOrder(>[ + isA(), + isA(), + ]), + ); + + derivPasskeysBloc.add(DerivPasskeysVerifyCredentialEvent()); + }); + + test( + 'DerivPasskeysVerifyCredentialEvent should emit DerivPasskeysErrorState if ServerException is thrown', + () { + when(() => mockDerivPasskeysService.verifyCredential( + jwtToken: any(named: 'jwtToken'), + passkeysConnectionInfoEntity: + any(named: 'passkeysConnectionInfoEntity'), + userAgent: any(named: 'userAgent'), + )).thenAnswer((_) async => throw ServerException( + errorCode: '', + message: '', + )); + + expectLater( + derivPasskeysBloc.stream, + emitsInOrder(>[ + isA(), + isA(), + ]), + ); + + derivPasskeysBloc.add(DerivPasskeysVerifyCredentialEvent()); + }); + + test( + 'DerivPasskeysVerifyCredentialEvent should emit DerivPasskeysErrorState if any other error is thrown', + () { + when(() => mockDerivPasskeysService.verifyCredential( + jwtToken: any(named: 'jwtToken'), + passkeysConnectionInfoEntity: + any(named: 'passkeysConnectionInfoEntity'), + userAgent: any(named: 'userAgent'), + )).thenAnswer((_) async => throw Exception()); + + expectLater( + derivPasskeysBloc.stream, + emitsInOrder(>[ + isA(), + isA(), + ]), + ); + + derivPasskeysBloc.add(DerivPasskeysVerifyCredentialEvent()); + }); + + test( + 'DerivPasskeysCreateCredentialEvent should emit DerivPasskeysCreatedSuccessfullyState', + () { + const DerivPasskeyEntity mockCredential = DerivPasskeyEntity( + id: 'id', + name: 'name', + createdAt: 1234, + lastUsed: null, + passkeyId: '', + storedOn: '', + ); + + when(() => mockDerivPasskeysService.createCredential()) + .thenAnswer((_) async => mockCredential); + + expectLater( + derivPasskeysBloc.stream, + emitsInOrder(>[ + isA(), + isA(), + isA(), + ]), + ); + + derivPasskeysBloc.add(DerivPasskeysCreateCredentialEvent()); + }); + + test( + 'DerivPasskeysCreateCredentialEvent should emit DerivPasskeysLoadedState if CanceledPlatformException is thrown', + () { + when(() => mockDerivPasskeysService.createCredential()) + .thenAnswer((_) async => throw CanceledPlatformException( + code: '', + message: '', + details: '', + )); + + expectLater( + derivPasskeysBloc.stream, + emitsInOrder(>[ + isA(), + isA(), + isA(), + ]), + ); + + derivPasskeysBloc.add(DerivPasskeysCreateCredentialEvent()); + }); + + test( + 'DerivPasskeysCreateCredentialEvent should emit DerivPasskeysErrorState if any other error is thrown', + () { + when(() => mockDerivPasskeysService.createCredential()) + .thenAnswer((_) async => throw Exception()); + + expectLater( + derivPasskeysBloc.stream, + emitsInOrder(>[ + isA(), + isA(), + ]), + ); + + derivPasskeysBloc.add(DerivPasskeysCreateCredentialEvent()); + }); + + test( + 'DerivPasskeysGetPasskeysListEvent should emit DerivPasskeysLoadedState', + () { + final List mockPasskeys = [ + const DerivPasskeyEntity( + id: 'id', + name: 'name', + createdAt: 1234, + lastUsed: null, + passkeyId: '', + storedOn: '', + ), + ]; + + when(() => mockDerivPasskeysService.getPasskeysList()) + .thenAnswer((_) async => mockPasskeys); + + expectLater( + derivPasskeysBloc.stream, + emitsInOrder(>[ + isA(), + isA(), + ]), + ); + + derivPasskeysBloc.add(const DerivPasskeysGetPasskeysListEvent()); + }); + + test( + 'DerivPasskeysGetPasskeysListEvent should emit DerivPasskeysErrorState if any error is thrown', + () { + when(() => mockDerivPasskeysService.getPasskeysList()) + .thenAnswer((_) async => throw Exception()); + + expectLater( + derivPasskeysBloc.stream, + emitsInOrder(>[ + isA(), + isA(), + ]), + ); + + derivPasskeysBloc.add(const DerivPasskeysGetPasskeysListEvent()); + }); + }); +} diff --git a/packages/deriv_passkeys/test/presentation/states/bloc/deriv_passkeys_event_test.dart b/packages/deriv_passkeys/test/presentation/states/bloc/deriv_passkeys_event_test.dart new file mode 100644 index 000000000..8991ea9bc --- /dev/null +++ b/packages/deriv_passkeys/test/presentation/states/bloc/deriv_passkeys_event_test.dart @@ -0,0 +1,64 @@ +import 'package:deriv_passkeys/deriv_passkeys.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('DerivPasskeysCreateCredentialEvent', () { + test('props should be empty', () { + final DerivPasskeysEvent event = DerivPasskeysCreateCredentialEvent(); + + expect(event.props, isEmpty); + }); + }); + + group('DerivPasskeysVerifyCredentialEvent', () { + test('props should be empty', () { + final DerivPasskeysEvent event = DerivPasskeysVerifyCredentialEvent(); + + expect(event.props, isEmpty); + }); + }); + + group('DerivPasskeysGetPasskeysListEvent', () { + test('props should be empty', () { + const DerivPasskeysEvent event = DerivPasskeysGetPasskeysListEvent(); + + expect(event.props, isEmpty); + }); + }); + + group('SetDerivPasskeysInitializedEvent', () { + test('props should be empty', () { + const DerivPasskeysEvent event = SetDerivPasskeysInitializedEvent(); + + expect(event.props, isEmpty); + }); + }); + + group('SetDerivPasskeysNotSupportedEvent', () { + test('props should be empty', () { + const DerivPasskeysEvent event = SetDerivPasskeysNotSupportedEvent(); + + expect(event.props, isEmpty); + }); + }); + + group('DerivPasskeysRevokeCredentialEvent', () { + test('props should contain options', () { + const String options = 'revoke_options'; + const DerivPasskeysEvent event = + DerivPasskeysRevokeCredentialEvent(options); + + expect(event.props, [options]); + }); + }); + + group('DerivPasskeysEditCredentialEvent', () { + test('props should contain options', () { + const String options = 'edit_options'; + const DerivPasskeysEvent event = + DerivPasskeysEditCredentialEvent(options); + + expect(event.props, [options]); + }); + }); +} diff --git a/packages/deriv_passkeys/test/presentation/states/bloc/deriv_passkeys_state_test.dart b/packages/deriv_passkeys/test/presentation/states/bloc/deriv_passkeys_state_test.dart new file mode 100644 index 000000000..65332d35d --- /dev/null +++ b/packages/deriv_passkeys/test/presentation/states/bloc/deriv_passkeys_state_test.dart @@ -0,0 +1,67 @@ +import 'package:deriv_passkeys/deriv_passkeys.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkey_entity.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('DerivPasskeysLoadedState', () { + test('props should contain passkeysList', () { + final List passkeysList = []; + final DerivPasskeysState state = DerivPasskeysLoadedState(passkeysList); + expect(state.props, contains(passkeysList)); + }); + }); + + group('DerivPasskeysInitializedState', () { + test('props should be empty', () { + final DerivPasskeysState state = DerivPasskeysInitializedState(); + expect(state.props, isEmpty); + }); + }); + + group('DerivPasskeysLoadingState', () { + test('props should be empty', () { + final DerivPasskeysState state = DerivPasskeysLoadingState(); + expect(state.props, isEmpty); + }); + }); + + group('DerivPasskeysCreatedSuccessfullyState', () { + test('props should be empty', () { + final DerivPasskeysState state = DerivPasskeysCreatedSuccessfullyState(); + expect(state.props, isEmpty); + }); + }); + + group('DerivPasskeysCredentialVerifiedState', () { + test('props should contain token', () { + const String token = 'example_token'; + const DerivPasskeysState state = + DerivPasskeysCredentialVerifiedState(token: token); + expect(state.props, contains(token)); + }); + }); + + group('DerivPasskeysNotSupportedState', () { + test('props should be empty', () { + final DerivPasskeysState state = DerivPasskeysNotSupportedState(); + expect(state.props, isEmpty); + }); + }); + + group('DerivPasskeysErrorState', () { + test('props should contain message and errorCode', () { + const String message = 'example_message'; + const String errorCode = 'example_error_code'; + const DerivPasskeysState state = + DerivPasskeysErrorState(message, errorCode: errorCode); + expect(state.props, containsAll([message, errorCode])); + }); + }); + + group('NoCredentialErrorState', () { + test('props should contain message and errorCode', () { + const NoCredentialErrorState state = NoCredentialErrorState(); + expect(state.props, contains('No credential found')); + }); + }); +} diff --git a/packages/deriv_passkeys/test/presentation/utils/date_time_utils_test.dart b/packages/deriv_passkeys/test/presentation/utils/date_time_utils_test.dart new file mode 100644 index 000000000..22c28d549 --- /dev/null +++ b/packages/deriv_passkeys/test/presentation/utils/date_time_utils_test.dart @@ -0,0 +1,22 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:deriv_passkeys/src/presentation/utils/date_time_utils.dart'; + +void main() { + group('DateTimeUtils', () { + test('dateTimeFromTimestamp should return DateTime', () { + const int timestamp = 1630444800000; + + final DateTime result = dateTimeFromTimestamp(timestamp); + + expect(result, isA()); + }); + + test('formattedDate should return formatted date string', () { + final DateTime date = DateTime(2023, 4, 20); // Provide a valid date + + final String result = formattedDate(date); + + expect(result, isA()); + }); + }); +} diff --git a/packages/deriv_passkeys/test/presentation/widgets/continue_with_passkey_button_test.dart b/packages/deriv_passkeys/test/presentation/widgets/continue_with_passkey_button_test.dart new file mode 100644 index 000000000..70c973c68 --- /dev/null +++ b/packages/deriv_passkeys/test/presentation/widgets/continue_with_passkey_button_test.dart @@ -0,0 +1,158 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:deriv_localizations/l10n/generated/deriv_passkeys/deriv_passkeys_localizations.dart'; +import 'package:deriv_localizations/l10n/generated/deriv_passkeys/deriv_passkeys_localizations_en.dart'; +import 'package:deriv_passkeys/src/presentation/states/bloc/deriv_passkeys_bloc.dart'; +import 'package:deriv_passkeys/src/presentation/widgets/continue_with_passkey_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MockDerivPasskeysBloc + extends MockBloc + implements DerivPasskeysBloc {} + +class _TestPage extends StatelessWidget { + const _TestPage(); + + @override + Widget build(BuildContext context) => const MaterialApp( + localizationsDelegates: >[ + DerivPasskeysLocalizations.delegate, + ], + locale: Locale('en'), + home: Scaffold( + body: ContinueWithPasskeyButton(), + ), + ); +} + +void main() { + group('ContinueWithPasskeyButton', () { + late MockDerivPasskeysBloc derivPasskeysBloc; + setUp(() { + derivPasskeysBloc = MockDerivPasskeysBloc(); + + when(() => derivPasskeysBloc.state).thenReturn( + DerivPasskeysInitializedState(), + ); + }); + + testWidgets('renders correctly', (WidgetTester tester) async { + await tester.pumpWidget( + BlocProvider( + create: (BuildContext context) => derivPasskeysBloc, + child: const _TestPage(), + ), + ); + + expect(find.byType(InkWell), findsOneWidget); + expect(find.byType(Container), findsOneWidget); + expect(find.byType(SvgPicture), findsOneWidget); + expect(find.text('Passkey'), findsOneWidget); + }); + + testWidgets('does not render if passkeys not supported', + (WidgetTester tester) async { + when(() => derivPasskeysBloc.state).thenReturn( + DerivPasskeysNotSupportedState(), + ); + await tester.pumpWidget( + BlocProvider( + create: (BuildContext context) => derivPasskeysBloc, + child: const _TestPage(), + ), + ); + + await tester.pumpAndSettle(); + + expect(find.byType(InkWell), findsNothing); + expect(find.byType(SvgPicture), findsNothing); + expect(find.text('Passkey'), findsNothing); + }); + + // if you want to test the onPressed callback, you can use the following code + testWidgets('should call DerivPasskeysVerifyCredentialEvent when pressed', + (WidgetTester tester) async { + await tester.pumpWidget( + BlocProvider( + create: (BuildContext context) => derivPasskeysBloc, + child: const _TestPage(), + ), + ); + await tester.pumpAndSettle(); + await tester.tap(find.byType(InkWell)); + await tester.pumpAndSettle(); + + verify(() => derivPasskeysBloc.add(DerivPasskeysVerifyCredentialEvent())) + .called(1); + }); + + testWidgets( + 'should show alert dialog when NoCredentialErrorState is emitted', + (WidgetTester tester) async { + whenListen( + derivPasskeysBloc, + Stream.fromIterable( + [const NoCredentialErrorState()])); + await tester.pumpWidget( + BlocProvider( + create: (BuildContext context) => derivPasskeysBloc, + child: const _TestPage(), + ), + ); + + await tester.pumpAndSettle(); + + expect(find.byType(AlertDialog), findsOneWidget); + expect(find.text(DerivPasskeysLocalizationsEn().noPasskeyFound), + findsOneWidget); + expect( + find.text(DerivPasskeysLocalizationsEn().noPasskeyFoundDescription), + findsOneWidget); + expect(find.text(DerivPasskeysLocalizationsEn().ok.toUpperCase()), + findsOneWidget); + + await tester + .tap(find.text(DerivPasskeysLocalizationsEn().ok.toUpperCase())); + await tester.pumpAndSettle(); + + expect(find.byType(AlertDialog), findsNothing); + }); + + testWidgets( + 'should show alert dialog with error message when DerivPasskeysErrorState is emitted with errorCode PASSKEYS_SERVICE_ERROR', + (WidgetTester tester) async { + whenListen( + derivPasskeysBloc, + Stream.fromIterable([ + const DerivPasskeysErrorState( + 'error_message', + errorCode: 'PASSKEYS_SERVICE_ERROR', + ) + ])); + await tester.pumpWidget( + BlocProvider( + create: (BuildContext context) => derivPasskeysBloc, + child: const _TestPage(), + ), + ); + + await tester.pumpAndSettle(); + + expect(find.byType(AlertDialog), findsOneWidget); + expect(find.text(DerivPasskeysLocalizationsEn().unexpectedError), + findsOneWidget); + expect(find.text('error_message'), findsOneWidget); + expect(find.text(DerivPasskeysLocalizationsEn().ok.toUpperCase()), + findsOneWidget); + + await tester + .tap(find.text(DerivPasskeysLocalizationsEn().ok.toUpperCase())); + await tester.pumpAndSettle(); + + expect(find.byType(AlertDialog), findsNothing); + }); + }); +} diff --git a/packages/deriv_passkeys/test/presentation/widgets/icon_text_row_widget_test.dart b/packages/deriv_passkeys/test/presentation/widgets/icon_text_row_widget_test.dart new file mode 100644 index 000000000..e1ad6fb79 --- /dev/null +++ b/packages/deriv_passkeys/test/presentation/widgets/icon_text_row_widget_test.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:deriv_passkeys/src/presentation/widgets/icon_text_row_widget.dart'; + +void main() { + testWidgets('IconTextRowWidget displays icon and text correctly', + (WidgetTester tester) async { + const String assetName = 'assets/svg/passkey_icon.svg'; + const String text = 'Sample Text'; + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: IconTextRowWidget( + assetName: assetName, + text: text, + ), + ), + ), + ); + + final Finder svgFinder = find.byType(SvgPicture); + final Finder textFinder = find.text(text); + + expect(svgFinder, findsOneWidget); + expect(textFinder, findsOneWidget); + + final SvgPicture svgWidget = tester.widget(svgFinder) as SvgPicture; + final Text textWidget = tester.widget(textFinder) as Text; + + expect((svgWidget.bytesLoader as SvgAssetLoader).packageName, + 'deriv_passkeys'); + expect((svgWidget.bytesLoader as SvgAssetLoader).assetName, assetName); + expect(textWidget.data, text); + }); +} diff --git a/packages/deriv_passkeys/test/presentation/widgets/passkey_widget_test.dart b/packages/deriv_passkeys/test/presentation/widgets/passkey_widget_test.dart new file mode 100644 index 000000000..c4d05714d --- /dev/null +++ b/packages/deriv_passkeys/test/presentation/widgets/passkey_widget_test.dart @@ -0,0 +1,37 @@ +import 'package:deriv_localizations/l10n/generated/deriv_passkeys/deriv_passkeys_localizations.dart'; +import 'package:deriv_passkeys/src/domain/entities/deriv_passkey_entity.dart'; +import 'package:deriv_passkeys/src/presentation/widgets/passkey_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('PasskeyWidget', () { + const DerivPasskeyEntity passkey = DerivPasskeyEntity( + storedOn: 'Device', + lastUsed: 1638297600000, + createdAt: 1638297600000, + id: '', + name: 'Passkey', + passkeyId: '', + ); + + testWidgets('should display passkey information', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + localizationsDelegates: >[ + DerivPasskeysLocalizations.delegate, + ], + locale: Locale('en'), + home: Scaffold( + body: PasskeyWidget(passkey: passkey), + ), + ), + ); + + expect(find.text('Passkey'), findsOneWidget); + expect(find.textContaining('Device'), findsOneWidget); + expect(find.textContaining('November 30th, 2021'), findsOneWidget); + }); + }); +} diff --git a/packages/deriv_passkeys/test/presentation/widgets/section_title_and_content_test.dart b/packages/deriv_passkeys/test/presentation/widgets/section_title_and_content_test.dart new file mode 100644 index 000000000..a8a1bf8eb --- /dev/null +++ b/packages/deriv_passkeys/test/presentation/widgets/section_title_and_content_test.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:deriv_passkeys/src/presentation/widgets/section_title_and_content.dart'; + +void main() { + group('SectionTitleAndContent', () { + testWidgets('should display the title and content', + (WidgetTester tester) async { + const String title = 'Test Title'; + const String content = 'Test Content'; + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: SectionTitleAndContent( + title: title, + texts: [content], + ), + ), + ), + ); + + final Finder titleFinder = find.text(title); + final Finder contentFinder = find.text(content); + + expect(titleFinder, findsOneWidget); + expect(contentFinder, findsOneWidget); + }); + }); +} diff --git a/packages/deriv_passkeys/test/presentation/widgets/unordered_list_widget_test.dart b/packages/deriv_passkeys/test/presentation/widgets/unordered_list_widget_test.dart new file mode 100644 index 000000000..dba0b13b9 --- /dev/null +++ b/packages/deriv_passkeys/test/presentation/widgets/unordered_list_widget_test.dart @@ -0,0 +1,53 @@ +import 'package:deriv_passkeys/src/presentation/widgets/unordered_list_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('UnorderedList', () { + const List texts = ['Item 1', 'Item 2', 'Item 3']; + const TextStyle? style = TextStyle(fontSize: 16); + + testWidgets('Renders the correct number of items', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: UnorderedList(texts: texts, style: style), + ), + ), + ); + + final Finder listItemFinder = find.byType(UnorderedListItem); + expect(listItemFinder, findsNWidgets(texts.length)); + }); + + testWidgets('Renders the correct text', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: UnorderedList(texts: texts, style: style), + ), + ), + ); + + for (final String text in texts) { + final Finder textFinder = find.text(text); + expect(textFinder, findsOneWidget); + } + }); + + testWidgets('Renders the correct bullet points', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: UnorderedList(texts: texts, style: style), + ), + ), + ); + + final Finder bulletPointFinder = find.text('\u2022'); + expect(bulletPointFinder, findsNWidgets(texts.length)); + }); + }); +}