extends HookConsumerWidget {
requestWithVerifyIdentity(({
required OnPasswordFlow onPasswordFlow,
required OnPasskeyFlow
onPasskeyFlow,
+ required OnBiometricsFlow
onBiometricsFlow,
}) {
return ref.read(
verifyUserIdentityProvider(
onGetPassword: onGetPassword,
onPasswordFlow: onPasswordFlow,
onPasskeyFlow: onPasskeyFlow,
+ onBiometricsFlow: onBiometricsFlow,
+ localisedReasonForBiometricsDialog: context.i18n.verify_with_biometrics_title,
).future,
);
});
@@ -81,6 +85,7 @@ class HookVerifyIdentityRequestBuilder
extends HookConsumerWidget {
requestWithVerifyIdentity(({
required OnPasswordFlow
onPasswordFlow,
required OnPasskeyFlow
onPasskeyFlow,
+ required OnBiometricsFlow
onBiometricsFlow,
}) async {
try {
return await ref.read(
@@ -88,6 +93,8 @@ class HookVerifyIdentityRequestBuilder
extends HookConsumerWidget {
onGetPassword: onGetPassword,
onPasswordFlow: onPasswordFlow,
onPasskeyFlow: onPasskeyFlow,
+ onBiometricsFlow: onBiometricsFlow,
+ localisedReasonForBiometricsDialog: context.i18n.verify_with_biometrics_title,
).future,
);
} finally {
diff --git a/lib/app/features/user/providers/user_delegation_provider.c.dart b/lib/app/features/user/providers/user_delegation_provider.c.dart
index 0ee8f6553..51284fc19 100644
--- a/lib/app/features/user/providers/user_delegation_provider.c.dart
+++ b/lib/app/features/user/providers/user_delegation_provider.c.dart
@@ -103,6 +103,11 @@ class UserDelegationManager extends _$UserDelegationManager {
.wallets
.generateHashSignatureWithPasskey(mainWallet.id, eventId);
},
+ onBiometricsFlow: ({required String localisedReason}) {
+ return ionIdentity(username: currentIdentityKeyName)
+ .wallets
+ .generateHashSignatureWithBiometrics(mainWallet.id, eventId, localisedReason);
+ },
);
final curveName = switch (mainWallet.signingKey.curve) {
diff --git a/lib/app/features/user/providers/user_verify_identity_provider.c.dart b/lib/app/features/user/providers/user_verify_identity_provider.c.dart
index d44274eaa..035136a9d 100644
--- a/lib/app/features/user/providers/user_verify_identity_provider.c.dart
+++ b/lib/app/features/user/providers/user_verify_identity_provider.c.dart
@@ -16,13 +16,22 @@ AutoDisposeFutureProvider verifyUserIdentityProvider({
required Future Function() onGetPassword,
required OnPasswordFlow onPasswordFlow,
required OnPasskeyFlow onPasskeyFlow,
+ required OnBiometricsFlow onBiometricsFlow,
+ required String localisedReasonForBiometricsDialog,
}) {
return FutureProvider.autoDispose((ref) async {
final username = ref.read(currentIdentityKeyNameSelectorProvider)!;
final ionIdentity = await ref.read(ionIdentityProvider.future);
final isPasswordFlowUser = ionIdentity(username: username).auth.isPasswordFlowUser();
+ final biometricsState = ionIdentity(username: username).auth.getBiometricsState();
if (isPasswordFlowUser) {
+ if (biometricsState == BiometricsState.enabled) {
+ try {
+ return await onBiometricsFlow(localisedReason: localisedReasonForBiometricsDialog);
+ // If biometrics flow fails then fallback to password flow
+ } catch (_) {}
+ }
final password = await onGetPassword();
if (password != null) {
return onPasswordFlow(password: password);
diff --git a/lib/l10n/app_en.arb.orig b/lib/l10n/app_en.arb.orig
deleted file mode 100644
index 248b139eb..000000000
--- a/lib/l10n/app_en.arb.orig
+++ /dev/null
@@ -1,684 +0,0 @@
-{
- "@@locale": "en",
- "day": "{count, plural, =1{day} other{days}}",
- "hour": "{count, plural, =1{hour} other{hours}}",
- "button_continue": "Continue",
- "button_follow": "Follow",
- "button_follow_back": "Follow back",
- "button_following": "Following",
- "button_save": "Save",
- "button_cancel": "Cancel",
- "button_log_out": "Log out",
- "button_turn_off": "Turn off",
- "button_unfollow": "Unfollow",
- "button_delete": "Delete",
- "button_edit": "Edit",
- "button_close": "Close",
- "button_register": "Register",
- "button_send": "Send",
- "button_request": "Request",
- "button_login": "Login",
- "button_try_again": "Try again",
- "button_retry": "Retry",
- "button_confirm": "Confirm",
- "button_restore": "Restore",
- "button_reset": "Reset",
- "button_back": "Back",
- "button_back_to_security": "Back to Security",
- "button_lets_start": "Let's start",
- "button_next": "Next",
- "button_add": "Add",
- "button_apply": "Apply",
- "button_schedule": "Schedule",
- "button_go_to_settings": "Go to Settings",
- "button_allow": "Allow",
- "button_dont_allow": "Don't Allow",
- "button_share_story": "Share story",
- "button_add_answer": "Add answer",
- "button_link": "Link",
- "button_share": "Share",
- "button_mute": "Mute",
- "button_unmute": "Unmute",
- "button_block": "Block",
- "button_report": "Report",
- "button_publish": "Publish",
- "button_learn_more": "Learn More",
- "button_forward": "Forward",
- "button_reply": "Reply",
- "button_copy": "Copy",
- "button_bookmark": "Bookmark",
- "button_save_changes": "Save Changes",
- "dropdown_select_category": "Select category",
- "dropdown_category_hate": "Hate",
- "dropdown_category_violence": "Violent speech",
- "dropdown_category_child_safety": "Child safety",
- "common_show_more": "Show more",
- "common_show_less": "Show less",
- "common_identity_key_name": "Identity key name",
- "common_password": "Password",
- "common_confirm_password": "Confirm password",
- "common_seconds": "{seconds}s",
- "common_congratulations": "Congratulations",
- "common_successfully": "Successfully",
- "common_select_languages": "Select languages",
- "common_no_access_permission": "To grant access, go to settings and enable the appropriate settings",
- "common_crop_image": "Crop Image",
- "common_photo": "Photo",
- "common_video": "Video",
- "common_voice_message": "Voice message",
- "common_poll": "Poll",
- "common_archive": "Archive",
- "common_select_option": "Select option",
- "common_select_coin": "Select coin",
- "common_email_address": "Email address",
- "common_information": "Information",
- "common_photos": "Photos",
- "common_camera": "Camera",
- "common_ion_pay": "ION Pay",
- "common_profile": "Profile",
- "common_document": "Document",
- "common_you": "You",
- "common_add_video": "Add video",
- "common_forwarded": "Forwarded",
- "common_forwarded_from": "Forwarded from",
- "common_title": "Title",
- "common_desc": "Description",
- "common_public": "Public",
- "common_private": "Private",
- "common_invitation_link": "Invitation link",
- "common_share_link": "Share link",
- "common_copied": "Copied",
- "auth_secured_by": "Secured by",
- "auth_privacy": "By continuing, you are agreeing to our [[:link]]terms_of_service[[/:link]] & [[:link]]privacy_policy[[/:link]]",
- "auth_terms_of_service": "Terms of Service",
- "auth_privacy_policy": "Privacy Policy",
- "auth_identity_io": "Identity.io",
- "two_fa_title": "2FA Verification",
- "two_fa_desc": "Please enter your confirmation code below",
- "two_fa_select": "Select option {number}",
- "two_fa_email": "Email code",
- "two_fa_sms": "SMS code",
- "two_fa_auth": "Authenticator code",
- "two_fa_code_confirmation": "Please enter the code sent to",
- "two_fa_delete_email_button": "Delete email",
- "two_fa_edit_email_button": "Edit email",
- "two_fa_deleting_email_title": "Deleting email",
- "two_fa_deleting_email_description": "To delete the email verification, you must confirm by selecting the options first",
- "two_fa_deleting_phone_title": "Phone number verification",
- "two_fa_deleting_phone_description": "To delete verification by phone, you must confirm by selecting the options first",
- "two_fa_account_linked_to": "Your account is linked to",
- "two_fa_option_backup": "Backup",
- "two_fa_option_email": "Email",
- "two_fa_option_authenticator": "Authenticator",
- "two_fa_option_phone": "Phone",
- "two_fa_delete_email_success": "The email address has been successfully deleted",
- "two_fa_delete_phone_success": "Verification by phone was successfully deleted",
- "two_fa_success_desc": "Your identity key has been restored. You can now access your account securely",
- "two_fa_failure_title": "2FA Verification error",
- "two_fa_failure_desc": "Please ensure all codes are correct and try again",
- "two_fa_failure_authenticator_title": "Authenticator not available",
- "two_fa_failure_authenticator_description": "To set up an Authenticator app, please first link an email address or phone number to your account",
- "two_fa_warning": "If you delete two-step authentication, it may put the security of your data at risk.",
- "sign_up_passkey_title": "Register with passkey",
- "sign_up_passkey_advantage_1_title": "No password to remember",
- "sign_up_passkey_advantage_1_description": "With passkey, you can use things like your fingerprint or face to login",
- "sign_up_passkey_advantage_2_title": "Works on all of your devices",
- "sign_up_passkey_advantage_2_description": "Passkey will automatically be available across your synced devices",
- "sign_up_passkey_advantage_3_title": "Keep your account safer",
- "sign_up_passkey_advantage_3_description": "Passkey offer state-of-the-art phishing resistance",
- "sign_up_passkey_use_password": "Use password instead",
- "sign_up_password_title": "Register",
- "sign_up_passkey_identity_key_name_taken": "Identity key name is taken",
- "sign_up_password_description": "Choose a strong password to create an account",
- "restore_identity_title": "Restore identity key",
- "restore_identity_type_description": "Select the type of identity key recovery",
- "restore_identity_type_google_drive_title": "Restore from Google Drive",
- "restore_identity_type_icloud_title": "Restore from iCloud",
- "restore_identity_type_icloud_description": "Restore your identity key from an iCloud backup",
- "restore_identity_type_credentials_title": "Restore using recovery credentials",
- "restore_identity_type_credentials_description": "Restore with Recovery code and Recovery key ID",
- "discover_creators_title": "Discover creators",
- "restore_identity_creds_description": "Please enter your recovery credentials below",
- "restore_identity_creds_recovery_code": "Recovery code",
- "restore_identity_creds_recovery_key": "Recovery key ID",
- "restore_identity_creds_action": "Recovery key ID",
- "discover_creators_description": "Connect with visionaries and inspiring voices",
- "fill_profile_title": "Your profile",
- "fill_profile_description": "Customize your account",
- "fill_profile_input_name": "Name",
- "fill_profile_input_nickname": "Nickname",
- "get_started_title": "Get started",
- "get_started_description": "Enter your identity key name to log in into your account",
- "get_started_method_divider": "or",
- "get_started_restore_button": "Restore identity key",
- "identity_key_name_description": "Think of your identity key name as a unique identifier of your account. You’ll need it to log in and recover your account, so keep it safe and don’t forget it",
- "identity_key_name_usage": "Use it to log in on any app secured by",
- "select_languages_description": "You’ll be shown content in the selected language",
- "dapps_section_title_highest_ranked": "Highest ranked",
- "dapps_section_title_recently_added": "Recently added",
- "dapps_section_title_favourites": "Favorites",
- "dapps_section_title_featured": "Featured",
- "dapps_section_title_categories": "Categories",
- "dapps_category_defi": "DeFi",
- "dapps_category_marketplaces": "Marketplaces",
- "dapps_category_games": "Games",
- "dapps_category_social": "Social",
- "dapps_category_utilities": "Utilities",
- "dapps_category_other": "Other",
- "dapps_favourites_added": "{count} added dApps",
- "dapps_favourites_empty_title": "You have no favourites dApps yet",
- "dapps_search_empty": "Search here for dApps, categories…",
- "dapp_details_launch_dapp_button_title": "Launch dApp",
- "dapp_details_tips": "Tips",
- "dapp_details_tips_games": "Games",
- "dapp_details_tips_global_rank": "Global Rank",
- "dapp_details_tips_vote": "Vote",
- "dapp_details_tips_voted": "Voted",
- "search_placeholder": "Search",
- "search_nothing_found": "Nothing was found for your query",
- "profile_following": "Following",
- "profile_followers": "Followers",
- "profile_following_with_counter": "Following ({counter})",
- "profile_followers_with_counter": "Followers ({counter})",
- "profile_privacy": "Settings & Privacy",
- "profile_help": "Help Center",
- "profile_profile_desc": "Your personal space",
- "profile_feed_desc": "Discover and engage",
- "profile_videos_desc": "Social streaming",
- "profile_articles_desc": "Trending stories",
- "profile_bookmarks": "Bookmarks",
- "profile_bookmarks_desc": "Your saved posts",
- "profile_switch_user_header": "User accounts",
- "profile_create_new_account": "Create a new account",
- "profile_log_out": "Log out {nickname}",
- "profile_followed_by": "Followed by ",
- "profile_followed_by_and": " and ",
- "profile_followed_by_and_others": "others",
- "profile_follows_you": "Follows you",
- "profile_none": "None",
- "profile_posts": "Posts",
- "profile_stories": "Stories",
- "profile_replies": "Replies",
- "profile_videos": "Videos",
- "profile_articles": "Articles",
- "profile_popup_block_user_desc": "Are you sure you want to block this user?",
- "profile_popup_report_title": "Report @{nickname}",
- "profile_popup_report_desc": "Select a category from the list below",
- "profile_popup_report_success_title": "Successfully sent",
- "profile_popup_report_success_desc": "Your report has been successfully sent",
- "profile_popup_unfollow": "Unfollow @{nickname}?",
- "profile_popup_unfollow_desc": "Once you unfollow, you will no longer see their updates or content in your feed.",
- "profile_notifications_popup_title": "Account notifications",
- "profile_edit": "Edit profile",
- "profile_save": "Save profile",
- "profile_bio": "Bio",
- "profile_location": "Location",
- "profile_website": "Website",
- "profile_send_option_title": "Send payment",
- "profile_send_option_desc": "Send funds quickly and securely",
- "profile_request_option_title": "Request payment",
- "profile_request_option_desc": "Securely receive funds with one tap",
- "profile_send_coin": "Send coin",
- "profile_request_funds": "Request funds",
- "profile_empty_state": "There's nothing here yet",
- "profile_creation_date": "December 2024",
- "notifications_title": "Notifications",
- "notifications_type_comments": "Comments",
- "notifications_type_followers": "Followers",
- "notifications_type_likes": "Likes",
- "notifications_followed_one": "[:username] followed you",
- "notifications_followed_two": "[:username] and [:username] followed you",
- "notifications_followed_many": "[:username] and {number} others followed you",
- "notifications_liked_one": "[:username] liked your post",
- "notifications_liked_two": "[:username] and [:username] liked your post",
- "notifications_liked_many": "[:username] and {number} others liked your post",
- "notifications_liked_reply_one": "[:username] liked your reply",
- "notifications_liked_reply_two": "[:username] and [:username] liked your reply",
- "notifications_liked_reply_many": "[:username] and {number} others liked your reply",
- "notifications_reply": "[:username] replied to your post",
- "notifications_share": "[:username] shared your post",
- "notifications_repost": "[:username] reposted your post",
- "notifications_empty_state": "You don't have any notifications",
- "settings_title": "Settings",
- "settings_security": "Security",
- "settings_privacy": "Privacy",
- "settings_push_notifications": "Push notifications",
- "settings_privacy_policy": "Privacy policy",
- "settings_terms_conditions": "Terms & conditions",
- "settings_logout": "Logout",
- "settings_app_version": "dApp version {version}",
- "settings_profile_edit": "Edit profile",
- "settings_app_language": "dApp language",
- "settings_content_language": "Content language",
- "settings_remaining_content_languages_number": "+ {number}",
- "privacy_group_wallet_address_title": "Wallet address",
- "privacy_group_who_can_message_you_title": "Who can message you",
- "privacy_group_who_can_invite_you_title": "Who can invite you in groups",
- "privacy_option_wallet_public": "Public",
- "privacy_option_wallet_private": "Private",
- "privacy_option_user_visibility_for_everyone": "Everyone",
- "privacy_option_user_visibility_for_followed_people": "People I follow",
- "privacy_option_user_visibility_for_friends": "Friends",
- "push_notification_device_permission": "Device permission",
- "push_notification_social_group_title": "Social",
- "push_notification_chat_group_title": "Chat",
- "push_notification_wallet_group_title": "Wallet",
- "push_notification_system_group_title": "System",
- "push_notification_social_option_posts": "Posts",
- "push_notification_social_option_mentions_replies": "Mentions and replies",
- "push_notification_social_option_reposts": "Reposts",
- "push_notification_social_option_likes": "Likes",
- "push_notification_social_option_new_followers": "New followers",
- "push_notification_chat_option_direct_messages": "Direct messages",
- "push_notification_chat_option_group_chats": "Group chats",
- "push_notification_chat_option_channels": "Channels",
- "push_notification_wallet_option_payment_request": "Payment request",
- "push_notification_wallet_option_payment_received": "Payment received",
- "push_notification_system_option_update": "Update",
- "app_language_title": "App language",
- "app_language_description": "You’ll be shown the app in the selected language",
- "confirm_logout_title": "Log out {username}?",
- "confirm_logout_description": "Are you sure you want to log out from the app?",
- "content_language_title": "Content languages",
- "content_language_description": "You’ll be shown content in the selected language",
- "category_aviation": "Aviation",
- "category_blockchain": "Blockchain",
- "category_business": "Business",
- "category_cars": "Cars",
- "category_cryptocurrency": "Cryptocurrency",
- "category_data_science": "Data Science",
- "category_education": "Education",
- "category_finance": "Finance",
- "category_gamer": "Gamer",
- "category_style": "Style",
- "category_restaurant": "Restaurant",
- "category_trading": "Trading",
- "category_technology": "Technology",
- "category_traveler": "Traveler",
- "category_news": "News",
- "wallet_balance": "Balance",
- "wallet_send": "Send",
- "wallet_send_coins": "Send coins",
- "wallet_receive_info": "When sending funds, the networks must match! otherwise, you may permanently lose your funds",
- "wallet_choose_network": "Choose network",
- "wallet_receive": "Receive",
- "wallet_receive_coins": "Receive coins",
- "wallet_share_address": "Share address",
- "wallet_scan": "Scan the QR code",
- "wallet_scan_hint": "Scan the QR code to send cryptocurrency",
- "wallet_sent": "Sent",
- "wallet_received": "Received",
- "wallet_hide": "Hide 0.00",
- "wallet_empty_coins": "You have no coins yet",
- "wallet_empty_nfts": "You don't have any NFT's",
- "wallet_manage_coins": "Manage coins",
- "wallet_manage_nfts": "Select chain",
- "wallet_buy_nfts": "Buy NFTs",
- "wallet_coin_address": "{coin} address",
- "wallet_network": "Network",
- "wallet_change": "change",
- "wallet_wallets": "Wallets",
- "wallet_manage_wallets": "Manage wallets",
- "wallet_create_new": "Create a new wallet",
- "wallet_create": "Create wallet",
- "wallet_name": "Wallet name",
- "wallet_edit": "Edit wallet",
- "wallet_delete": "Delete wallet",
- "wallet_delete_q": "Delete wallet?",
- "wallet_delete_message": "All coins on this wallet will be lost. Are you sure you want to delete this wallet?",
- "wallet_enter_address": "Enter address",
- "wallet_usdt_amount": "USDT amount",
- "wallet_arrival_time": "Arrival time",
- "wallet_arrival_time_type_normal": "Normal",
- "wallet_arrival_time_minutes": "min",
- "wallet_network_fee": "Network fee",
- "wallet_max": "max",
- "wallet_send_to": "Send to",
- "wallet_asset": "Asset",
- "wallet_title": "Wallet",
- "wallet_transaction_successful": "Transfer successful",
- "wallet_transaction_details": "Details",
- "wallet_transaction_details_arrival_time_format": "dd.MM.yyyy HH:mm:ss",
- "wallet_explore_transaction_details_title": "View on explorer",
- "wallet_invite_friends": "Invite friend",
- "wallet_friends_does_not_have_account": "Your friend doesn’t have an Ice account.",
- "wallet_approximate_in_usd": "~ ${amount}",
- "wallet_coin_amount": "{coin} amount",
- "sorting_price_asc": "Price: low to high",
- "sorting_price_desc": "Price: high to low",
- "wallet_sorting_title": "Sorting the list",
- "contacts_title": "Contacts",
- "contacts_select_title": "Select contact",
- "contacts_allow_pop_up_title": "Ice is better with friends",
- "contacts_allow_pop_up_desc": "Sync your contacts, see who is already on Ice, and send and receive Ice payments from any of your contacts.",
- "contacts_allow_pop_up_action": "Allow contacts access",
- "core_view_all": "view all",
- "core_all": "All",
- "core_chain": "Chain",
- "core_nfts": "NFTs",
- "core_coins": "Coins",
- "core_dapps": "dApps",
- "core_done": "Done",
- "core_empty_search": "No results found",
- "core_empty_transactions_history": "You don't have any transactions yet",
- "date_today": "Today",
- "date_yesterday": "Yesterday",
- "general_feed": "Feed",
- "general_videos": "Videos",
- "general_articles": "Articles",
- "feed_for_you": "For you",
- "feed_following": "Following",
- "feed_read_time_in_mins": "{mins} min read",
- "feed_trending_videos": "Trending Videos",
- "feed_modal_title": "Create value",
- "feed_modal_post": "Post",
- "feed_modal_post_description": "Voice your ideas",
- "feed_modal_story": "Story",
- "feed_modal_story_description": "Express the moment",
- "feed_modal_video": "Video",
- "feed_modal_video_description": "Show the world in motion",
- "feed_modal_article": "Article",
- "feed_repost_type": "Type",
- "feed_repost": "Repost",
- "feed_someone_reposted": "{someone} reposted",
- "feed_quote_post": "Quote post",
- "feed_write_comment": "Write comment",
- "feed_comment_hint": "Write your comment!",
- "feed_add_story": "Add to story",
- "feed_copy_link": "Copy link",
- "feed_whatsapp": "WhatsApp",
- "feed_telegram": "Telegram",
- "feed_x": "X",
- "feed_more": "More",
- "feed_send": "Send",
- "feed_modal_article_description": "Share your wisdom",
- "feed_share_via": "Share via message",
- "feed_search_empty": "Search here for users, hashtags, channels...",
- "feed_search_history_title": "Recent searches",
- "feed_search_history_delete_title": "Delete search history?",
- "feed_search_history_delete_message": "Are you sure you want to delete all search history?",
- "feed_search_filter_title": "Filters",
- "feed_search_filter_anyone": "From anyone",
- "feed_search_filter_following": "People you follow",
- "feed_search_filter_people": "People",
- "feed_search_filter_languages": "Languages",
- "feed_advanced_search_category_top": "Top",
- "feed_advanced_search_category_latest": "Latest",
- "feed_advanced_search_category_people": "People",
- "feed_advanced_search_category_photos": "Photos",
- "feed_advanced_search_category_videos": "Videos",
- "feed_advanced_search_category_groups": "Groups",
- "feed_advanced_search_category_channels": "Channels",
- "video_not_found": "Video not found",
- "turn_notifications_title": "Turn on notifications",
- "turn_notifications_description": "Receive notifications when you transfer and receive funds",
- "turn_notifications_receive": "Receive notifications when your sending or receiving assets",
- "turn_notifications_stay_up": "Stay up to date with the latest news",
- "turn_notifications_chat": "Chat and receive notifications even if the application is closed",
- "turn_notifications_sent_title": "Sent ICE",
- "turn_notifications_sent_description": "You sent 12.43 ICE to @james",
- "turn_notifications_sent_time": "15m ago",
- "turn_notifications_new_follower_title": "New follower",
- "turn_notifications_new_follower_description": "@curtis has started following you",
- "turn_notifications_new_follower_time": "24m ago",
- "turn_notifications_new_message_title": "New message",
- "turn_notifications_new_message_description": "@marie has sent you a message",
- "turn_notifications_new_message_time": "31m ago",
- "send_nft_navigation_title": "Send NFT",
- "send_nft_description": "Description",
- "send_nft_token_id": "Token ID",
- "send_nft_token_network": "Network",
- "send_nft_token_standard": "Token standard",
- "send_nft_token_contract_address": "Contract address",
- "send_nft_title": "Send NFT",
- "post_page_title": "Post",
- "post_show_replies": "Show replies",
- "post_hide_replies": "Hide replies",
- "post_reply_hint": "Write your reply",
- "post_replying_to": "Replying to",
- "post_reply_sent": "Your reply was sent",
- "send_nft_confirm_asset": "Asset",
- "send_nft_confirm_network": "Network",
- "send_nft_confirm_arrival_time": "Arrival time",
- "send_nft_confirm_network_fee": "Network fee",
- "transaction_details_title": "Transaction details",
- "transaction_details_view_on_explorer": "View on explorer",
- "post_menu_not_interested": "Not interested",
- "post_menu_follow_nickname": "Follow @{nickname}",
- "post_menu_unfollow_nickname": "Unfollow @{nickname}",
- "post_menu_block_nickname": "Block @{nickname}",
- "post_menu_report_post": "Report post",
- "protect_account_header_security": "Security",
- "protect_account_title_secure_account": "Secure your account",
- "protect_account_description_secure_account": "Securing your account ensures you never lose access to your data and funds",
- "protect_account_button": "Protect account",
- "protect_account_description_secure_account_2fa": "To secure your account, back it up and enable at least one 2FA option",
- "protect_account_create_recovery_error": "An error occurred while fetching the data.",
- "backup_title": "Select backup",
- "backup_description": "Backups enable you to restore your data and wallet if something goes wrong",
- "backup_option_with_icloud_title": "Backup with iCloud",
- "backup_option_with_google_drive_title": "Backup with Google Drive",
- "backup_option_with_google_drive_description": "Safe and simple way to protect your account",
- "backup_option_with_recovery_keys_title": "Recovery keys",
- "backup_option_with_recovery_keys_description": "Write down and store your keys on paper for secure account recovery",
- "secure_your_recovery_keys_title": "Secure your recovery keys",
- "secure_your_recovery_keys_description": "Please complete this process in a private place to ensure your account's safety",
- "recovery_keys_successfully_protected_title": "Successfully protected",
- "recovery_keys_successfully_protected_description": "Your recovery keys have been securely backed up. Please keep them safe for future account recovery",
- "error_recovery_keys_title": "Recovery keys error",
- "error_recovery_keys_description": "You have entered incorrect data",
- "error_screenshots_arent_secure_title": "Screenshots aren’t secure",
- "error_screenshots_arent_secure_description": "Anyone who has access to your keys can use your assets. We recommend writing with your hands.",
- "error_nickname_invalid": "Only letters, numbers, and dots are allowed",
- "error_passwords_are_not_equal": "Passwords are not equal",
- "error_website_invalid": "Invalid website url",
- "error_general_title": "Something went wrong",
- "error_general_description": "An unexpected error occurred. Please try again later. {info}",
- "error_general_error_code": "Error code: {error}",
- "error_input_length": "Must be over {amount} characters",
- "error_input_numbers": "Must contain 1 number",
- "error_input_all_cases": "Uppercase and lowercase letters",
- "error_input_special_character": "Must contain 1 special character",
- "error_identity_name_invalid": "Only lowercase letters, numbers, dots and hyphens are allowed",
- "warning_avoid_storing_keys": "Avoid storing keys on any device to prevent losing access to funds in case of a hack",
- "warning_authenticator_setup": "Keep this key safe to restore access if you lose your device.",
- "authenticator_setup_title": "Authenticator setup",
- "authenticator_setup_description": "In order to link the key, you need one of the following Authenticator applications",
- "authenticator_setup_key": "Setup key",
- "authenticator_is_linked_to_account": "Your account is linked to the authentication application",
- "authenticator_delete_title": "Deleting an authenticator",
- "authenticator_delete_description": "To delete the authenticator, you must confirm by selecting the options first",
- "authenticator_has_deleted": "The authenticator was successfully deleted",
- "follow_instructions_title": "Follow instructions",
- "follow_instructions_description": "Copy the installation key and paste it into your Authentication application",
- "confirm_the_code_title": "Confirm the code",
- "authenticator_protected_description": "Your authenticator was successfully configured and you are now protected",
- "email_verification_title": "Email verification",
- "email_verification_description": "Enter your email address for verification",
- "email_confirmation_title": "Confirm email",
- "email_success_description": "The email address has been successfully verified and added for 2FA",
- "phone_verification_title": "Phone number verification",
- "phone_verification_description": "Enter your phone number for verification",
- "phone_confirmation_title": "Confirm phone number",
- "phone_success_description": "The phone number has been successfully verified and added for 2FA",
- "phone_number": "Phone number",
- "phone_number_invalid": "Invalid phone number",
- "select_countries_nav_title": "Select country",
- "create_post_modal_title": "New post",
- "create_post_modal_placeholder": "What’s happening?",
- "create_article_nav_title": "New article",
- "create_article_title_placeholder": "Title",
- "create_article_add_cover": "Add cover image",
- "create_article_story_placeholder": "Write your story...",
- "create_video_edit_cover": "Edit cover",
- "create_video_new_video": "New video",
- "create_video_input_placeholder": "Add a description (optional)",
- "gallery_add_photo_title": "Add photo",
- "gallery_add_media_title": "Add media",
- "camera": "Camera",
- "visibility_settings_title_video": "Who can view this video",
- "visibility_settings_title_story": "Who can view this story",
- "visibility_settings_everyone": "Everyone",
- "visibility_settings_followed_accounts": "Accounts you follow",
- "visibility_settings_verified_accounts": "Verified accounts",
- "visibility_settings_mentioned_accounts": "Only accounts you mention",
- "schedule_modal_nav_title": "Set date and time",
- "cancel_creation_post_title": "Cancel post?",
- "cancel_creation_description": "Are you sure you want to cancel your progress?",
- "cancel_creation_article_title": "Cancel article?",
- "photo_library_require_access_title": "\"{appName} app\" would like to access your photo library",
- "photo_library_require_access_description": "This lets you share photos from your library and save photos to your camera roll.",
- "camera_require_access_title": "\"{appName} app\" would like to access your camera",
- "camera_require_access_description": "{appName} application would like to access the camera",
- "push_notifications_require_access_title": "\"{appName} app\" would like to Send You Notifications",
- "push_notifications_require_access_description": "Notifications may include alerts, sounds, and icon badges. These can be configured is Settings.",
- "gallery_permission_headline": "Gallery permission",
- "gallery_no_access_title": "There is no access to your gallery",
- "camera_permission_headline": "Camera permission",
- "camera_no_access_title": "There is no access to your camera",
- "camera_no_access_description": "To grant access, go to settings and enable the appropriate settings",
- "push_notifications_permission_headline": "Notifications permission",
- "push_notifications_no_access_title": "Permission Not Available",
- "push_notifications_no_access_description": "You have previously denied notification permission. Please go to settings to enable notifications.",
- "story_preview_title": "Story preview",
- "story_settings_title": "Who can reply this story",
- "article_settings_title": "Who can reply this article",
- "chat_title": "Chats",
- "chat_empty_description": "You have no conversations yet",
- "chat_new_message_button": "New message",
- "poll_length_modal_title": "Pool length time",
- "poll_length_button_title": "Poll length",
- "poll_add_answer_button_title": "Add answer",
- "poll_choice_placeholder": "Choice {number}",
- "poll_title_placeholder": "Write a question for the poll",
- "chat_recents_money_request_message": "Money requested",
- "toolbar_link_title": "Add a link",
- "toolbar_link_placeholder": "http://",
- "messaging_empty_description": "Messages and voices are end-to-end encrypted.",
- "chat_modal_title": "Start conversation",
- "chat_modal_private_description": "Start a private, one-on-one chat",
- "chat_modal_group_description": "Chat with multiple people together",
- "chat_modal_channel_description": "Share updates with a wide audience",
- "chat_profile_share_modal_title": "Share profile",
- "new_chat_modal_title": "New chat",
- "new_chat_modal_description": "Search above for users, groups, and channels...",
- "new_chat_modal_new_group_button": "New group",
- "new_chat_modal_new_channel_button": "New channel",
- "chat_read_all": "Read All",
- "chat_delete_modal_title": "Delete chat?",
- "chat_delete_modal_description": "Are you sure you want to delete all selected chats?",
- "chat_search_empty": "Search here for users, chats, groups, and channels...",
- "chat_money_request_title": "Money requested",
- "chat_money_received_title": "Money received",
- "chat_money_received_button": "View transaction",
- "chat_profile_share_button": "Write a message",
- "chat_learn_more_modal_title": "Privacy First, Always",
- "chat_learn_more_modal_description": "Your chats are private and encrypted by default. We prioritize your privacy, and your conversations are protected with end-to-end encryption.",
- "chat_add_poll_title": "New poll",
- "channel_create_title": "Create a new channel",
- "channel_create_type": "Channel type",
- "channel_create_admins": "Channel admins",
- "channel_create_action": "Create channel",
- "channel_create_add_photo": "Add channel photo",
- "channel_create_type_select_title": "Choose channel type",
- "channel_create_type_public_desc": "Public channels are searchable and open to all users.",
- "channel_create_type_private_desc": "Private channels can't be found in search and aren't end-to-end encrypted.",
- "channel_create_admins_title": "Admin management",
- "channel_create_admins_action": "Add administrator",
- "channel_create_admin_type_title": "Choose admin type",
- "channel_create_admin_type_owner": "Owner",
- "channel_create_admin_type_admin": "Admin",
- "channel_create_admin_type_moderator": "Moderator",
- "channel_create_admin_type_remove": "Remove role",
- "channel_create_admin_type_remove_title": "Remove role?",
- "channel_create_admin_type_remove_desc": "Are you sure you want to remove the role?",
- "channel_created_message": "The channel has been created",
- "group_create_title": "New Group",
- "group_create_name_label": "Group Name",
- "group_create_type": "Group type",
- "group_create_members_number": "Group members ({members})",
- "group_create_create_button": "Create group",
- "group_create_type_title": "Choose group type",
- "group_create_type_public_desc": "Public channels are searchable and open to all users.",
- "group_create_type_private_desc": "Private channels can't be found in search and aren't end-to-end encrypted.",
- "group_create_type_encrypted": "Encrypted",
- "group_create_type_encrypted_desc": "End-to-end encryption is used. Only you and the people you communicate with can see the messages.",
- "group_created_message": "The group has been created",
- "notification_video_loading": "Your video is loading...",
- "notification_story_loading": "Your story is loading...",
- "notification_post_loading": "Your post is loading...",
- "notification_article_loading": "Your article is loading...",
- "notification_reply_loading": "Your reply is loading...",
- "notification_repost_loading": "Your repost is loading...",
- "notification_video_published": "Your video has been published",
- "notification_story_published": "Your story has been published",
- "notification_post_published": "Your post has been published",
- "notification_article_published": "Your article has been published",
- "notification_reply_published": "Your reply has been published",
- "notification_repost_successful": "Successfully reposted",
- "chat_groups_joined": "Joined",
- "chat_groups_explore": "Explore",
- "chat_groups_subscribed": "Subscribed",
- "code_block_type_plain_text": "Plain text",
- "code_block_type_swift": "Swift",
- "code_block_type_c": "C",
- "code_block_type_c_plus_plus": "C++",
- "code_block_type_c_sharp": "C#",
- "code_block_type_css": "CSS",
- "code_block_type_java": "Java",
- "code_block_type_javascript": "JavaScript",
- "code_block_type_python": "Python",
- "code_block_type_dart": "Dart",
- "write_a_message": "Write a message...",
- "reaction_was_sent": "Reaction was sent",
- "share_via": "Share via...",
- "topic_blockchain": "Blockchain",
- "topic_business": "Business",
- "topic_cryptocurrency": "Cryptocurrency",
- "topic_data_science": "Data Science",
- "topic_finance": "Finance",
- "topic_games": "Games",
- "topic_style": "Style",
- "topic_lifechange": "Life Change",
- "topic_life": "Life",
- "topic_trading": "Trading",
- "topic_technology": "Technology",
- "topic_travel": "Travel",
- "topic_news": "News",
- "topic_people": "People",
- "topic_world": "World",
- "topics_add": "Add",
- "topics_title": "Topics",
- "article_preview_title": "Article preview",
- "article_page_title": "Article",
- "article_page_from_author": "from {name}",
- "update_update_title": "Please update",
- "update_update_desc": "You are using an old app version and need to update it in order to use it further",
- "update_update_action": "Update now",
- "update_uptodate_title": "You are up to date",
- "update_uptodate_desc": "You are now using the latest version. Check all the changes below",
- "update_uptodate_action": "View changelog",
- "emoji_category_smileys_people": "Smileys & People",
- "emoji_category_animals_nature": "Animals & Nature",
- "emoji_category_food_drink": "Food & Drink",
- "emoji_category_activities": "Activities",
- "emoji_category_travel_places": "Travel & Places",
- "emoji_category_objects": "Objects",
- "emoji_category_symbols": "Symbols",
- "emoji_category_flags": "Flags",
- "recent_emoji_reactions": "Recent reactions",
- "passkeys_prompt_title": "Verify with passkey",
- "passkeys_prompt_description": "Your device will ask your fingerprint, face or screen lock to confirm",
- "verify_with_password_title": "Verify with password",
- "verify_with_password_desc": "Please confirm your password to continue.",
- "verify_with_password_prompt_desc": "Your device will ask your password to confirm",
- "verify_with_biometrics_title": "Verify with biometrics",
-<<<<<<< Updated upstream
- "members_count": "{count, plural, =1 {{count} member} other {{count} members}}",
- "all_chains_item": "All chains"
-=======
- "biometrics_suggestion_title": "Add biometric authentication",
- "biometrics_suggestion_desc": "Do you want to use your device biometric data for a faster authentication?",
- "members_count": "{count, plural, =1 {{count} member} other {{count} members}}"
->>>>>>> Stashed changes
-}
diff --git a/packages/ion_identity_client/lib/src/auth/services/create_credentials/create_new_credentials_service.dart b/packages/ion_identity_client/lib/src/auth/services/create_credentials/create_new_credentials_service.dart
index 7fdbaa1c6..1fb3f4eef 100644
--- a/packages/ion_identity_client/lib/src/auth/services/create_credentials/create_new_credentials_service.dart
+++ b/packages/ion_identity_client/lib/src/auth/services/create_credentials/create_new_credentials_service.dart
@@ -52,6 +52,23 @@ class CreateNewCredentialsService {
),
);
},
+ onBiometricsFlow: ({required String localisedReason}) {
+ return identitySigner.registerWithPasskey(
+ UserRegistrationChallenge(
+ null,
+ credentialChallenge.rp,
+ credentialChallenge.user,
+ null,
+ null,
+ credentialChallenge.challenge,
+ credentialChallenge.authenticatorSelection,
+ credentialChallenge.attestation,
+ credentialChallenge.pubKeyCredParams,
+ credentialChallenge.excludeCredentials ?? [],
+ null,
+ ),
+ );
+ },
);
final credentialRequest = dataSource.buildCreateCredentialSigningRequest(
diff --git a/packages/ion_identity_client/lib/src/auth/services/create_credentials/create_recovery_credentials_service.dart b/packages/ion_identity_client/lib/src/auth/services/create_credentials/create_recovery_credentials_service.dart
index 99d2bc182..0c78c47b8 100644
--- a/packages/ion_identity_client/lib/src/auth/services/create_credentials/create_recovery_credentials_service.dart
+++ b/packages/ion_identity_client/lib/src/auth/services/create_credentials/create_recovery_credentials_service.dart
@@ -63,6 +63,13 @@ class CreateRecoveryCredentialsService {
CredentialResponse.fromJson,
);
},
+ onBiometricsFlow: ({required String localisedReason}) {
+ return userActionSigner.signWithBiometrics(
+ credentialRequest,
+ CredentialResponse.fromJson,
+ localisedReason,
+ );
+ },
);
return CreateRecoveryCredentialsSuccess(
diff --git a/packages/ion_identity_client/lib/src/auth/services/key_service.dart b/packages/ion_identity_client/lib/src/auth/services/key_service.dart
index 4fa4e2103..01c65ae0a 100644
--- a/packages/ion_identity_client/lib/src/auth/services/key_service.dart
+++ b/packages/ion_identity_client/lib/src/auth/services/key_service.dart
@@ -4,6 +4,7 @@ import 'dart:convert';
import 'dart:typed_data';
import 'package:asn1lib/asn1lib.dart';
+import 'package:convert/convert.dart';
import 'package:cryptography/cryptography.dart' as crypto;
import 'package:ion_identity_client/src/auth/dtos/key_pair_data.dart';
@@ -31,6 +32,34 @@ class KeyService {
);
}
+ /// Reconstructs a KeyPairData object from a hex-encoded Ed25519 private key (seed).
+ Future reconstructKeyPairFromPrivateKeyBytes(String hexEncodedPrivateKeyBytes) async {
+ final privateKeyBytes = Uint8List.fromList(hex.decode(hexEncodedPrivateKeyBytes));
+
+ if (privateKeyBytes.length != 32) {
+ throw ArgumentError(
+ 'Invalid private key seed length: expected 32 bytes, got ${privateKeyBytes.length}',
+ );
+ }
+
+ final algorithm = crypto.Ed25519();
+ final keyPair = await algorithm.newKeyPairFromSeed(privateKeyBytes);
+ final keyPairData = await keyPair.extract();
+ final publicKey = keyPairData.publicKey;
+
+ // Convert to PEM
+ final publicKeyPem = _encodeEd25519PublicKeyToPem(Uint8List.fromList(publicKey.bytes));
+ final privateKeyPem = _encodeEd25519PrivateKeyToPem(privateKeyBytes);
+
+ return KeyPairData(
+ keyPair: keyPairData,
+ publicKey: publicKey,
+ publicKeyPem: publicKeyPem,
+ privateKeyPem: privateKeyPem,
+ privateKeyBytes: privateKeyBytes,
+ );
+ }
+
Future reconstructKeyPairFromEncryptedPrivateKey(
String encryptedPrivateKey,
String recoveryCode,
diff --git a/packages/ion_identity_client/lib/src/auth/services/twofa/twofa_service.dart b/packages/ion_identity_client/lib/src/auth/services/twofa/twofa_service.dart
index e2f5dc6cd..b0da0164a 100644
--- a/packages/ion_identity_client/lib/src/auth/services/twofa/twofa_service.dart
+++ b/packages/ion_identity_client/lib/src/auth/services/twofa/twofa_service.dart
@@ -117,6 +117,13 @@ class TwoFAService {
hash.toString(),
);
},
+ onBiometricsFlow: ({required String localisedReason}) {
+ return _wallets.generateHashSignatureWithBiometrics(
+ mainWallet.id,
+ hash.toString(),
+ localisedReason,
+ );
+ },
);
final signature = signatureResponse.signature['encoded'].toString().substring(2);
diff --git a/packages/ion_identity_client/lib/src/core/types/ion_exception.dart b/packages/ion_identity_client/lib/src/core/types/ion_exception.dart
index e4eea5d5f..601c06250 100644
--- a/packages/ion_identity_client/lib/src/core/types/ion_exception.dart
+++ b/packages/ion_identity_client/lib/src/core/types/ion_exception.dart
@@ -30,6 +30,10 @@ class PasskeyValidationException extends IONIdentityException {
const PasskeyValidationException() : super('Passkey validation failed');
}
+class BiometricsValidationException extends IONIdentityException {
+ const BiometricsValidationException() : super('Biometrics validation failed');
+}
+
class PasswordFlowNotAvailableForTheUserException extends IONIdentityException {
const PasswordFlowNotAvailableForTheUserException()
: super('Password flow is not available for this user');
diff --git a/packages/ion_identity_client/lib/src/core/types/types.dart b/packages/ion_identity_client/lib/src/core/types/types.dart
index 0a0b271cc..cdb898b4f 100644
--- a/packages/ion_identity_client/lib/src/core/types/types.dart
+++ b/packages/ion_identity_client/lib/src/core/types/types.dart
@@ -4,8 +4,10 @@ typedef JsonObject = Map;
typedef OnPasswordFlow = Future Function({required String password});
typedef OnPasskeyFlow = Future Function();
+typedef OnBiometricsFlow = Future Function({required String localisedReason});
typedef OnVerifyIdentity = Future Function({
+ required OnBiometricsFlow onBiometricsFlow,
required OnPasswordFlow onPasswordFlow,
required OnPasskeyFlow onPasskeyFlow,
});
diff --git a/packages/ion_identity_client/lib/src/signer/identity_signer.dart b/packages/ion_identity_client/lib/src/signer/identity_signer.dart
index e0d7df8a0..687e4c74c 100644
--- a/packages/ion_identity_client/lib/src/signer/identity_signer.dart
+++ b/packages/ion_identity_client/lib/src/signer/identity_signer.dart
@@ -50,7 +50,7 @@ class IdentitySigner {
required String credentialId,
required CredentialKind credentialKind,
}) async {
- return passwordSigner.createCredentialAssertion(
+ return passwordSigner.signWithPassword(
challenge: challenge,
encryptedPrivateKey: encryptedPrivateKey,
password: password,
@@ -59,6 +59,22 @@ class IdentitySigner {
);
}
+ Future signWithBiometrics({
+ required String username,
+ required String localisedReason,
+ required String challenge,
+ required String credentialId,
+ required CredentialKind credentialKind,
+ }) async {
+ return passwordSigner.signWithBiometrics(
+ username: username,
+ localisedReason: localisedReason,
+ challenge: challenge,
+ credentialKind: credentialKind,
+ credentialId: credentialId,
+ );
+ }
+
Future isPasskeyAvailable() {
return passkeySigner.canAuthenticate();
}
diff --git a/packages/ion_identity_client/lib/src/signer/password_signer.dart b/packages/ion_identity_client/lib/src/signer/password_signer.dart
index b8fdcd1ac..182d64e19 100644
--- a/packages/ion_identity_client/lib/src/signer/password_signer.dart
+++ b/packages/ion_identity_client/lib/src/signer/password_signer.dart
@@ -90,7 +90,7 @@ class PasswordSigner {
);
}
- Future createCredentialAssertion({
+ Future signWithPassword({
required String challenge,
required String encryptedPrivateKey,
required String password,
@@ -99,25 +99,43 @@ class PasswordSigner {
}) async {
final keyPair =
await keyService.reconstructKeyPairFromEncryptedPrivateKey(encryptedPrivateKey, password);
- final clientData = _buildClientData(
+ return _createCredentialAssertion(
+ keyPair: keyPair,
challenge: challenge,
- origin: config.origin,
- clientDataType: ClientDataType.getKey,
+ credentialId: credentialId,
+ credentialKind: credentialKind,
);
+ }
- final signature = await _signDataWithPrivateKey(
- data: clientData,
- privateKey: keyPair.keyPair,
- signatureEncryption: SignatureEncryption.base64Url,
+ Future signWithBiometrics({
+ required String challenge,
+ required String credentialId,
+ required String username,
+ required String localisedReason,
+ required CredentialKind credentialKind,
+ }) async {
+ final biometricsState = biometricsStateStorage.getBiometricsState(username: username);
+ if (biometricsState != BiometricsState.enabled) {
+ throw const BiometricsValidationException();
+ }
+ final privateKey = privateKeyStorage.getPrivateKey(
+ username: username,
);
+ if (privateKey == null) {
+ throw const BiometricsValidationException();
+ }
- return AssertionRequestData(
- kind: credentialKind,
- credentialAssertion: CredentialAssertionData(
- clientData: base64UrlEncode(utf8.encode(clientData)),
- credId: credentialId,
- signature: signature,
- ),
+ final didAuthenticate = await _authWithBiometrics(localisedReason: localisedReason);
+ if (didAuthenticate == false) {
+ throw const BiometricsValidationException();
+ }
+
+ final keyPair = await keyService.reconstructKeyPairFromPrivateKeyBytes(privateKey);
+ return _createCredentialAssertion(
+ keyPair: keyPair,
+ challenge: challenge,
+ credentialId: credentialId,
+ credentialKind: credentialKind,
);
}
@@ -132,22 +150,24 @@ class PasswordSigner {
required String username,
required String localisedReason,
}) async {
- final auth = LocalAuthentication();
+ final didAuthenticate = await _authWithBiometrics(localisedReason: localisedReason);
+ await biometricsStateStorage.updateBiometricsState(
+ username: username,
+ biometricsState: didAuthenticate ? BiometricsState.enabled : BiometricsState.failed,
+ );
+ }
+
+ Future _authWithBiometrics({
+ required String localisedReason,
+ }) async {
+ final localAuth = LocalAuthentication();
try {
- final didAuthenticate = await auth.authenticate(
+ return await localAuth.authenticate(
localizedReason: localisedReason,
options: const AuthenticationOptions(stickyAuth: true),
);
- await biometricsStateStorage.updateBiometricsState(
- username: username,
- biometricsState: didAuthenticate ? BiometricsState.enabled : BiometricsState.failed,
- );
} catch (_) {
- await biometricsStateStorage.updateBiometricsState(
- username: username,
- biometricsState: BiometricsState.failed,
- );
- rethrow;
+ return false;
}
}
@@ -271,4 +291,32 @@ class PasswordSigner {
? formattedStr.substring(0, formattedStr.length - 1)
: formattedStr;
}
+
+ Future _createCredentialAssertion({
+ required KeyPairData keyPair,
+ required String challenge,
+ required String credentialId,
+ required CredentialKind credentialKind,
+ }) async {
+ final clientData = _buildClientData(
+ challenge: challenge,
+ origin: config.origin,
+ clientDataType: ClientDataType.getKey,
+ );
+
+ final signature = await _signDataWithPrivateKey(
+ data: clientData,
+ privateKey: keyPair.keyPair,
+ signatureEncryption: SignatureEncryption.base64Url,
+ );
+
+ return AssertionRequestData(
+ kind: credentialKind,
+ credentialAssertion: CredentialAssertionData(
+ clientData: base64UrlEncode(utf8.encode(clientData)),
+ credId: credentialId,
+ signature: signature,
+ ),
+ );
+ }
}
diff --git a/packages/ion_identity_client/lib/src/signer/user_action_signer.dart b/packages/ion_identity_client/lib/src/signer/user_action_signer.dart
index 01efc2f14..4da898661 100644
--- a/packages/ion_identity_client/lib/src/signer/user_action_signer.dart
+++ b/packages/ion_identity_client/lib/src/signer/user_action_signer.dart
@@ -52,11 +52,7 @@ class UserActionSigner {
// The assertion here is obtained by using the password to unlock
// a password-protected key. If this key is unavailable, an exception is thrown.
obtainAssertion: (challenge) async {
- final credentialDescriptor = challenge.allowCredentials.passwordProtectedKey?.firstOrNull;
- // If no password-protected credential is available, throw an exception.
- if (credentialDescriptor == null || credentialDescriptor.encryptedPrivateKey == null) {
- throw const PasswordFlowNotAvailableForTheUserException();
- }
+ final credentialDescriptor = _extractPasswordProtectedCredentials(challenge);
return identitySigner.signWithPassword(
challenge: challenge.challenge,
@@ -69,6 +65,40 @@ class UserActionSigner {
);
}
+ Future signWithBiometrics(
+ UserActionSigningRequest request,
+ T Function(JsonObject) responseDecoder,
+ String localisedReason,
+ ) async {
+ return _sign(
+ request: request,
+ responseDecoder: responseDecoder,
+ obtainAssertion: (challenge) async {
+ final credentialDescriptor = _extractPasswordProtectedCredentials(challenge);
+
+ return identitySigner.signWithBiometrics(
+ challenge: challenge.challenge,
+ username: request.username,
+ credentialId: credentialDescriptor.id,
+ credentialKind: CredentialKind.PasswordProtectedKey,
+ localisedReason: localisedReason,
+ );
+ },
+ );
+ }
+
+ PublicKeyCredentialDescriptor _extractPasswordProtectedCredentials(
+ UserActionChallenge challenge,
+ ) {
+ final credentialDescriptor = challenge.allowCredentials.passwordProtectedKey?.firstOrNull;
+ // If no password-protected credential is available, throw an exception.
+ if (credentialDescriptor == null || credentialDescriptor.encryptedPrivateKey == null) {
+ throw const PasswordFlowNotAvailableForTheUserException();
+ }
+
+ return credentialDescriptor;
+ }
+
/// A private helper method that encapsulates the shared logic for both signWithPasskey and signWithPassword.
///
/// Steps:
diff --git a/packages/ion_identity_client/lib/src/wallets/ion_identity_wallets.dart b/packages/ion_identity_client/lib/src/wallets/ion_identity_wallets.dart
index 746a5b6c7..46e9a1119 100644
--- a/packages/ion_identity_client/lib/src/wallets/ion_identity_wallets.dart
+++ b/packages/ion_identity_client/lib/src/wallets/ion_identity_wallets.dart
@@ -124,4 +124,15 @@ class IONIdentityWallets {
hash: hash,
password: password,
);
+
+ Future generateHashSignatureWithBiometrics(
+ String walletId,
+ String hash,
+ String localisedReason,
+ ) =>
+ _generateSignatureService.generateHashSignatureWithBiometrics(
+ walletId: walletId,
+ hash: hash,
+ localisedReason: localisedReason,
+ );
}
diff --git a/packages/ion_identity_client/lib/src/wallets/services/create_wallet/create_wallet_service.dart b/packages/ion_identity_client/lib/src/wallets/services/create_wallet/create_wallet_service.dart
index c93f59f30..b25c576c1 100644
--- a/packages/ion_identity_client/lib/src/wallets/services/create_wallet/create_wallet_service.dart
+++ b/packages/ion_identity_client/lib/src/wallets/services/create_wallet/create_wallet_service.dart
@@ -41,6 +41,13 @@ class CreateWalletService {
Wallet.fromJson,
);
},
+ onBiometricsFlow: ({required String localisedReason}) {
+ return _userActionSigner.signWithBiometrics(
+ request,
+ Wallet.fromJson,
+ localisedReason,
+ );
+ },
);
}
}
diff --git a/packages/ion_identity_client/lib/src/wallets/services/generate_signature/generate_signature_service.dart b/packages/ion_identity_client/lib/src/wallets/services/generate_signature/generate_signature_service.dart
index 9e7b606ab..c7477b173 100644
--- a/packages/ion_identity_client/lib/src/wallets/services/generate_signature/generate_signature_service.dart
+++ b/packages/ion_identity_client/lib/src/wallets/services/generate_signature/generate_signature_service.dart
@@ -51,6 +51,24 @@ class GenerateSignatureService {
);
}
+ Future generateHashSignatureWithBiometrics({
+ required String walletId,
+ required String hash,
+ required String localisedReason,
+ String? externalId,
+ }) async {
+ return _generateSignature(
+ walletId: walletId,
+ hash: hash,
+ externalId: externalId,
+ signFn: (request) => _userActionSigner.signWithBiometrics(
+ request,
+ GenerateSignatureResponse.fromJson,
+ localisedReason,
+ ),
+ );
+ }
+
Future generateMessageSignatureWithPasskey({
required String walletId,
required String message,
From 5818e03493ae87d9f43eb3f1c47c0dee5a387fd3 Mon Sep 17 00:00:00 2001
From: Ice Hades <119406114+ice-hades@users.noreply.github.com>
Date: Thu, 9 Jan 2025 22:25:37 +0400
Subject: [PATCH 3/6] chore: changes after review
---
ios/Runner/Info.plist | 2 +-
.../create_new_credentials_service.dart | 16 +---------------
2 files changed, 2 insertions(+), 16 deletions(-)
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 5cc077ba8..edf36eaa4 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -69,7 +69,7 @@
This app needs access to your photo library to save images.
NSMicrophoneUsageDescription
This app requires microphone access for recording audio.
- NSFaceIDUsageDescription
+ NSFaceIDUsageDescription
This app uses Face ID to authenticate the user.
diff --git a/packages/ion_identity_client/lib/src/auth/services/create_credentials/create_new_credentials_service.dart b/packages/ion_identity_client/lib/src/auth/services/create_credentials/create_new_credentials_service.dart
index 1fb3f4eef..514715136 100644
--- a/packages/ion_identity_client/lib/src/auth/services/create_credentials/create_new_credentials_service.dart
+++ b/packages/ion_identity_client/lib/src/auth/services/create_credentials/create_new_credentials_service.dart
@@ -53,21 +53,7 @@ class CreateNewCredentialsService {
);
},
onBiometricsFlow: ({required String localisedReason}) {
- return identitySigner.registerWithPasskey(
- UserRegistrationChallenge(
- null,
- credentialChallenge.rp,
- credentialChallenge.user,
- null,
- null,
- credentialChallenge.challenge,
- credentialChallenge.authenticatorSelection,
- credentialChallenge.attestation,
- credentialChallenge.pubKeyCredParams,
- credentialChallenge.excludeCredentials ?? [],
- null,
- ),
- );
+ throw UnimplementedError('Cannot register with biometrics');
},
);
From e2550c85cb2b6de4bf703b6e9685b6fe9a58ea53 Mon Sep 17 00:00:00 2001
From: Ice Hades <119406114+ice-hades@users.noreply.github.com>
Date: Fri, 10 Jan 2025 12:50:36 +0400
Subject: [PATCH 4/6] chore: changes after review
---
lib/app/features/user/providers/biometrics_provider.c.dart | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/app/features/user/providers/biometrics_provider.c.dart b/lib/app/features/user/providers/biometrics_provider.c.dart
index 6869405cf..baab8bb7d 100644
--- a/lib/app/features/user/providers/biometrics_provider.c.dart
+++ b/lib/app/features/user/providers/biometrics_provider.c.dart
@@ -12,7 +12,7 @@ Future userBiometricsState(
Ref ref, {
required String username,
}) async {
- final ionIdentity = await ref.read(ionIdentityProvider.future);
+ final ionIdentity = await ref.watch(ionIdentityProvider.future);
return ionIdentity(username: username).auth.getBiometricsState();
}
From f609276cf59353ee3ad90afbc09ea9d09baaf96d Mon Sep 17 00:00:00 2001
From: ice-hades <119406114+ice-hades@users.noreply.github.com>
Date: Fri, 10 Jan 2025 13:46:00 +0400
Subject: [PATCH 5/6] Update Info.plist
---
ios/Runner/Info.plist | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index edf36eaa4..a17ba3571 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -69,8 +69,8 @@
This app needs access to your photo library to save images.
NSMicrophoneUsageDescription
This app requires microphone access for recording audio.
- NSFaceIDUsageDescription
- This app uses Face ID to authenticate the user.
+ NSFaceIDUsageDescription
+ This app uses Face ID to authenticate the user.
From e3ca65d9b5f8fa940c29cfacb25cada45b2ca58c Mon Sep 17 00:00:00 2001
From: ion-endymion <188437551+ice-endymion@users.noreply.github.com>
Date: Fri, 10 Jan 2025 10:55:02 +0100
Subject: [PATCH 6/6] chore: format Info.plist
---
ios/Runner/Info.plist | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index a17ba3571..f9752c196 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -69,8 +69,8 @@
This app needs access to your photo library to save images.
NSMicrophoneUsageDescription
This app requires microphone access for recording audio.
- NSFaceIDUsageDescription
- This app uses Face ID to authenticate the user.
+ NSFaceIDUsageDescription
+ This app uses Face ID to authenticate the user.