diff --git a/Mixin.xcodeproj/project.pbxproj b/Mixin.xcodeproj/project.pbxproj index bb9d5e0aa..d592bef5d 100644 --- a/Mixin.xcodeproj/project.pbxproj +++ b/Mixin.xcodeproj/project.pbxproj @@ -942,6 +942,15 @@ 9459693628B28F18006F5421 /* TIPNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9459693528B28F18006F5421 /* TIPNavigationController.swift */; }; 9459693928B28FF8006F5421 /* TIPActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9459693728B28FF8006F5421 /* TIPActionViewController.swift */; }; 9459693A28B28FF8006F5421 /* TIPActionView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9459693828B28FF8006F5421 /* TIPActionView.xib */; }; + 945A72782E59BC48006A11E9 /* TIPQuizView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 945A72772E59BC48006A11E9 /* TIPQuizView.xib */; }; + 945A72792E59BC48006A11E9 /* TIPQuizViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945A72762E59BC48006A11E9 /* TIPQuizViewController.swift */; }; + 945A72882E59BE1F006A11E9 /* TIPQuizQuestionCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 945A72872E59BE1F006A11E9 /* TIPQuizQuestionCell.xib */; }; + 945A72892E59BE1F006A11E9 /* TIPQuizQuestionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945A72862E59BE1F006A11E9 /* TIPQuizQuestionCell.swift */; }; + 945A72922E59BE2E006A11E9 /* TIPQuizAnswerCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 945A72912E59BE2E006A11E9 /* TIPQuizAnswerCell.xib */; }; + 945A72932E59BE2E006A11E9 /* TIPQuizAnswerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945A72902E59BE2E006A11E9 /* TIPQuizAnswerCell.swift */; }; + 945A72C02E59C8BF006A11E9 /* TIPQuizAnswer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945A72BF2E59C8BB006A11E9 /* TIPQuizAnswer.swift */; }; + 945A72E12E59EE38006A11E9 /* TIPQuizAnswerView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 945A72E02E59EE38006A11E9 /* TIPQuizAnswerView.xib */; }; + 945A72E22E59EE38006A11E9 /* TIPQuizAnswerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945A72DF2E59EE38006A11E9 /* TIPQuizAnswerViewController.swift */; }; 945AA6582567B18200B39415 /* Country.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945AA6572567B18200B39415 /* Country.swift */; }; 945C59102DC215A90024F6DE /* MembershipViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945C590F2DC215A50024F6DE /* MembershipViewController.swift */; }; 945C59192DC215F60024F6DE /* MembershipCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945C59172DC215F60024F6DE /* MembershipCell.swift */; }; @@ -2627,6 +2636,15 @@ 9459693528B28F18006F5421 /* TIPNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TIPNavigationController.swift; sourceTree = ""; }; 9459693728B28FF8006F5421 /* TIPActionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TIPActionViewController.swift; sourceTree = ""; }; 9459693828B28FF8006F5421 /* TIPActionView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TIPActionView.xib; sourceTree = ""; }; + 945A72762E59BC48006A11E9 /* TIPQuizViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TIPQuizViewController.swift; sourceTree = ""; }; + 945A72772E59BC48006A11E9 /* TIPQuizView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TIPQuizView.xib; sourceTree = ""; }; + 945A72862E59BE1F006A11E9 /* TIPQuizQuestionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TIPQuizQuestionCell.swift; sourceTree = ""; }; + 945A72872E59BE1F006A11E9 /* TIPQuizQuestionCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TIPQuizQuestionCell.xib; sourceTree = ""; }; + 945A72902E59BE2E006A11E9 /* TIPQuizAnswerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TIPQuizAnswerCell.swift; sourceTree = ""; }; + 945A72912E59BE2E006A11E9 /* TIPQuizAnswerCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TIPQuizAnswerCell.xib; sourceTree = ""; }; + 945A72BF2E59C8BB006A11E9 /* TIPQuizAnswer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TIPQuizAnswer.swift; sourceTree = ""; }; + 945A72DF2E59EE38006A11E9 /* TIPQuizAnswerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TIPQuizAnswerViewController.swift; sourceTree = ""; }; + 945A72E02E59EE38006A11E9 /* TIPQuizAnswerView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TIPQuizAnswerView.xib; sourceTree = ""; }; 945AA6572567B18200B39415 /* Country.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Country.swift; sourceTree = ""; }; 945C590F2DC215A50024F6DE /* MembershipViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MembershipViewController.swift; sourceTree = ""; }; 945C59172DC215F60024F6DE /* MembershipCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MembershipCell.swift; sourceTree = ""; }; @@ -4709,6 +4727,15 @@ 94C359F428B506D40010F53C /* TIPFullscreenInputViewController.swift */, 9459693728B28FF8006F5421 /* TIPActionViewController.swift */, 943A63E228BDEC27008E0F78 /* TIPPopupInputViewController.swift */, + 945A72BF2E59C8BB006A11E9 /* TIPQuizAnswer.swift */, + 945A72872E59BE1F006A11E9 /* TIPQuizQuestionCell.xib */, + 945A72862E59BE1F006A11E9 /* TIPQuizQuestionCell.swift */, + 945A72912E59BE2E006A11E9 /* TIPQuizAnswerCell.xib */, + 945A72902E59BE2E006A11E9 /* TIPQuizAnswerCell.swift */, + 945A72772E59BC48006A11E9 /* TIPQuizView.xib */, + 945A72762E59BC48006A11E9 /* TIPQuizViewController.swift */, + 945A72E02E59EE38006A11E9 /* TIPQuizAnswerView.xib */, + 945A72DF2E59EE38006A11E9 /* TIPQuizAnswerViewController.swift */, ); path = PIN; sourceTree = ""; @@ -6219,6 +6246,7 @@ 94CD471D2BECAE9D00A3DE10 /* InscriptionHashCell.xib in Resources */, 7B833E9D22C4AED20063929D /* ic_app_loading@3x.webp in Resources */, 946983B529D2A0DE00E73428 /* NetworkFeeOptionCell.xib in Resources */, + 945A72782E59BC48006A11E9 /* TIPQuizView.xib in Resources */, 274264A920011C79000A8808 /* mixin.caf in Resources */, 94317D082AFBE57200FDC9C9 /* MemoCell.xib in Resources */, 94095BB725BE8BC100F44832 /* TextPreviewView.xib in Resources */, @@ -6338,6 +6366,7 @@ 9417EF352DC3549C005079CB /* MembershipBenefitCell.xib in Resources */, 941979552BA47782002BA19F /* Web3MessageCell.xib in Resources */, 7B595359226749A100D59DB4 /* PeerCell.xib in Resources */, + 945A72E12E59EE38006A11E9 /* TIPQuizAnswerView.xib in Resources */, 948723392C9DF6380034BCB0 /* AddMarketAlertView.xib in Resources */, 940EF8B12C8AE573000AD1C6 /* ShareMarketAsPictureView.xib in Resources */, 94DC24392D08051C006501B3 /* DesktopSessionValidationView.xib in Resources */, @@ -6421,6 +6450,7 @@ 94DD0D882DC327ED00C70CA2 /* MembershipPlanSelectorCell.xib in Resources */, 7BEC4DF121A7D054003336F2 /* AssetCell.xib in Resources */, 949A3686261D9C5C004251B2 /* post.css in Resources */, + 945A72922E59BE2E006A11E9 /* TIPQuizAnswerCell.xib in Resources */, 7C0E15E1270053AC002FC718 /* UnknownURLWindow.xib in Resources */, 94E978B32D09818B00FFCC12 /* ClockSkewView.xib in Resources */, 94CA5C572CE5F5F80086C484 /* PaymentCollectibleCell.xib in Resources */, @@ -6486,6 +6516,7 @@ 7C952D0027E036240083F92B /* ExpiredMessageTableHeaderView.xib in Resources */, 94CB85DA2C6B4EC600135641 /* TokenInfoCell.xib in Resources */, 94902E462DA6CD6D00E52FC6 /* SimpleWeb3TransactionTableHeaderView.xib in Resources */, + 945A72882E59BE1F006A11E9 /* TIPQuizQuestionCell.xib in Resources */, 944C61082B6AB6B400C7DF06 /* AuthenticationPreviewCompactInfoCell.xib in Resources */, DFE356B51FB4990100E0E3A8 /* Setting.storyboard in Resources */, 9489247F2D572EA200D9BCCB /* TokenReceiverTrayView.xib in Resources */, @@ -7528,6 +7559,7 @@ 9414A7832BE78C2F00FA35EB /* Web3TransferInputAmountViewController.swift in Sources */, 94D3CEA22C3EFD4B00E1E542 /* AppCardV1MessageCell.swift in Sources */, DF2A245E1FCC5D15003A8C1E /* GroupAddMemberCell.swift in Sources */, + 945A72E22E59EE38006A11E9 /* TIPQuizAnswerViewController.swift in Sources */, DFB6CE1E23C4805B00FB6615 /* KeychainExtension.swift in Sources */, 7B2E3E5A1FA0816D00DDDDEB /* LoginContinueButton.swift in Sources */, 944C60FC2B6A896400C7DF06 /* AuthenticationPreviewHeaderView.swift in Sources */, @@ -7861,6 +7893,7 @@ 7CEB736229DBC894006FB5B2 /* DeviceTransferAsset.swift in Sources */, 947BE3EF2DBA95B100A98677 /* ReviewPendingWeb3TransactionJob.swift in Sources */, E08F4DDC2395345500C0D021 /* SharedAppCell.swift in Sources */, + 945A72792E59BC48006A11E9 /* TIPQuizViewController.swift in Sources */, 7B9F810E222EA3EC0012BFDD /* PopupPresentationAnimator.swift in Sources */, 94D63DD62646C29100FD7EE8 /* MessageViewModelFactory.swift in Sources */, 94BF6D5B2B67EA99002CD556 /* DepositNetworkSelectorViewController.swift in Sources */, @@ -7902,9 +7935,11 @@ 94F268032E1586EF00A8A1E0 /* AddressAssets.swift in Sources */, 7B5E9B4C2437434D000AE24E /* CircleEditingButton.swift in Sources */, 7B4CBDC22528533600BA66D0 /* ClipSwitcher.swift in Sources */, + 945A72892E59BE1F006A11E9 /* TIPQuizQuestionCell.swift in Sources */, 941995022CCE79E700F5425C /* IntroductionViewController.swift in Sources */, 942FD6762D38CF46004DCC4C /* SwapOrderTableViewController.swift in Sources */, 94CB85CF2C6B43A900135641 /* TokenMyBalanceCell.swift in Sources */, + 945A72932E59BE2E006A11E9 /* TIPQuizAnswerCell.swift in Sources */, 948FBC882D6EF5CE000795E4 /* CommonWalletViewController.swift in Sources */, 9458A0942CC9450C00A51154 /* MnemonicsViewController.swift in Sources */, 7CB17B642771602A00CF4C94 /* DeleteAccountConfirmWindow.swift in Sources */, @@ -7999,6 +8034,7 @@ 7B9F8110222EA40F0012BFDD /* BackgroundDismissablePopupPresentationController.swift in Sources */, 7B36920C233B36D4007321A7 /* MediaTypeOverlayView.swift in Sources */, 7BC2D27F21992FA9002FAC1D /* SnapshotCell.swift in Sources */, + 945A72C02E59C8BF006A11E9 /* TIPQuizAnswer.swift in Sources */, 94D9DF6125F89D6E00FC2F28 /* PopupTip.swift in Sources */, 7B3CDA6824FFF2D8003A3E80 /* AnimatedStickerView.swift in Sources */, 7C8FA8F62768926D00855AFD /* SecuritySettingViewController.swift in Sources */, diff --git a/Mixin/Assets.xcassets/radio_button.imageset/Contents.json b/Mixin/Assets.xcassets/radio_button.imageset/Contents.json new file mode 100644 index 000000000..91543bb11 --- /dev/null +++ b/Mixin/Assets.xcassets/radio_button.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "radio_button@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "radio_button@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mixin/Assets.xcassets/radio_button.imageset/radio_button@2x.png b/Mixin/Assets.xcassets/radio_button.imageset/radio_button@2x.png new file mode 100644 index 000000000..091f048af Binary files /dev/null and b/Mixin/Assets.xcassets/radio_button.imageset/radio_button@2x.png differ diff --git a/Mixin/Assets.xcassets/radio_button.imageset/radio_button@3x.png b/Mixin/Assets.xcassets/radio_button.imageset/radio_button@3x.png new file mode 100644 index 000000000..9d379d31b Binary files /dev/null and b/Mixin/Assets.xcassets/radio_button.imageset/radio_button@3x.png differ diff --git a/Mixin/Assets.xcassets/radio_button_selected.imageset/Contents.json b/Mixin/Assets.xcassets/radio_button_selected.imageset/Contents.json new file mode 100644 index 000000000..8b3eaa6e9 --- /dev/null +++ b/Mixin/Assets.xcassets/radio_button_selected.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "radio_button_selected@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "radio_button_selected@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mixin/Assets.xcassets/radio_button_selected.imageset/radio_button_selected@2x.png b/Mixin/Assets.xcassets/radio_button_selected.imageset/radio_button_selected@2x.png new file mode 100644 index 000000000..772840e54 Binary files /dev/null and b/Mixin/Assets.xcassets/radio_button_selected.imageset/radio_button_selected@2x.png differ diff --git a/Mixin/Assets.xcassets/radio_button_selected.imageset/radio_button_selected@3x.png b/Mixin/Assets.xcassets/radio_button_selected.imageset/radio_button_selected@3x.png new file mode 100644 index 000000000..bceddd8d2 Binary files /dev/null and b/Mixin/Assets.xcassets/radio_button_selected.imageset/radio_button_selected@3x.png differ diff --git a/Mixin/Assets.xcassets/tip_quiz_correct.imageset/Contents.json b/Mixin/Assets.xcassets/tip_quiz_correct.imageset/Contents.json new file mode 100644 index 000000000..972c8ec13 --- /dev/null +++ b/Mixin/Assets.xcassets/tip_quiz_correct.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "tip_quiz_correct@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "tip_quiz_correct@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mixin/Assets.xcassets/tip_quiz_correct.imageset/tip_quiz_correct@2x.png b/Mixin/Assets.xcassets/tip_quiz_correct.imageset/tip_quiz_correct@2x.png new file mode 100644 index 000000000..2837b5fcd Binary files /dev/null and b/Mixin/Assets.xcassets/tip_quiz_correct.imageset/tip_quiz_correct@2x.png differ diff --git a/Mixin/Assets.xcassets/tip_quiz_correct.imageset/tip_quiz_correct@3x.png b/Mixin/Assets.xcassets/tip_quiz_correct.imageset/tip_quiz_correct@3x.png new file mode 100644 index 000000000..05fe07db5 Binary files /dev/null and b/Mixin/Assets.xcassets/tip_quiz_correct.imageset/tip_quiz_correct@3x.png differ diff --git a/Mixin/Assets.xcassets/tip_quiz_wrong.imageset/Contents.json b/Mixin/Assets.xcassets/tip_quiz_wrong.imageset/Contents.json new file mode 100644 index 000000000..18922cf2d --- /dev/null +++ b/Mixin/Assets.xcassets/tip_quiz_wrong.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "tip_quiz_wrong@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "tip_quiz_wrong@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mixin/Assets.xcassets/tip_quiz_wrong.imageset/tip_quiz_wrong@2x.png b/Mixin/Assets.xcassets/tip_quiz_wrong.imageset/tip_quiz_wrong@2x.png new file mode 100644 index 000000000..4078a0825 Binary files /dev/null and b/Mixin/Assets.xcassets/tip_quiz_wrong.imageset/tip_quiz_wrong@2x.png differ diff --git a/Mixin/Assets.xcassets/tip_quiz_wrong.imageset/tip_quiz_wrong@3x.png b/Mixin/Assets.xcassets/tip_quiz_wrong.imageset/tip_quiz_wrong@3x.png new file mode 100644 index 000000000..9b065daea Binary files /dev/null and b/Mixin/Assets.xcassets/tip_quiz_wrong.imageset/tip_quiz_wrong@3x.png differ diff --git a/Mixin/Extension/URLExtension.swift b/Mixin/Extension/URLExtension.swift index 5b84e76cc..57186e534 100644 --- a/Mixin/Extension/URLExtension.swift +++ b/Mixin/Extension/URLExtension.swift @@ -24,6 +24,7 @@ extension URL { static let recallMessage = URL(string: R.string.localizable.url_recall_message())! static let support = URL(string: R.string.localizable.url_support())! static let watchWallet = URL(string: R.string.localizable.url_watch_wallet())! + static let whatIsPIN = URL(string: R.string.localizable.url_what_is_pin())! func getKeyVals() -> [String: String] { return URLComponents(url: self, resolvingAgainstBaseURL: true)?.getKeyVals() ?? [:] diff --git a/Mixin/Resources/en.lproj/Localizable.strings b/Mixin/Resources/en.lproj/Localizable.strings index 8063b19a7..782da5cdb 100644 --- a/Mixin/Resources/en.lproj/Localizable.strings +++ b/Mixin/Resources/en.lproj/Localizable.strings @@ -284,6 +284,7 @@ "chat_text_size" = "Chat Text Size"; "chat_waiting" = "Waiting for %1$@ to get online and establish an encrypted session. %2$@."; "chats" = "Chats"; +"check_answer" = "Check answer"; "check_backup" = "Check Backup"; "check_mnemonic_phrase" = "Check your mnemonic phrase"; "check_mnemonic_phrase_desc" = "Input each word in the order it was presented to you."; @@ -1388,6 +1389,12 @@ "tip_create_pin_title" = "Set a 6 digit PIN to create your first digital wallet"; "tip_creation_introduction" = "Set a 6 digit Wallet PIN to create your first digital wallet, the PIN is based on the Throttled Identity Protocol, which is the decentralized key derivation protocol, read the document to learn more."; "tip_introduction" = "The 6-digit PIN is based on the Throttled Identity Protocol, which is the decentralized key derivation protocol, read the document to learn more."; +"tip_quiz_answer" = "The PIN is the key to your wallet, and it’s unrecoverable if lost."; +"tip_quiz_correct_answer" = "I’ll lose access to my account and funds."; +"tip_quiz_question" = "What happens if I lose my PIN?"; +"tip_quiz_title_correct" = "Correct!"; +"tip_quiz_title_wrong" = "Oops, wrong answer"; +"tip_quiz_wrong_answer" = "I can recover my account later."; "title_participants" = "%d Participant"; "title_participants_count" = "%d Participants"; "to" = "To"; @@ -1510,6 +1517,7 @@ "url_unrecognized_hint" = "%@ failed to recognize, please upgrade Mixin to the latest version or contact the developer to check the link"; "url_unsupported_message" = "https://support.mixin.one/en/article/how-to-do-when-you-receive-a-message-like-this-this-type-of-message-is-not-supported-please-upgrade-mixin-17j1t3p/"; "url_watch_wallet" = "https://support.mixin.one/en/article/how-to-add-a-watch-only-address-17ifo8w"; +"url_what_is_pin" = "https://support.mixin.one/en/article/what-is-pin-1pjyhow/"; "use_another_address" = "Use Another Address"; "use_biometry" = "Use %@"; "use_system_text_size" = "Use System Text Size"; diff --git a/Mixin/Resources/es.lproj/Localizable.strings b/Mixin/Resources/es.lproj/Localizable.strings index 3b7bd111d..e976a14df 100644 --- a/Mixin/Resources/es.lproj/Localizable.strings +++ b/Mixin/Resources/es.lproj/Localizable.strings @@ -284,6 +284,7 @@ "chat_text_size" = "Tamaño del texto del chat"; "chat_waiting" = "Esperando a que %1$@ se conecte y establezca una sesión cifrada. %2$@."; "chats" = "Chats"; +"check_answer" = "Check answer"; "check_backup" = "Check Backup"; "check_mnemonic_phrase" = "Check your mnemonic phrase"; "check_mnemonic_phrase_desc" = "Input each word in the order it was presented to you."; @@ -1388,6 +1389,12 @@ "tip_create_pin_title" = "Establece un PIN de 6 dígitos para crear tu primera cartera digital"; "tip_creation_introduction" = "Establece un PIN de cartera de 6 dígitos para crear tu primera cartera digital, el PIN se basa en el Protocolo de identidad limitado, que es el protocolo de derivación de clave descentralizado, lee el documento para obtener más información."; "tip_introduction" = "El PIN de 6 dígitos se basa en el Throttled Identity Protocol, que es el protocolo de derivación de claves descentralizado, lee el documento para obtener más información."; +"tip_quiz_answer" = "The PIN is the key to your wallet, and it’s unrecoverable if lost."; +"tip_quiz_correct_answer" = "I’ll lose access to my account and funds."; +"tip_quiz_question" = "What happens if I lose my PIN?"; +"tip_quiz_title_correct" = "Correct!"; +"tip_quiz_title_wrong" = "Oops, wrong answer"; +"tip_quiz_wrong_answer" = "I can recover my account later."; "title_participants" = "%d Participante"; "title_participants_count" = "%d Participantes"; "to" = "A"; @@ -1510,6 +1517,7 @@ "url_unrecognized_hint" = "%@ no se ha podido reconocer, actualiza Mixin a la última versión o comunícate con el desarrollador para verificar el enlace"; "url_unsupported_message" = "https://support.mixin.one/en/article/how-to-do-when-you-receive-a-message-like-this-this-type-of-message-is-not-supported-please-upgrade-mixin-17j1t3p/"; "url_watch_wallet" = "https://support.mixin.one/en/article/how-to-add-a-watch-only-address-17ifo8w"; +"url_what_is_pin" = "https://support.mixin.one/en/article/what-is-pin-1pjyhow/"; "use_another_address" = "Use Another Address"; "use_biometry" = "Usar %@"; "use_system_text_size" = "Usar tamaño de texto del sistema"; diff --git a/Mixin/Resources/ja.lproj/Localizable.strings b/Mixin/Resources/ja.lproj/Localizable.strings index cfccf11cd..081e40d4f 100644 --- a/Mixin/Resources/ja.lproj/Localizable.strings +++ b/Mixin/Resources/ja.lproj/Localizable.strings @@ -284,6 +284,7 @@ "chat_text_size" = "Chat Text Size"; "chat_waiting" = "オンラインで暗号化されたやりとりを開始するまであと%1$@。%2$@"; "chats" = "Chats"; +"check_answer" = "Check answer"; "check_backup" = "Check Backup"; "check_mnemonic_phrase" = "Check your mnemonic phrase"; "check_mnemonic_phrase_desc" = "Input each word in the order it was presented to you."; @@ -1388,6 +1389,12 @@ "tip_create_pin_title" = "6桁のPINコードを作成してください"; "tip_creation_introduction" = "Set a 6 digit Wallet PIN to create your first digital wallet, the PIN is based on the Throttled Identity Protocol, which is the decentralized key derivation protocol, read the document to learn more."; "tip_introduction" = "The 6-digit PIN is based on the Throttled Identity Protocol, which is the decentralized key derivation protocol, read the document to learn more."; +"tip_quiz_answer" = "The PIN is the key to your wallet, and it’s unrecoverable if lost."; +"tip_quiz_correct_answer" = "I’ll lose access to my account and funds."; +"tip_quiz_question" = "What happens if I lose my PIN?"; +"tip_quiz_title_correct" = "Correct!"; +"tip_quiz_title_wrong" = "Oops, wrong answer"; +"tip_quiz_wrong_answer" = "I can recover my account later."; "title_participants" = "%d人の参加者"; "title_participants_count" = "%d人の参加者"; "to" = "宛先"; @@ -1510,6 +1517,7 @@ "url_unrecognized_hint" = "%@を認識できません、Mixinを最新版にアップデートするか、もしくは開発陣に連絡してリンクを確認してください。"; "url_unsupported_message" = "https://support.mixin.one/en/article/how-to-do-when-you-receive-a-message-like-this-this-type-of-message-is-not-supported-please-upgrade-mixin-17j1t3p/"; "url_watch_wallet" = "https://support.mixin.one/en/article/how-to-add-a-watch-only-address-17ifo8w"; +"url_what_is_pin" = "https://support.mixin.one/en/article/what-is-pin-1pjyhow/"; "use_another_address" = "Use Another Address"; "use_biometry" = "%@を使用する"; "use_system_text_size" = "Use System Text Size"; diff --git a/Mixin/Resources/ru.lproj/Localizable.strings b/Mixin/Resources/ru.lproj/Localizable.strings index a430fd5d3..c931e3541 100644 --- a/Mixin/Resources/ru.lproj/Localizable.strings +++ b/Mixin/Resources/ru.lproj/Localizable.strings @@ -284,6 +284,7 @@ "chat_text_size" = "Chat Text Size"; "chat_waiting" = "Ожидание, пока %1$@ подключится к сети и установит зашифрованный сеанс. %2$@."; "chats" = "Chats"; +"check_answer" = "Check answer"; "check_backup" = "Check Backup"; "check_mnemonic_phrase" = "Check your mnemonic phrase"; "check_mnemonic_phrase_desc" = "Input each word in the order it was presented to you."; @@ -1388,6 +1389,12 @@ "tip_create_pin_title" = "Set a 6 digit PIN to create your first digital wallet"; "tip_creation_introduction" = "Set a 6 digit Wallet PIN to create your first digital wallet, the PIN is based on the Throttled Identity Protocol, which is the decentralized key derivation protocol, read the document to learn more."; "tip_introduction" = "The 6-digit PIN is based on the Throttled Identity Protocol, which is the decentralized key derivation protocol, read the document to learn more."; +"tip_quiz_answer" = "The PIN is the key to your wallet, and it’s unrecoverable if lost."; +"tip_quiz_correct_answer" = "I’ll lose access to my account and funds."; +"tip_quiz_question" = "What happens if I lose my PIN?"; +"tip_quiz_title_correct" = "Correct!"; +"tip_quiz_title_wrong" = "Oops, wrong answer"; +"tip_quiz_wrong_answer" = "I can recover my account later."; "title_participants" = "%d участник"; "title_participants_count" = "%d участников"; "to" = "В"; @@ -1510,6 +1517,7 @@ "url_unrecognized_hint" = "%@ не удалось распознать, обновите Mixin до последней версии или свяжитесь с разработчиком, чтобы проверить ссылку"; "url_unsupported_message" = "https://support.mixin.one/en/article/how-to-do-when-you-receive-a-message-like-this-this-type-of-message-is-not-supported-please-upgrade-mixin-17j1t3p/"; "url_watch_wallet" = "https://support.mixin.one/en/article/how-to-add-a-watch-only-address-17ifo8w"; +"url_what_is_pin" = "https://support.mixin.one/en/article/what-is-pin-1pjyhow/"; "use_another_address" = "Use Another Address"; "use_biometry" = "Использовать %@"; "use_system_text_size" = "Use System Text Size"; diff --git a/Mixin/Resources/zh-Hans.lproj/Localizable.strings b/Mixin/Resources/zh-Hans.lproj/Localizable.strings index c85749fa4..e3a94f1fe 100644 --- a/Mixin/Resources/zh-Hans.lproj/Localizable.strings +++ b/Mixin/Resources/zh-Hans.lproj/Localizable.strings @@ -284,6 +284,7 @@ "chat_text_size" = "聊天字体大小"; "chat_waiting" = "等待%1$@上线后建立加密会话。%2$@。"; "chats" = "聊天"; +"check_answer" = "检查答案"; "check_backup" = "检查备份"; "check_mnemonic_phrase" = "检查您的助记词"; "check_mnemonic_phrase_desc" = "按照呈现给您的顺序输入每个单词。"; @@ -1388,6 +1389,12 @@ "tip_create_pin_title" = "设置 6 位数字 PIN 创建你的第一个数字钱包"; "tip_creation_introduction" = "设置 6 位数字 PIN 创建你的第一个加密货币钱包,PIN 基于去中心化密钥派生协议 Throttled Identity Protocol,阅读文档以了解更多。"; "tip_introduction" = "6 位数字 PIN 基于去中心化密钥派生协议 Throttled Identity Protocol,阅读文档以了解更多。"; +"tip_quiz_answer" = "PIN 是你钱包的钥匙,丢失后无法找回。"; +"tip_quiz_correct_answer" = "我将无法访问账户和资金。"; +"tip_quiz_question" = "如果我丢失了 PIN,会发生什么?"; +"tip_quiz_title_correct" = "正确!"; +"tip_quiz_title_wrong" = "哎呀,答错了"; +"tip_quiz_wrong_answer" = "我可以恢复我的账户。"; "title_participants" = "%d 成员"; "title_participants_count" = "%d 成员"; "to" = "至"; @@ -1510,6 +1517,7 @@ "url_unrecognized_hint" = "%@ 识别失败,请升级至 Mixin 最新版本或联系开发者检查链接"; "url_unsupported_message" = "https://support.mixin.one/zh/article/mixin-h92cxa/"; "url_watch_wallet" = "https://support.mixin.one/zh/article/5aac5l2v5re75yqg6kec5af5zyw5z2a77yf-r3pitk/"; +"url_what_is_pin" = "https://support.mixin.one/zh/article/pin-yhpicw/"; "use_another_address" = "换个地址"; "use_biometry" = "采用%@"; "use_system_text_size" = "跟随系统字体大小"; diff --git a/Mixin/Resources/zh-Hant.lproj/Localizable.strings b/Mixin/Resources/zh-Hant.lproj/Localizable.strings index bb7cdf336..1d3b5bd7f 100644 --- a/Mixin/Resources/zh-Hant.lproj/Localizable.strings +++ b/Mixin/Resources/zh-Hant.lproj/Localizable.strings @@ -284,6 +284,7 @@ "chat_text_size" = "聊天字型大小"; "chat_waiting" = "等待%1$@上線後建立加密會話。%2$@。"; "chats" = "聊天"; +"check_answer" = "檢查答案"; "check_backup" = "檢查備份"; "check_mnemonic_phrase" = "檢查您的助記詞"; "check_mnemonic_phrase_desc" = "按照呈現給您的順序輸入每個單詞。"; @@ -1388,6 +1389,12 @@ "tip_create_pin_title" = "設定 6 位數字 PIN 建立你的第一個數字錢包"; "tip_creation_introduction" = "設定 6 位數字 PIN 建立你的第一個加密貨幣錢包,PIN 基於去中心化金鑰派生協議 Throttled Identity Protocol,閱讀檔案以瞭解更多。"; "tip_introduction" = "6 位數字 PIN 基於去中心化金鑰派生協議 Throttled Identity Protocol,閱讀檔案以瞭解更多。"; +"tip_quiz_answer" = "PIN 是你錢包的鑰匙,丟失後無法找回。"; +"tip_quiz_correct_answer" = "我將無法訪問賬戶和資金。"; +"tip_quiz_question" = "如果我丟失了 PIN,會發生什麼?"; +"tip_quiz_title_correct" = "正確!"; +"tip_quiz_title_wrong" = "哎呀,答錯了"; +"tip_quiz_wrong_answer" = "我可以恢復我的賬戶。"; "title_participants" = "%d 成員"; "title_participants_count" = "%d 成員"; "to" = "至"; @@ -1510,6 +1517,7 @@ "url_unrecognized_hint" = "%@ 識別失敗,請升級至 Mixin 最新版本或聯絡開發者檢查連結"; "url_unsupported_message" = "https://support.mixin.one/zh/article/mixin-h92cxa/"; "url_watch_wallet" = "https://support.mixin.one/zh/article/5aac5l2v5re75yqg6kec5af5zyw5z2a77yf-r3pitk/"; +"url_what_is_pin" = "https://support.mixin.one/zh/article/pin-yhpicw/"; "use_another_address" = "換個地址"; "use_biometry" = "採用%@"; "use_system_text_size" = "跟隨系統字型大小"; diff --git a/Mixin/UserInterface/Controllers/Login/CheckSessionEnvironmentViewController.swift b/Mixin/UserInterface/Controllers/Login/CheckSessionEnvironmentViewController.swift index 5759ddde0..25467f240 100644 --- a/Mixin/UserInterface/Controllers/Login/CheckSessionEnvironmentViewController.swift +++ b/Mixin/UserInterface/Controllers/Login/CheckSessionEnvironmentViewController.swift @@ -75,7 +75,8 @@ final class CheckSessionEnvironmentViewController: UIViewController { Logger.login.info(category: "CheckSessionEnvironment", message: "Create username") isUsernameJustInitialized = true let username = UsernameViewController() - reload(content: username) + let navigationController = GeneralAppearanceNavigationController(rootViewController: username) + reload(content: navigationController) } else if AppGroupUserDefaults.Account.canRestoreFromPhone { Logger.login.info(category: "CheckSessionEnvironment", message: "Restore chat") let restore = RestoreChatViewController() diff --git a/Mixin/UserInterface/Controllers/Login/SignalLoadingViewController.swift b/Mixin/UserInterface/Controllers/Login/SignalLoadingViewController.swift index 01f04d18c..5131a0432 100644 --- a/Mixin/UserInterface/Controllers/Login/SignalLoadingViewController.swift +++ b/Mixin/UserInterface/Controllers/Login/SignalLoadingViewController.swift @@ -53,7 +53,7 @@ final class SignalLoadingViewController: LoginLoadingViewController { @objc private func presentCustomerService(_ sender: Any) { let customerService = CustomerServiceViewController(presentLoginLogsOnLongPressingTitle: true) present(customerService, animated: true) - reporter.report(event: .customerServiceDialog, tags: ["source": "sign_up"]) + reporter.report(event: .customerServiceDialog, tags: ["source": "signal_loading"]) } private func syncSignalKeys() { diff --git a/Mixin/UserInterface/Controllers/Login/UsernameViewController.swift b/Mixin/UserInterface/Controllers/Login/UsernameViewController.swift index e2e171acd..91cb3976b 100644 --- a/Mixin/UserInterface/Controllers/Login/UsernameViewController.swift +++ b/Mixin/UserInterface/Controllers/Login/UsernameViewController.swift @@ -5,6 +5,10 @@ final class UsernameViewController: LoginInfoInputViewController, CheckSessionEn override func viewDidLoad() { super.viewDidLoad() + navigationItem.rightBarButtonItem = .customerService( + target: self, + action: #selector(presentCustomerService(_:)) + ) titleLabel.text = R.string.localizable.whats_your_name() textField.text = makeDefaultUsername() editingChangedAction(self) @@ -27,6 +31,12 @@ final class UsernameViewController: LoginInfoInputViewController, CheckSessionEn } } + @objc func presentCustomerService(_ sender: Any) { + let customerService = CustomerServiceViewController(presentLoginLogsOnLongPressingTitle: true) + present(customerService, animated: true) + reporter.report(event: .customerServiceDialog, tags: ["source": "username"]) + } + private func makeDefaultUsername() -> String? { let name = UIDevice.current.name let deviceName: String diff --git a/Mixin/UserInterface/Controllers/Wallet/PIN/TIPActionViewController.swift b/Mixin/UserInterface/Controllers/Wallet/PIN/TIPActionViewController.swift index fc99ddfc7..e1cc246cb 100644 --- a/Mixin/UserInterface/Controllers/Wallet/PIN/TIPActionViewController.swift +++ b/Mixin/UserInterface/Controllers/Wallet/PIN/TIPActionViewController.swift @@ -52,6 +52,10 @@ final class TIPActionViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() + navigationItem.rightBarButtonItem = .customerService( + target: self, + action: #selector(presentCustomerService(_:)) + ) progressLabel.font = UIFontMetrics.default.scaledFont(for: .monospacedDigitSystemFont(ofSize: 14, weight: .regular)) progressLabel.adjustsFontForContentSizeCategory = true performAction() @@ -62,6 +66,12 @@ final class TIPActionViewController: UIViewController { Logger.tip.info(category: "TIPAction", message: "View did appear with action: \(action.debugDescription)") } + @objc private func presentCustomerService(_ sender: Any) { + let customerService = CustomerServiceViewController(presentLoginLogsOnLongPressingTitle: true) + present(customerService, animated: true) + reporter.report(event: .customerServiceDialog, tags: ["source": "tip_action"]) + } + private func performAction() { guard let accountCounterBefore = LoginManager.shared.account?.tipCounter else { return @@ -173,17 +183,20 @@ final class TIPActionViewController: UIViewController { @MainActor private func finish() { Logger.tip.info(category: "TIPAction", message: "Finished successfully") - let title: String switch action { case .create: - title = R.string.localizable.set_pin_successfully() + alert(R.string.localizable.set_pin_successfully()) { (_) in + let quiz = TIPQuizViewController() + self.tipNavigationController?.setViewControllers([quiz], animated: true) + } case .change: - title = R.string.localizable.change_pin_successfully() + alert(R.string.localizable.change_pin_successfully()) { (_) in + self.tipNavigationController?.finish() + } case .migrate: - title = R.string.localizable.upgrade_tip_successfully() - } - alert(title) { (_) in - self.tipNavigationController?.finish() + alert(R.string.localizable.upgrade_tip_successfully()) { (_) in + self.tipNavigationController?.finish() + } } } @@ -212,6 +225,7 @@ final class TIPActionViewController: UIViewController { } } } catch { + Logger.tip.error(category: "TIPAction", message: "Handle failed with: \(error)") await MainActor.run { let intro: TIPIntroViewController switch action { diff --git a/Mixin/UserInterface/Controllers/Wallet/PIN/TIPIntroViewController.swift b/Mixin/UserInterface/Controllers/Wallet/PIN/TIPIntroViewController.swift index 447024aeb..75b65db2d 100644 --- a/Mixin/UserInterface/Controllers/Wallet/PIN/TIPIntroViewController.swift +++ b/Mixin/UserInterface/Controllers/Wallet/PIN/TIPIntroViewController.swift @@ -179,6 +179,12 @@ final class TIPIntroViewController: UIViewController { navigationController?.presentingViewController?.dismiss(animated: true) } + @objc func presentCustomerService(_ sender: Any) { + let customerService = CustomerServiceViewController(presentLoginLogsOnLongPressingTitle: true) + present(customerService, animated: true) + reporter.report(event: .customerServiceDialog, tags: ["source": "tip_intro"]) + } + } extension TIPIntroViewController: CoreTextLabelDelegate { @@ -212,6 +218,10 @@ extension TIPIntroViewController { default: navigationItem.leftBarButtonItem = nil } + navigationItem.rightBarButtonItem = .customerService( + target: self, + action: #selector(presentCustomerService(_:)) + ) } private func setNoticeHidden(_ hidden: Bool) { diff --git a/Mixin/UserInterface/Controllers/Wallet/PIN/TIPQuizAnswer.swift b/Mixin/UserInterface/Controllers/Wallet/PIN/TIPQuizAnswer.swift new file mode 100644 index 000000000..7c90e31bb --- /dev/null +++ b/Mixin/UserInterface/Controllers/Wallet/PIN/TIPQuizAnswer.swift @@ -0,0 +1,6 @@ +import Foundation + +enum TIPQuizAnswer: Int, CaseIterable { + case wrong + case correct +} diff --git a/Mixin/UserInterface/Controllers/Wallet/PIN/TIPQuizAnswerCell.swift b/Mixin/UserInterface/Controllers/Wallet/PIN/TIPQuizAnswerCell.swift new file mode 100644 index 000000000..ba5a18301 --- /dev/null +++ b/Mixin/UserInterface/Controllers/Wallet/PIN/TIPQuizAnswerCell.swift @@ -0,0 +1,28 @@ +import UIKit + +final class TIPQuizAnswerCell: UICollectionViewCell { + + @IBOutlet weak var imageView: UIImageView! + @IBOutlet weak var label: UILabel! + + override var isSelected: Bool { + didSet { + imageView.image = if isSelected { + R.image.radio_button_selected() + } else { + R.image.radio_button() + } + } + } + + override func awakeFromNib() { + super.awakeFromNib() + contentView.layer.cornerRadius = 8 + contentView.layer.masksToBounds = true + label.setFont( + scaledFor: .systemFont(ofSize: 14), + adjustForContentSize: true + ) + } + +} diff --git a/Mixin/UserInterface/Controllers/Wallet/PIN/TIPQuizAnswerCell.xib b/Mixin/UserInterface/Controllers/Wallet/PIN/TIPQuizAnswerCell.xib new file mode 100644 index 000000000..60c3fa4ab --- /dev/null +++ b/Mixin/UserInterface/Controllers/Wallet/PIN/TIPQuizAnswerCell.xib @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Mixin/UserInterface/Controllers/Wallet/PIN/TIPQuizAnswerView.xib b/Mixin/UserInterface/Controllers/Wallet/PIN/TIPQuizAnswerView.xib new file mode 100644 index 000000000..af3c4dd4e --- /dev/null +++ b/Mixin/UserInterface/Controllers/Wallet/PIN/TIPQuizAnswerView.xib @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Mixin/UserInterface/Controllers/Wallet/PIN/TIPQuizAnswerViewController.swift b/Mixin/UserInterface/Controllers/Wallet/PIN/TIPQuizAnswerViewController.swift new file mode 100644 index 000000000..2d8dd7682 --- /dev/null +++ b/Mixin/UserInterface/Controllers/Wallet/PIN/TIPQuizAnswerViewController.swift @@ -0,0 +1,88 @@ +import UIKit + +final class TIPQuizAnswerViewController: UIViewController { + + @IBOutlet weak var imageView: UIImageView! + @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var descriptionLabel: UILabel! + @IBOutlet weak var finishButton: UIButton! + + var onTryAgain: (() -> Void)? + + private let answer: TIPQuizAnswer + private let popupManager = PopupPresentationManager() + + init(answer: TIPQuizAnswer) { + self.answer = answer + let nib = R.nib.tipQuizAnswerView + super.init(nibName: nib.name, bundle: nib.bundle) + modalPresentationStyle = .custom + transitioningDelegate = popupManager + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + view.layer.cornerRadius = 13 + view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + view.layer.masksToBounds = true + + var finishButtonAttributes = AttributeContainer() + finishButtonAttributes.font = UIFontMetrics.default.scaledFont( + for: .systemFont(ofSize: 16, weight: .medium) + ) + finishButtonAttributes.foregroundColor = .white + + switch answer { + case .wrong: + imageView.image = R.image.tip_quiz_wrong() + titleLabel.text = R.string.localizable.tip_quiz_title_wrong() + finishButton.configuration?.attributedTitle = AttributedString( + R.string.localizable.try_again(), + attributes: finishButtonAttributes + ) + case .correct: + imageView.image = R.image.tip_quiz_correct() + titleLabel.text = R.string.localizable.tip_quiz_title_correct() + finishButton.configuration?.attributedTitle = AttributedString( + R.string.localizable.got_it(), + attributes: finishButtonAttributes + ) + } + descriptionLabel.text = R.string.localizable.tip_quiz_answer() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + updatePreferredContentSizeHeight() + } + + override func viewSafeAreaInsetsDidChange() { + super.viewSafeAreaInsetsDidChange() + updatePreferredContentSizeHeight() + } + + @IBAction func finish(_ sender: Any) { + switch answer { + case .wrong: + onTryAgain?() + presentingViewController?.dismiss(animated: true) + case .correct: + if let navigationController = presentingViewController as? TIPNavigationController { + navigationController.finish() + } + } + } + + private func updatePreferredContentSizeHeight() { + view.layoutIfNeeded() + let width = view.bounds.width + let fittingSize = CGSize(width: width, height: UIView.layoutFittingExpandedSize.height) + preferredContentSize.height = view.systemLayoutSizeFitting(fittingSize).height + } + +} diff --git a/Mixin/UserInterface/Controllers/Wallet/PIN/TIPQuizQuestionCell.swift b/Mixin/UserInterface/Controllers/Wallet/PIN/TIPQuizQuestionCell.swift new file mode 100644 index 000000000..614138e9c --- /dev/null +++ b/Mixin/UserInterface/Controllers/Wallet/PIN/TIPQuizQuestionCell.swift @@ -0,0 +1,16 @@ +import UIKit + +final class TIPQuizQuestionCell: UICollectionViewCell { + + @IBOutlet weak var label: UILabel! + + override func awakeFromNib() { + super.awakeFromNib() + label.setFont( + scaledFor: .systemFont(ofSize: 18, weight: .semibold), + adjustForContentSize: true + ) + label.text = R.string.localizable.tip_quiz_question() + } + +} diff --git a/Mixin/UserInterface/Controllers/Wallet/PIN/TIPQuizQuestionCell.xib b/Mixin/UserInterface/Controllers/Wallet/PIN/TIPQuizQuestionCell.xib new file mode 100644 index 000000000..93e940c3c --- /dev/null +++ b/Mixin/UserInterface/Controllers/Wallet/PIN/TIPQuizQuestionCell.xib @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Mixin/UserInterface/Controllers/Wallet/PIN/TIPQuizView.xib b/Mixin/UserInterface/Controllers/Wallet/PIN/TIPQuizView.xib new file mode 100644 index 000000000..6e7306ba7 --- /dev/null +++ b/Mixin/UserInterface/Controllers/Wallet/PIN/TIPQuizView.xib @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Mixin/UserInterface/Controllers/Wallet/PIN/TIPQuizViewController.swift b/Mixin/UserInterface/Controllers/Wallet/PIN/TIPQuizViewController.swift new file mode 100644 index 000000000..0d508d5aa --- /dev/null +++ b/Mixin/UserInterface/Controllers/Wallet/PIN/TIPQuizViewController.swift @@ -0,0 +1,180 @@ +import UIKit +import SafariServices + +final class TIPQuizViewController: UIViewController { + + private enum Section: Int, CaseIterable { + case question + case answer + } + + @IBOutlet weak var collectionView: UICollectionView! + @IBOutlet weak var checkAnswerButton: UIButton! + @IBOutlet weak var explainPINButton: UIButton! + + private var selectedAnswer: TIPQuizAnswer? { + didSet { + checkAnswerButton.isEnabled = selectedAnswer != nil + } + } + + override func viewDidLoad() { + super.viewDidLoad() + + navigationItem.rightBarButtonItem = .customerService( + target: self, + action: #selector(presentCustomerService(_:)) + ) + + collectionView.register(R.nib.tipQuizQuestionCell) + collectionView.register(R.nib.tipQuizAnswerCell) + collectionView.collectionViewLayout = UICollectionViewCompositionalLayout { sectionIndex, _ in + switch Section(rawValue: sectionIndex)! { + case .question: + let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1)) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(326)) + let group: NSCollectionLayoutGroup = .horizontal(layoutSize: groupSize, subitems: [item]) + let section = NSCollectionLayoutSection(group: group) + section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16) + return section + case .answer: + let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1)) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(54)) + let group: NSCollectionLayoutGroup = .horizontal(layoutSize: groupSize, subitems: [item]) + let section = NSCollectionLayoutSection(group: group) + section.interGroupSpacing = 10 + section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20) + return section + } + } + collectionView.dataSource = self + collectionView.delegate = self + + checkAnswerButton.configuration = { + var config: UIButton.Configuration = .filled() + config.baseBackgroundColor = R.color.background_tinted() + config.baseForegroundColor = .white + config.contentInsets = NSDirectionalEdgeInsets(top: 12, leading: 0, bottom: 11, trailing: 0) + config.cornerStyle = .capsule + + var attributes = AttributeContainer() + attributes.font = UIFontMetrics.default.scaledFont( + for: .systemFont(ofSize: 16, weight: .medium) + ) + config.attributedTitle = AttributedString( + R.string.localizable.check_answer(), + attributes: attributes + ) + + return config + }() + checkAnswerButton.titleLabel?.adjustsFontForContentSizeCategory = true + + explainPINButton.configuration?.attributedTitle = { + var attributes = AttributeContainer() + attributes.font = UIFontMetrics.default.scaledFont( + for: .systemFont(ofSize: 16, weight: .medium) + ) + attributes.foregroundColor = R.color.theme() + return AttributedString( + R.string.localizable.what_is_pin(), + attributes: attributes + ) + }() + explainPINButton.titleLabel?.adjustsFontForContentSizeCategory = true + + selectedAnswer = nil + } + + @IBAction func checkAnswer(_ sender: Any) { + guard let selectedAnswer else { + return + } + let answer = TIPQuizAnswerViewController(answer: selectedAnswer) + answer.onTryAgain = { [weak self] in + guard let self else { + return + } + for indexPath in self.collectionView.indexPathsForSelectedItems ?? [] { + self.collectionView.deselectItem(at: indexPath, animated: false) + } + self.selectedAnswer = nil + } + present(answer, animated: true) + } + + @IBAction func explainPIN(_ sender: Any) { + let safari = SFSafariViewController(url: .whatIsPIN) + present(safari, animated: true) + } + + @objc private func presentCustomerService(_ sender: Any) { + let customerService = CustomerServiceViewController() + present(customerService, animated: true) + } + +} + +extension TIPQuizViewController: UICollectionViewDataSource { + + func numberOfSections(in collectionView: UICollectionView) -> Int { + Section.allCases.count + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + switch Section(rawValue: section)! { + case .question: + 1 + case .answer: + TIPQuizAnswer.allCases.count + } + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + switch Section(rawValue: indexPath.section)! { + case .question: + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: R.reuseIdentifier.tip_quiz_question, for: indexPath)! + return cell + case .answer: + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: R.reuseIdentifier.tip_quiz_answer, for: indexPath)! + let answer = TIPQuizAnswer(rawValue: indexPath.item)! + cell.isSelected = answer == selectedAnswer + cell.label.text = switch answer { + case .wrong: + R.string.localizable.tip_quiz_wrong_answer() + case .correct: + R.string.localizable.tip_quiz_correct_answer() + } + return cell + } + } + +} + +extension TIPQuizViewController: UICollectionViewDelegate { + + func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { + switch Section(rawValue: indexPath.section)! { + case .question: + false + case .answer: + true + } + } + + func collectionView(_ collectionView: UICollectionView, shouldDeselectItemAt indexPath: IndexPath) -> Bool { + false + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + switch Section(rawValue: indexPath.section)! { + case .question: + break + case .answer: + selectedAnswer = TIPQuizAnswer(rawValue: indexPath.item)! + } + } + +} diff --git a/MixinServices/MixinServices/Services/API/MixinAPI.swift b/MixinServices/MixinServices/Services/API/MixinAPI.swift index 91b32f1a0..0cc39825d 100644 --- a/MixinServices/MixinServices/Services/API/MixinAPI.swift +++ b/MixinServices/MixinServices/Services/API/MixinAPI.swift @@ -267,9 +267,12 @@ extension MixinAPI { return makeRequest(session) .validate(statusCode: 200...299) .responseData(queue: queue, completionHandler: { (response) in + let requestId = response.request?.value(forHTTPHeaderField: "x-request-id") ?? "" + let path = response.request?.url?.path ?? "(null)" + switch response.result { case .success(let data): - if let requestId = response.request?.value(forHTTPHeaderField: "x-request-id"), !requestId.isEmpty { + if !requestId.isEmpty { let responseRequestId = response.response?.value(forHTTPHeaderField: "x-request-id") ?? "" if requestId != responseRequestId { Logger.general.error(category: "MixinAPI", message: "Mismatched request id. Request path: \(response.request?.url?.path), id: \(requestId), responded header: \(response.response?.allHeaderFields)") @@ -284,18 +287,17 @@ extension MixinAPI { } else if case .some(.unauthorized) = responseObject.error { handleDeauthorization(response: response.response) } else if let error = responseObject.error { + Logger.general.error(category: "MixinAPI", message: "Request with path: \(path), id: \(requestId), failed with error: \(error)") completion(.failure(.response(error))) } else { completion(.success(try JSONDecoder.default.decode(Response.self, from: data))) } } catch { - Logger.general.error(category: "MixinAPI", message: "Failed to decode response: \(error)" ) + Logger.general.error(category: "MixinAPI", message: "Request with path: \(path), id: \(requestId), failed to decode response: \(error)") reporter.report(error: error) completion(.failure(.invalidJSON(error))) } case let .failure(error): - let path = response.request?.url?.path ?? "(null)" - let requestId = response.request?.value(forHTTPHeaderField: "x-request-id") ?? "(null)" Logger.general.error(category: "MixinAPI", message: "Request with path: \(path), id: \(requestId), failed with error: \(error)" ) if shouldToggleServer(for: error) { MixinHost.toggle(currentHttpHost: host)