diff --git a/Rarime.xcodeproj/project.pbxproj b/Rarime.xcodeproj/project.pbxproj index cfae50d7..fc2615bb 100644 --- a/Rarime.xcodeproj/project.pbxproj +++ b/Rarime.xcodeproj/project.pbxproj @@ -69,6 +69,7 @@ 5659151F2BE92B1F0050795A /* AppIconManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5659151E2BE92B1F0050795A /* AppIconManager.swift */; }; 565E221C2BF7B33D00171692 /* CameraPermissionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 565E221B2BF7B33D00171692 /* CameraPermissionView.swift */; }; 56722F202BCD62BB00300AD0 /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56722F1F2BCD62BB00300AD0 /* ProfileView.swift */; }; + 567ADEC42C25CF32007070A7 /* JSONDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567ADEC32C25CF32007070A7 /* JSONDocument.swift */; }; 567AEC9B2BBD9B8100A20E83 /* Passport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567AEC9A2BBD9B8100A20E83 /* Passport.swift */; }; 568350AC2BEA304B00B70E3D /* PassportIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 568350AB2BEA304B00B70E3D /* PassportIdentifier.swift */; }; 568820FA2C172C75001B729F /* ReserveTokensView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 568820F92C172C75001B729F /* ReserveTokensView.swift */; }; @@ -265,6 +266,7 @@ 5659151E2BE92B1F0050795A /* AppIconManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconManager.swift; sourceTree = ""; }; 565E221B2BF7B33D00171692 /* CameraPermissionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPermissionView.swift; sourceTree = ""; }; 56722F1F2BCD62BB00300AD0 /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = ""; }; + 567ADEC32C25CF32007070A7 /* JSONDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONDocument.swift; sourceTree = ""; }; 567AEC9A2BBD9B8100A20E83 /* Passport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Passport.swift; sourceTree = ""; }; 568350AB2BEA304B00B70E3D /* PassportIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassportIdentifier.swift; sourceTree = ""; }; 568820F92C172C75001B729F /* ReserveTokensView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReserveTokensView.swift; sourceTree = ""; }; @@ -611,6 +613,7 @@ CE8FEB602C1B03A70008381A /* Circtuits */, 567AEC9A2BBD9B8100A20E83 /* Passport.swift */, 5655E4272BD2C3FD00B86C5C /* Transaction.swift */, + 567ADEC32C25CF32007070A7 /* JSONDocument.swift */, CEF251392BD6E22700050507 /* User.swift */, CE68BE3C2BD9B23D00D92EBB /* ZkProof.swift */, CE68BE3E2BD9B4B200D92EBB /* Relayer.swift */, @@ -1215,6 +1218,7 @@ CE31E54A2C11FE150039CAA5 /* ImportIdentityView.swift in Sources */, 560F0C1F2BD027EA00067054 /* WalletReceiveView.swift in Sources */, CED314672C0483BD00DF31D5 /* MailView.swift in Sources */, + 567ADEC42C25CF32007070A7 /* JSONDocument.swift in Sources */, 5655E4302BD2CA5500B86C5C /* AppColorScheme.swift in Sources */, CED909D02BDBA9FC0065E949 /* Cosmos.swift in Sources */, 56A88FCF2C062AB200D3A64A /* AirdropCheckboxView.swift in Sources */, diff --git a/Rarime/Code/Models/JSONDocument.swift b/Rarime/Code/Models/JSONDocument.swift new file mode 100644 index 00000000..9b1cf93e --- /dev/null +++ b/Rarime/Code/Models/JSONDocument.swift @@ -0,0 +1,25 @@ +import SwiftUI +import UniformTypeIdentifiers + +class JSONDocument: FileDocument { + let json: Data + + static var readableContentTypes: [UTType] = [.json] + + init(_ json: Data) { + self.json = json + } + + required init(configuration: ReadConfiguration) throws { + if let data = configuration.file.regularFileContents { + self.json = data + return + } + + self.json = Data() + } + + func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { + return FileWrapper(regularFileWithContents: json) + } +} diff --git a/Rarime/Code/Modules/Home/Views/HomeView.swift b/Rarime/Code/Modules/Home/Views/HomeView.swift index eb229d89..cf29dcb0 100644 --- a/Rarime/Code/Modules/Home/Views/HomeView.swift +++ b/Rarime/Code/Modules/Home/Views/HomeView.swift @@ -37,8 +37,12 @@ struct HomeView: View { && userManager.registerZkProof != nil } + var isWalletBalanceDisplayed: Bool { + passportManager.passport != nil && passportManager.isUnsupportedForRewards + } + var displayedBalance: Double { - passportManager.isUnsupportedForRewards + isWalletBalanceDisplayed ? userManager.balance / Double(Rarimo.rarimoTokenMantis) : Double(pointsBalance?.amount ?? 0) } @@ -159,10 +163,10 @@ struct HomeView: View { private var header: some View { VStack(alignment: .leading, spacing: 8) { Button(action: { - mainViewModel.selectTab(passportManager.isUnsupportedForRewards ? .wallet : .rewards) + mainViewModel.selectTab(isWalletBalanceDisplayed ? .wallet : .rewards) }) { HStack(spacing: 4) { - Text(passportManager.isUnsupportedForRewards ? "Balance: RMO" : "Reserved RMO").body3() + Text(isWalletBalanceDisplayed ? "Balance: RMO" : "Reserved RMO").body3() Image(Icons.caretRight).iconSmall() } } @@ -266,7 +270,7 @@ struct HomeView: View { } do { - if passportManager.isUnsupportedForRewards { + if isWalletBalanceDisplayed { let balance = try await userManager.fetchBalanse() userManager.balance = Double(balance) ?? 0 return diff --git a/Rarime/Code/Modules/Rewards/Views/InviteFriendsView.swift b/Rarime/Code/Modules/Rewards/Views/InviteFriendsView.swift index 32ceb52b..73b4b38b 100644 --- a/Rarime/Code/Modules/Rewards/Views/InviteFriendsView.swift +++ b/Rarime/Code/Modules/Rewards/Views/InviteFriendsView.swift @@ -37,7 +37,7 @@ struct InviteFriendsView: View { VStack(alignment: .leading, spacing: 20) { VStack(alignment: .leading, spacing: 4) { if let codes = balance.referralCodes { - Text("Invited \(codes.filter { $0.status == .rewarded }.count)/\(codes.count)") + Text("Invited \(codes.filter { $0.status != .active }.count)/\(codes.count)") .subtitle3() .foregroundStyle(.textPrimary) } @@ -79,6 +79,16 @@ private struct InviteCodeView: View { ConfigManager.shared.api.referralURL.appendingPathComponent("\(code)").absoluteString } + var usedStatusText: String { + switch status { + case .awaiting: String(localized: "Scan your passport") + case .rewarded: String(localized: "Passport scanned") + case .banned: String(localized: "Unsupported country") + case .limited: String(localized: "Rewards limit reached") + default: String(localized: "Need passport scan") + } + } + var body: some View { if status == .active { HStack { @@ -89,7 +99,7 @@ private struct InviteCodeView: View { Text(invitationLink.dropFirst(8)) .body3() .foregroundStyle(.textSecondary) - Text(status.rawValue.uppercased()) + Text("Active") .body4() .foregroundStyle(.successDark) } @@ -117,7 +127,7 @@ private struct InviteCodeView: View { Circle() .fill(.componentHovered) .frame(width: 4) - Text(status == .rewarded ? "Passport scanned" : "Need passport scan") + Text(usedStatusText) .body4() } .foregroundStyle(.textSecondary) diff --git a/Rarime/Code/Modules/ScanPassport/Views/PassportProofView.swift b/Rarime/Code/Modules/ScanPassport/Views/PassportProofView.swift index 20ab0093..994980cb 100644 --- a/Rarime/Code/Modules/ScanPassport/Views/PassportProofView.swift +++ b/Rarime/Code/Modules/ScanPassport/Views/PassportProofView.swift @@ -10,15 +10,15 @@ struct PassportProofView: View { let onFinish: (ZkProof) -> Void let onClose: () -> Void let onError: () -> Void - + @State private var downloadingMessage = "" private func register() async { - do { - let zkProof = try await passportViewModel.register() { message in + do { + let zkProof = try await passportViewModel.register { message in downloadingMessage = message } - + if passportViewModel.processingStatus != .success { return } try await Task.sleep(nanoseconds: NSEC_PER_SEC) @@ -170,44 +170,43 @@ private struct RevocationNFCScan: View { @EnvironmentObject var passportViewModel: PassportViewModel var body: some View { - ZStack { - VStack(spacing: 16) { - Spacer() - Image(Icons.swap) - .square(80) - .foregroundStyle(.textPrimary) - Text("Please scan your NFC card") - .h6() - .foregroundStyle(.textPrimary) - Text("This is required to revoke your passport") - .body3() - .foregroundStyle(.textSecondary) - .multilineTextAlignment(.center) - Spacer() - AppButton(text: "Revoke with NFC") { - NFCScanner.scanPassport( - mrzViewModel.mrzKey, - passportViewModel.revocationChallenge, - onCompletion: { result in - switch result { - case .success(let passport): - passportViewModel.revocationPassportPublisher.send(passport) - passportViewModel.isUserRevoking = false - case .failure(let error): - LoggerUtil.passport.error("failed to read passport data: \(error.localizedDescription, privacy: .public)") - - passportViewModel.revocationPassportPublisher.send(completion: .failure(error)) - - passportViewModel.isUserRevoking = false - } + VStack(spacing: 16) { + Spacer() + Image(Icons.warning) + .iconLarge() + .frame(width: 72, height: 72) + .background(.warningLighter, in: Circle()) + .foregroundStyle(.warningMain) + Text("Passport is already registered") + .h6() + .foregroundStyle(.textPrimary) + Text("Please, scan your passport one more time to revoke your previous registration") + .body3() + .foregroundStyle(.textSecondary) + .multilineTextAlignment(.center) + Spacer() + AppButton(text: "Scan passport") { + NFCScanner.scanPassport( + mrzViewModel.mrzKey, + passportViewModel.revocationChallenge, + onCompletion: { result in + switch result { + case .success(let passport): + passportViewModel.revocationPassportPublisher.send(passport) + passportViewModel.isUserRevoking = false + case .failure(let error): + LoggerUtil.passport.error("failed to read passport data: \(error.localizedDescription, privacy: .public)") + + passportViewModel.revocationPassportPublisher.send(completion: .failure(error)) + + passportViewModel.isUserRevoking = false } - ) - } - .controlSize(.large) + } + ) } - .padding(20) - .background(.backgroundOpacity, in: RoundedRectangle(cornerRadius: 24)) + .controlSize(.large) } + .padding(20) } } diff --git a/Rarime/Code/Modules/ScanPassport/Views/WaitlistPassportView.swift b/Rarime/Code/Modules/ScanPassport/Views/WaitlistPassportView.swift index e5beb139..10f8b380 100644 --- a/Rarime/Code/Modules/ScanPassport/Views/WaitlistPassportView.swift +++ b/Rarime/Code/Modules/ScanPassport/Views/WaitlistPassportView.swift @@ -1,4 +1,5 @@ import Identity +import MessageUI import SwiftUI struct WaitlistPassportView: View { @@ -10,10 +11,16 @@ struct WaitlistPassportView: View { let onCancel: () -> Void @State private var isSending = false + @State private var isExporting = false + @State private var isCopied = false var country: Country { passportViewModel.passportCountry } + + var serializedPassport: Data { + return (try? passportViewModel.passport?.serialize()) ?? Data() + } var body: some View { HomeIntroLayout( @@ -61,13 +68,17 @@ struct WaitlistPassportView: View { } } .dynamicSheet(isPresented: $isSending, fullScreen: true) { - MailView( - subject: "Passport from: \(UIDevice.modelName)", - attachment: (try? passportViewModel.passport?.serialize()) ?? Data(), - fileName: "passport.json", - isShowing: $isSending, - result: .constant(nil) - ) + if MFMailComposeViewController.canSendMail() { + MailView( + subject: "Passport from: \(UIDevice.modelName)", + attachment: (try? passportViewModel.passport?.serialize()) ?? Data(), + fileName: "passport.json", + isShowing: $isSending, + result: .constant(nil) + ) + } else { + savePassportDataView + } } } @@ -122,6 +133,90 @@ struct WaitlistPassportView: View { } } } + + var savePassportDataView: some View { + VStack(alignment: .leading, spacing: 32) { + VStack(spacing: 16) { + Image(Icons.identificationCard) + .iconLarge() + .frame(width: 72, height: 72) + .background(.componentPrimary, in: Circle()) + .foregroundStyle(.textPrimary) + Text("Save your passport data") + .h6() + .foregroundStyle(.textPrimary) + Text("Your passport data will be saved on your device. You can share it with us to expedite the support of your passport.") + .body3() + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: 320) + .foregroundStyle(.textSecondary) + HorizontalDivider() + VStack(alignment: .leading, spacing: 16) { + Text("HOW TO SHARE") + .overline2() + .foregroundStyle(.textSecondary) + Text("1. Save passport data on your device") + .body3() + .foregroundStyle(.textPrimary) + Text("2. Send the saved file to the email address below") + .body3() + .foregroundStyle(.textPrimary) + HStack(spacing: 8) { + Text(ConfigManager.shared.feedback.feedbackEmail) + .body2() + .foregroundStyle(.textPrimary) + Image(isCopied ? Icons.check : Icons.copySimple).iconMedium() + } + .onTapGesture { + if isCopied { return } + UIPasteboard.general.string = ConfigManager.shared.feedback.feedbackEmail + isCopied = true + FeedbackGenerator.shared.impact(.medium) + + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + isCopied = false + } + } + .frame(maxWidth: .infinity) + .padding(.horizontal, 16) + .padding(.vertical, 14) + .background(.componentPrimary) + .foregroundStyle(.textPrimary) + .cornerRadius(8) + Text("3. When we support your country, you will be notified in the app") + .body3() + .foregroundStyle(.textPrimary) + } + .frame(maxWidth: .infinity) + } + .frame(maxWidth: .infinity) + Spacer() + AppButton( + text: "Save to files", + rightIcon: Icons.arrowRight, + action: { isExporting = true } + ) + .controlSize(.large) + .fileExporter( + isPresented: $isExporting, + document: JSONDocument(serializedPassport), + contentType: .json, + defaultFilename: "passport.json" + ) { result in + switch result { + case .success: + LoggerUtil.common.info("Passport data saved") + onNext() + case .failure(let error): + LoggerUtil.common.error("Failed to save passport data: \(error, privacy: .public)") + } + } + } + .padding(.top, 80) + .padding(.bottom, 20) + .padding(.horizontal, 24) + } } #Preview { diff --git a/Rarime/Code/Modules/Security/Views/LockScreenView.swift b/Rarime/Code/Modules/Security/Views/LockScreenView.swift index 7d002179..88b220ca 100644 --- a/Rarime/Code/Modules/Security/Views/LockScreenView.swift +++ b/Rarime/Code/Modules/Security/Views/LockScreenView.swift @@ -12,7 +12,7 @@ struct LockScreenView: View { @State private var failedAttempts = 0 - @State private var banTimeEnd = AppUserDefaults.shared.banTimeEnd + @State private var banTimeEnd = AppUserDefaults.shared.banTimeEnd var body: some View { ZStack { @@ -67,7 +67,7 @@ struct LockScreenView: View { } func authByFaceID() { - if securityManager.faceIdState != .enabled { + if securityManager.faceIdState != .enabled || banTimeEnd != nil { return } diff --git a/Rarime/Code/Modules/Wallet/Views/WalletView.swift b/Rarime/Code/Modules/Wallet/Views/WalletView.swift index 75503d7a..fc07a881 100644 --- a/Rarime/Code/Modules/Wallet/Views/WalletView.swift +++ b/Rarime/Code/Modules/Wallet/Views/WalletView.swift @@ -60,6 +60,8 @@ struct WalletView: View { VStack(alignment: .leading, spacing: 20) { header AssetsSlider(walletAssets: [selectedAsset], isLoading: isBalanceFetching) + HorizontalDivider() + .padding(.horizontal, 20) transactionsList } .padding(.bottom, 120) @@ -122,25 +124,22 @@ struct WalletView: View { } private var transactionsList: some View { - VStack { - CardContainer { - VStack(alignment: .leading, spacing: 20) { - Text("Transactions") - .subtitle3() - .foregroundStyle(.textPrimary) - ForEach(walletManager.transactions) { tx in - TransactionItem(tx: tx, token: token) - } - if walletManager.transactions.isEmpty { - Text("No transactions yet") - .body3() - .foregroundStyle(.textSecondary) - } + CardContainer { + VStack(alignment: .leading, spacing: 20) { + Text("Transactions") + .subtitle3() + .foregroundStyle(.textPrimary) + ForEach(walletManager.transactions) { tx in + TransactionItem(tx: tx, token: token) + } + if walletManager.transactions.isEmpty { + Text("No transactions yet") + .body3() + .foregroundStyle(.textSecondary) } } - Spacer() } - .background(.backgroundOpacity) + .padding(.horizontal, 12) } func fetchBalance() { diff --git a/Rarime/Resources/Localizable.xcstrings b/Rarime/Resources/Localizable.xcstrings index d30f3dda..a819a820 100644 --- a/Rarime/Resources/Localizable.xcstrings +++ b/Rarime/Resources/Localizable.xcstrings @@ -119,6 +119,15 @@ } } } + }, + "1. Save passport data on your device" : { + + }, + "2. Send the saved file to the email address below" : { + + }, + "3. When we support your country, you will be notified in the app" : { + }, "About the reward program" : { @@ -135,6 +144,9 @@ }, "Account Locked" : { + }, + "Active" : { + }, "Active tasks" : { @@ -1183,6 +1195,9 @@ }, "HOW CAN I GET THE INVITE CODE?" : { + }, + "HOW TO SHARE" : { + }, "Hungary" : { @@ -1760,6 +1775,9 @@ } } } + }, + "Passport is already registered" : { + }, "Passport read successfully" : { "localizations" : { @@ -1815,9 +1833,6 @@ } } } - }, - "Please scan your NFC card" : { - }, "Please store the private key safely and do not share it with anyone. If you lose this key, you will not be able to recover the account and will lose access forever." : { "localizations" : { @@ -1848,6 +1863,9 @@ } } } + }, + "Please, scan your passport one more time to revoke your previous registration" : { + }, "Poland" : { @@ -2126,10 +2144,10 @@ "Review Transaction" : { }, - "Revoke with NFC" : { + "Rewards" : { }, - "Rewards" : { + "Rewards limit reached" : { }, "RMO tokens will be exclusively distributed via the RariMe app. To claim tokens, create an incognito profile using your biometric passport. Depending on which country issued the passport, you’ll either be able to claim a token right away or be put on a waitlist.\n\nIf you are added to the waitlist it means that you are eligible to claim tokens in the next wave of airdrops. The app will notify you when you are added.\n\nCertain jurisdictions are excluded from the reward program: %@" : { @@ -2176,6 +2194,12 @@ }, "Saudi Arabia" : { + }, + "Save to files" : { + + }, + "Save your passport data" : { + }, "Scan" : { "localizations" : { @@ -2186,6 +2210,9 @@ } } } + }, + "Scan passport" : { + }, "Scan QR" : { "localizations" : { @@ -2196,6 +2223,9 @@ } } } + }, + "Scan your passport" : { + }, "Scan your Passport" : { "localizations" : { @@ -2515,9 +2545,6 @@ } } } - }, - "This is required to revoke your passport" : { - }, "Timor-Leste" : { @@ -2820,6 +2847,9 @@ }, "Your Address" : { + }, + "Your passport data will be saved on your device. You can share it with us to expedite the support of your passport." : { + }, "Your passport proof is ready" : { "localizations" : {