diff --git a/.locale-state.metadata b/.locale-state.metadata index 4bb1fb801d..0bfd249df9 100644 --- a/.locale-state.metadata +++ b/.locale-state.metadata @@ -1,4 +1,4 @@ { "project": "apple-mail-new", - "locale": "0d39f2e91da281e1a30dfc158cc6c4257d0a7d5a" + "locale": "c3c613cfc201210c3d95bdda446c74a329ffc290" } \ No newline at end of file diff --git a/Modules/App/Sources/DataSources/SenderImageDataSource.swift b/Modules/App/Sources/DataSources/SenderImageDataSource.swift index 50eaa2ae0d..82c089a206 100644 --- a/Modules/App/Sources/DataSources/SenderImageDataSource.swift +++ b/Modules/App/Sources/DataSources/SenderImageDataSource.swift @@ -61,7 +61,7 @@ final class SenderImageAPIDataSource: Sendable, SenderImageDataSource { address: params.address, bimiSelector: params.bimiSelector, displaySenderImage: params.displaySenderImage, - size: 128, + size: .s128, mode: colorScheme == .dark ? "dark" : "light", format: "png" ) diff --git a/Modules/App/Sources/Model/SystemFolderLabel.swift b/Modules/App/Sources/Model/SystemFolderLabel.swift index 2389bfb864..1be7968340 100644 --- a/Modules/App/Sources/Model/SystemFolderLabel.swift +++ b/Modules/App/Sources/Model/SystemFolderLabel.swift @@ -46,7 +46,7 @@ extension SystemLabel { L10n.Mailbox.SystemFolder.scheduled case .snoozed: L10n.Mailbox.SystemFolder.snoozed - case .categorySocial, .categoryPromotions, .catergoryUpdates, .categoryForums, .categoryDefault, .blocked, .pinned: + case .categorySocial, .categoryPromotions, .categoryUpdates, .categoryForums, .categoryDefault, .blocked, .pinned, .categoryNewsletter, .categoryTransactions: fatalError("Not implemented") } } @@ -75,7 +75,7 @@ extension SystemLabel { DS.Icon.icClockPaperPlane.image case .snoozed: DS.Icon.icClock.image - case .categorySocial, .categoryPromotions, .catergoryUpdates, .categoryForums, .categoryDefault, .blocked, .pinned: + case .categorySocial, .categoryPromotions, .categoryUpdates, .categoryForums, .categoryDefault, .blocked, .pinned, .categoryNewsletter, .categoryTransactions: fatalError("Not implemented") } } diff --git a/Modules/App/Sources/Payments/UpsellConfiguration.swift b/Modules/App/Sources/Payments/UpsellConfiguration.swift index 75058b4c68..d64e34ec0a 100644 --- a/Modules/App/Sources/Payments/UpsellConfiguration.swift +++ b/Modules/App/Sources/Payments/UpsellConfiguration.swift @@ -32,8 +32,4 @@ extension UpsellConfiguration { false #endif } - - var humanReadableUpsoldPlanName: String { - "Mail Plus" - } } diff --git a/Modules/App/Sources/Resources/Localizable.xcstrings b/Modules/App/Sources/Resources/Localizable.xcstrings index 37354debe1..d76a205281 100644 --- a/Modules/App/Sources/Resources/Localizable.xcstrings +++ b/Modules/App/Sources/Resources/Localizable.xcstrings @@ -57066,7 +57066,7 @@ "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Aktivno vas ščitimo pred sledenjem. Tukaj si lahko ogledate vohunske slikovne pike, ki smo jih blokirali, in povezave za sledenje, ki smo jih očistili v tem sporočilu. [Več o tem](https://proton.me/support/email-tracker-protection)" + "value" : "Aktivno vas ščitimo pred sledenjem. Tukaj si lahko ogledate vohunske slikovne pike, ki smo jih blokirali, in sledilne povezave, ki smo jih očistili v tem sporočilu. [Več o tem](https://proton.me/support/email-tracker-protection)" } }, "tr" : { diff --git a/Modules/App/Sources/State/Mailbox/MailboxModel.swift b/Modules/App/Sources/State/Mailbox/MailboxModel.swift index d9dc05ea1d..8d4e99e9b9 100644 --- a/Modules/App/Sources/State/Mailbox/MailboxModel.swift +++ b/Modules/App/Sources/State/Mailbox/MailboxModel.swift @@ -397,6 +397,8 @@ extension MailboxModel { showScrollerErrorIfNotNetwork(error: error) let isLastPage = await !conversationScrollerHasMore() paginatedDataSource.handle(update: .init(isLastPage: isLastPage, value: .error(error), completion: nil)) + case .categoryViewChanged: + break } } @@ -443,6 +445,8 @@ extension MailboxModel { showScrollerErrorIfNotNetwork(error: error) let isLastPage = await !messageScrollerHasMore() paginatedDataSource.handle(update: .init(isLastPage: isLastPage, value: .error(error), completion: nil)) + case .categoryViewChanged: + break } } diff --git a/Modules/App/Sources/UI/Actions/Draft/DraftPresenter.swift b/Modules/App/Sources/UI/Actions/Draft/DraftPresenter.swift index 4571b1a511..a151b4c38f 100644 --- a/Modules/App/Sources/UI/Actions/Draft/DraftPresenter.swift +++ b/Modules/App/Sources/UI/Actions/Draft/DraftPresenter.swift @@ -160,7 +160,7 @@ extension DraftPresenter { undoScheduleSendProvider: UndoScheduleSendProvider = .mockInstance ) -> DraftPresenter { .init( - userSession: .init(noHandle: .init()), + userSession: .init(noPointer: .init()), draftProvider: .dummy, undoSendProvider: undoSendProvider, undoScheduleSendProvider: undoScheduleSendProvider diff --git a/Modules/App/Sources/UI/Actions/Draft/DraftProvider.swift b/Modules/App/Sources/UI/Actions/Draft/DraftProvider.swift index ae655b4f88..8956f64af5 100644 --- a/Modules/App/Sources/UI/Actions/Draft/DraftProvider.swift +++ b/Modules/App/Sources/UI/Actions/Draft/DraftProvider.swift @@ -28,6 +28,6 @@ extension DraftProvider { } static var dummy: Self { - .init(makeDraft: { _, _ in NewDraftResult.ok(.init(noHandle: .init())) }) + .init(makeDraft: { _, _ in NewDraftResult.ok(.init(noPointer: .init())) }) } } diff --git a/Modules/App/Sources/UI/Actions/LabelAs/LabelAsActions.swift b/Modules/App/Sources/UI/Actions/LabelAs/LabelAsActions.swift index 019477addf..70340c9069 100644 --- a/Modules/App/Sources/UI/Actions/LabelAs/LabelAsActions.swift +++ b/Modules/App/Sources/UI/Actions/LabelAs/LabelAsActions.swift @@ -48,6 +48,6 @@ extension LabelAsActions { extension Undo { static var dummy: Undo { - Undo(noHandle: .init()) + Undo(noPointer: .init()) } } diff --git a/Modules/App/Sources/UI/Actions/Views/LabelAs/LabelAsSheetPreviewProvider.swift b/Modules/App/Sources/UI/Actions/Views/LabelAs/LabelAsSheetPreviewProvider.swift index ce06c9cdfc..677be42393 100644 --- a/Modules/App/Sources/UI/Actions/Views/LabelAs/LabelAsSheetPreviewProvider.swift +++ b/Modules/App/Sources/UI/Actions/Views/LabelAs/LabelAsSheetPreviewProvider.swift @@ -22,7 +22,7 @@ enum LabelAsSheetPreviewProvider { static func testData() -> LabelAsSheetModel { .init( input: .init(sheetType: .labelAs, ids: [], mailboxItem: .message(isLastMessageInCurrentLocation: false)), - mailbox: .init(noHandle: .init()), + mailbox: .init(noPointer: .init()), availableLabelAsActions: .init( message: { _, _ in .ok(testLabels()) }, conversation: { _, _ in .ok([]) } diff --git a/Modules/App/Sources/UI/Extensions/MailUserSession+Dummy.swift b/Modules/App/Sources/UI/Extensions/MailUserSession+Dummy.swift index c1ca0649d8..b8a35c9107 100644 --- a/Modules/App/Sources/UI/Extensions/MailUserSession+Dummy.swift +++ b/Modules/App/Sources/UI/Extensions/MailUserSession+Dummy.swift @@ -19,6 +19,6 @@ import proton_app_uniffi extension MailUserSession { static var dummy: MailUserSession { - .init(noHandle: .init()) + .init(noPointer: .init()) } } diff --git a/Modules/App/Sources/UI/Extensions/Mailbox+Dummy.swift b/Modules/App/Sources/UI/Extensions/Mailbox+Dummy.swift index 489ce54770..c856459d53 100644 --- a/Modules/App/Sources/UI/Extensions/Mailbox+Dummy.swift +++ b/Modules/App/Sources/UI/Extensions/Mailbox+Dummy.swift @@ -19,6 +19,6 @@ import proton_app_uniffi extension Mailbox { static var dummy: Mailbox { - .init(noHandle: .init()) + .init(noPointer: .init()) } } diff --git a/Modules/App/Sources/UI/Screens/AppSettings/AppProtection/AppProtectionSelectionScreen.swift b/Modules/App/Sources/UI/Screens/AppSettings/AppProtection/AppProtectionSelectionScreen.swift index 5783f2be9a..e466b7066d 100644 --- a/Modules/App/Sources/UI/Screens/AppSettings/AppProtection/AppProtectionSelectionScreen.swift +++ b/Modules/App/Sources/UI/Screens/AppSettings/AppProtection/AppProtectionSelectionScreen.swift @@ -126,8 +126,8 @@ struct AppProtectionSelectionScreen: View { .init(type: .faceID, isSelected: true), ] ), - appSettingsRepository: MailSession(noHandle: .init()), - appProtectionConfigurator: MailSession(noHandle: .init()) + appSettingsRepository: MailSession(noPointer: .init()), + appProtectionConfigurator: MailSession(noPointer: .init()) ) } } diff --git a/Modules/App/Sources/UI/Screens/AppSettings/AppSettingsScreen.swift b/Modules/App/Sources/UI/Screens/AppSettings/AppSettingsScreen.swift index 15ea547266..42cae6bb6b 100644 --- a/Modules/App/Sources/UI/Screens/AppSettings/AppSettingsScreen.swift +++ b/Modules/App/Sources/UI/Screens/AppSettings/AppSettingsScreen.swift @@ -199,7 +199,7 @@ struct AppSettingsScreen: View { NavigationStack { AppSettingsScreen( state: .initial(isDiscreetAppIconEnabled: false), - customSettings: CustomSettings(noHandle: .init()) + customSettings: CustomSettings(noPointer: .init()) ) } } diff --git a/Modules/App/Sources/UI/Screens/Mailbox/MailboxListView/MailboxListView.swift b/Modules/App/Sources/UI/Screens/Mailbox/MailboxListView/MailboxListView.swift index 2a6c91a526..2939e144ea 100644 --- a/Modules/App/Sources/UI/Screens/Mailbox/MailboxListView/MailboxListView.swift +++ b/Modules/App/Sources/UI/Screens/Mailbox/MailboxListView/MailboxListView.swift @@ -185,8 +185,8 @@ private extension SystemLabel { func emptyScreenVariant(isUnreadFilterOn: Bool) -> NoResultsView.Variant { switch self { case .inbox, .allDrafts, .allSent, .sent, .trash, .spam, .allMail, .archive, .drafts, .starred, .scheduled, - .almostAllMail, .snoozed, .categorySocial, .categoryPromotions, .catergoryUpdates, .categoryForums, - .categoryDefault, .blocked, .pinned: + .almostAllMail, .snoozed, .categorySocial, .categoryPromotions, .categoryUpdates, .categoryForums, + .categoryDefault, .blocked, .pinned, .categoryNewsletter, .categoryTransactions: .mailbox(isUnreadFilterOn: isUnreadFilterOn) case .outbox: .outbox diff --git a/Modules/App/Sources/UI/Screens/ReportProblem/ReportProblemScreen.swift b/Modules/App/Sources/UI/Screens/ReportProblem/ReportProblemScreen.swift index c13744c819..d6a29723bc 100644 --- a/Modules/App/Sources/UI/Screens/ReportProblem/ReportProblemScreen.swift +++ b/Modules/App/Sources/UI/Screens/ReportProblem/ReportProblemScreen.swift @@ -197,5 +197,5 @@ struct ReportProblemScreen: View { } #Preview { - ReportProblemScreen(reportProblemService: MailUserSession(noHandle: .init())) + ReportProblemScreen(reportProblemService: MailUserSession(noPointer: .init())) } diff --git a/Modules/App/Sources/UI/Screens/Search/SearchModel.swift b/Modules/App/Sources/UI/Screens/Search/SearchModel.swift index 7a257d5080..6ddff480bf 100644 --- a/Modules/App/Sources/UI/Screens/Search/SearchModel.swift +++ b/Modules/App/Sources/UI/Screens/Search/SearchModel.swift @@ -231,6 +231,8 @@ final class SearchModel: ObservableObject { AppLogger.log(error: error, category: .mailbox) let isLastPage = await !searchScrollerHasMore() paginatedDataSource.handle(update: .init(isLastPage: isLastPage, value: .error(error), completion: nil)) + case .categoryViewChanged: + break } } diff --git a/Modules/App/Sources/UI/Screens/Settings/SettingsScreen.swift b/Modules/App/Sources/UI/Screens/Settings/SettingsScreen.swift index fe28b0457a..d14a0223c1 100644 --- a/Modules/App/Sources/UI/Screens/Settings/SettingsScreen.swift +++ b/Modules/App/Sources/UI/Screens/Settings/SettingsScreen.swift @@ -324,7 +324,7 @@ private extension SettingsPreference { #Preview { NavigationStack { SettingsScreen( - mailUserSession: MailUserSession(noHandle: .init()), + mailUserSession: MailUserSession(noPointer: .init()), accountAuthCoordinator: .mock(), upsellCoordinator: .init( mailUserSession: .dummy, diff --git a/Modules/App/Sources/UI/Screens/Sidebar/SidebarScreen.swift b/Modules/App/Sources/UI/Screens/Sidebar/SidebarScreen.swift index 6e7ec7b7b2..5ad1566f52 100644 --- a/Modules/App/Sources/UI/Screens/Sidebar/SidebarScreen.swift +++ b/Modules/App/Sources/UI/Screens/Sidebar/SidebarScreen.swift @@ -232,8 +232,6 @@ struct SidebarScreen: View { @ViewBuilder private func upsellSidebarItem(item: SidebarItem, upsellType: UpsellType) -> some View { - let planName = UpsellConfiguration.mail.humanReadableUpsoldPlanName - SidebarItemButton( item: item, isTappable: isButtonTappable, @@ -241,7 +239,7 @@ struct SidebarScreen: View { content: { HStack(spacing: .zero) { sidebarItemImage(icon: upsellType.icon.image, isSelected: false, renderingMode: .original) - itemNameLabel(name: upsellType.title(planName: planName), isSelected: false, foregroundColor: upsellType.tint) + itemNameLabel(name: upsellType.title, isSelected: false) Spacer() } } @@ -370,11 +368,11 @@ struct SidebarScreen: View { .accessibilityIdentifier(SidebarScreenIdentifiers.badgeIcon) } - private func itemNameLabel(name: String, isSelected: Bool, foregroundColor: Color? = nil) -> some View { + private func itemNameLabel(name: String, isSelected: Bool) -> some View { Text(name) .font(.subheadline) .fontWeight(isSelected ? .bold : .regular) - .foregroundStyle(foregroundColor ?? (isSelected ? DS.Color.Sidebar.textSelected : DS.Color.Sidebar.textNorm)) + .foregroundStyle(isSelected ? DS.Color.Sidebar.textSelected : DS.Color.Sidebar.textNorm) .lineLimit(1) .accessibilityIdentifier(SidebarScreenIdentifiers.textItem) } @@ -455,24 +453,25 @@ private extension SidebarOtherItem { } private extension UpsellType { + var planName: String { + switch self { + case .mailPlus: + "Mail Plus" + case .unlimited: + "Unlimited" + } + } + var icon: ImageResource { switch self { - case .mailPlus, .unlimited: + case .mailPlus: DS.Icon.icDiamond + case .unlimited: + DS.Icon.icInfinity } } - func title(planName: String) -> String { - switch self { - case .mailPlus, .unlimited: - L10n.Sidebar.upgrade(to: planName).string - } - } - - var tint: Color? { - switch self { - case .mailPlus, .unlimited: - nil - } + var title: String { + L10n.Sidebar.upgrade(to: planName).string } } diff --git a/Modules/App/Sources/UI/Views/ViewModifiers/MainToolbar.swift b/Modules/App/Sources/UI/Views/ViewModifiers/MainToolbar.swift index 798e7e8f7e..3fab60d83c 100644 --- a/Modules/App/Sources/UI/Views/ViewModifiers/MainToolbar.swift +++ b/Modules/App/Sources/UI/Views/ViewModifiers/MainToolbar.swift @@ -90,6 +90,8 @@ struct MainToolbar: ViewModifier { if case .eligible(let upsellType) = upsellEligibility { ToolbarItem(placement: .topBarTrailing) { upsellButton(for: upsellType) + .frame(width: 26, height: 26) + .clipShape(.circle) } } ToolbarItem(placement: .topBarTrailing) { @@ -111,10 +113,10 @@ struct MainToolbar: ViewModifier { if !selectionMode.hasItems { ToolbarItemGroup(placement: .topBarTrailing) { HStack(spacing: DS.Spacing.standard) { + searchButton if case .eligible(let upsellType) = upsellEligibility { upsellButton(for: upsellType) } - searchButton avatarView() } } @@ -141,15 +143,35 @@ struct MainToolbar: ViewModifier { } } + @ViewBuilder private func upsellButton(for upsellType: UpsellType) -> some View { - Button(L10n.MainToolbar.upgrade, image: upsellType.icon) { - Task { - do { - let upsellScreenModel = try await upsellCoordinator.presentUpsellScreen(entryPoint: .mailboxTopBar, upsellType: upsellType) - onEvent(.onUpsell(upsellScreenModel)) - } catch { - toastStateStore.present(toast: .error(message: error.localizedDescription)) - } + switch upsellType { + case .mailPlus: + Button(L10n.MainToolbar.upgrade, image: upsellType.icon) { + performUpsellAction(upsellType: upsellType) + } + case .unlimited: + Button { + performUpsellAction(upsellType: upsellType) + } label: { + Image(upsellType.icon) + .renderingMode(.original) + .resizable() + .scaledToFit() + .frame(width: 32, height: 32) + .clipShape(RoundedRectangle(cornerRadius: Theme.radius.large)) + } + .accessibilityLabel(L10n.MainToolbar.upgrade) + } + } + + private func performUpsellAction(upsellType: UpsellType) { + Task { + do { + let upsellScreenModel = try await upsellCoordinator.presentUpsellScreen(entryPoint: .mailboxTopBar, upsellType: upsellType) + onEvent(.onUpsell(upsellScreenModel)) + } catch { + toastStateStore.present(toast: .error(message: error.localizedDescription)) } } } @@ -203,8 +225,10 @@ enum MainToolbarEvent { private extension UpsellType { var icon: ImageResource { switch self { - case .mailPlus, .unlimited: + case .mailPlus: DS.Icon.icBrandProtonMailUpsellBlackAndWhite + case .unlimited: + DS.Icon.icBrandProtonUnlimitedUpsellHeader } } } diff --git a/Modules/App/Tests/Doubles/MailboxStub.swift b/Modules/App/Tests/Doubles/MailboxStub.swift index 2c29b464f0..547423c80c 100644 --- a/Modules/App/Tests/Doubles/MailboxStub.swift +++ b/Modules/App/Tests/Doubles/MailboxStub.swift @@ -26,11 +26,11 @@ final class MailboxStub: Mailbox, @unchecked Sendable { init(viewMode: ViewMode) { self._viewMode = viewMode - super.init(noHandle: .init()) + super.init(noPointer: .init()) } @available(*, unavailable) - required init(unsafeFromHandle handle: UInt64) { - fatalError("init(unsafeFromHandle:) has not been implemented") + required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + fatalError("init(unsafeFromRawPointer:) has not been implemented") } } diff --git a/Modules/App/Tests/Doubles/SidebarSpy.swift b/Modules/App/Tests/Doubles/SidebarSpy.swift index 6e33e55153..3f34ce5450 100644 --- a/Modules/App/Tests/Doubles/SidebarSpy.swift +++ b/Modules/App/Tests/Doubles/SidebarSpy.swift @@ -67,6 +67,6 @@ final class SidebarSpy: SidebarProtocol, @unchecked Sendable { let mappedIndex: CallbackIndex = lastIndex == -1 ? .folder : .init(rawValue: lastIndex + 1)! spiedWatchers[mappedIndex] = callback - return .ok(WatchHandleDummy(noHandle: .init())) + return .ok(WatchHandleDummy(noPointer: .init())) } } diff --git a/Modules/App/Tests/TestData/MailUserSession+Dummy.swift b/Modules/App/Tests/TestData/MailUserSession+Dummy.swift index 74398d8453..685e3069d4 100644 --- a/Modules/App/Tests/TestData/MailUserSession+Dummy.swift +++ b/Modules/App/Tests/TestData/MailUserSession+Dummy.swift @@ -19,6 +19,6 @@ import proton_app_uniffi extension MailUserSession { static var dummy: MailUserSession { - .init(noHandle: .init()) + .init(noPointer: .init()) } } diff --git a/Modules/App/Tests/TestData/Mailbox+Dummy.swift b/Modules/App/Tests/TestData/Mailbox+Dummy.swift index 81cbf2e8bd..07bee2540f 100644 --- a/Modules/App/Tests/TestData/Mailbox+Dummy.swift +++ b/Modules/App/Tests/TestData/Mailbox+Dummy.swift @@ -19,6 +19,6 @@ import proton_app_uniffi extension Mailbox { static var dummy: Mailbox { - .init(noHandle: .init()) + .init(noPointer: .init()) } } diff --git a/Modules/App/Tests/Tests/Snapshots/Screens/Mailbox/MailboxListView/MailboxItemCellSnapshotTests.swift b/Modules/App/Tests/Tests/Snapshots/Screens/Mailbox/MailboxListView/MailboxItemCellSnapshotTests.swift index 0102590a16..9db749bcd5 100644 --- a/Modules/App/Tests/Tests/Snapshots/Screens/Mailbox/MailboxListView/MailboxItemCellSnapshotTests.swift +++ b/Modules/App/Tests/Tests/Snapshots/Screens/Mailbox/MailboxListView/MailboxItemCellSnapshotTests.swift @@ -40,7 +40,7 @@ final class MailboxItemCellSnapshotTests { ("expiration_time_message", .makeSimpleMessage(type: .expirationTime)), ]) func mailboxItemCell(testName: String, model: MailboxItemCellUIModel) { - assertSnapshotsOnIPhoneX(of: MailboxItemCell.testCell(model: model), testName: testName) + assertSnapshotsOnIPhoneX(of: MailboxItemCell.testCell(model: model), precision: 0.99, testName: testName) } } diff --git a/Modules/App/Tests/Tests/Snapshots/Screens/Settings/SettingsScreenSnapshotTests.swift b/Modules/App/Tests/Tests/Snapshots/Screens/Settings/SettingsScreenSnapshotTests.swift index cb227e71be..a3ac2d458c 100644 --- a/Modules/App/Tests/Tests/Snapshots/Screens/Settings/SettingsScreenSnapshotTests.swift +++ b/Modules/App/Tests/Tests/Snapshots/Screens/Settings/SettingsScreenSnapshotTests.swift @@ -27,7 +27,7 @@ import XCTest @MainActor class SettingsScreenSnapshotTests: BaseTestCase { func testSettingsScreenLayoutsCorrectOnIphoneX() { - let store = AppAppearanceStore(mailSession: { MailSession(noHandle: .init()) }) + let store = AppAppearanceStore(mailSession: { MailSession(noPointer: .init()) }) let mailUserSession = MailUserSessionSpy(id: "") mailUserSession.stubbedAccountDetails = .testData mailUserSession.stubbedUser = .testData diff --git a/Modules/App/Tests/Tests/Snapshots/Screens/Sidebar/SidebarScreenSnapshotTests.swift b/Modules/App/Tests/Tests/Snapshots/Screens/Sidebar/SidebarScreenSnapshotTests.swift index 2a590606cc..3d144c5a1b 100644 --- a/Modules/App/Tests/Tests/Snapshots/Screens/Sidebar/SidebarScreenSnapshotTests.swift +++ b/Modules/App/Tests/Tests/Snapshots/Screens/Sidebar/SidebarScreenSnapshotTests.swift @@ -37,9 +37,10 @@ final class SidebarScreenSnapshotTests { createFolder: .createFolder ) - @Test(arguments: [UIUserInterfaceStyle.light, .dark]) - func testSidebarWithDataLayoutsCorrectOnIphoneX(style: UIUserInterfaceStyle) { + @Test(arguments: [UIUserInterfaceStyle.light, .dark], [UpsellType.mailPlus, .unlimited]) + func testSidebarWithDataLayoutsCorrectOnIphoneX(style: UIUserInterfaceStyle, upsellType: UpsellType) { var state = self.state + state.upsell = .upsell(upsellType) state.folders = [SidebarCustomFolder.topSecretFolder].map(\.sidebarFolder) state.system = [PMSystemLabel.inbox, .sent, .outbox].compactMap(\.sidebarSystemFolder) @@ -48,7 +49,7 @@ final class SidebarScreenSnapshotTests { let screenModel = SidebarModel( state: state, sidebar: SidebarSpy(), - upsellEligibilityPublisher: .init(constant: .eligible(.mailPlus)) + upsellEligibilityPublisher: .init(constant: .eligible(upsellType)) ) let sidebarScreen = SidebarScreen( screenModel: screenModel, @@ -57,7 +58,7 @@ final class SidebarScreenSnapshotTests { ) .environmentObject(AppUIStateStore(sidebarState: .init(zIndex: .zero, visibleWidth: 320))) - assertSnapshotsOnIPhoneX(of: sidebarScreen, styles: [style]) + assertSnapshotsOnIPhoneX(of: sidebarScreen, named: "\(upsellType)", styles: [style]) } @Test(arguments: [UIUserInterfaceStyle.light, .dark]) diff --git a/Modules/App/Tests/Tests/Snapshots/Screens/Sidebar/__Snapshots__/SidebarScreenSnapshotTests/testSidebarWithDataLayoutsCorrectOnIphoneX-style.dark.png b/Modules/App/Tests/Tests/Snapshots/Screens/Sidebar/__Snapshots__/SidebarScreenSnapshotTests/testSidebarWithDataLayoutsCorrectOnIphoneX-style-upsellType.mailPlus_dark.png similarity index 100% rename from Modules/App/Tests/Tests/Snapshots/Screens/Sidebar/__Snapshots__/SidebarScreenSnapshotTests/testSidebarWithDataLayoutsCorrectOnIphoneX-style.dark.png rename to Modules/App/Tests/Tests/Snapshots/Screens/Sidebar/__Snapshots__/SidebarScreenSnapshotTests/testSidebarWithDataLayoutsCorrectOnIphoneX-style-upsellType.mailPlus_dark.png diff --git a/Modules/App/Tests/Tests/Snapshots/Screens/Sidebar/__Snapshots__/SidebarScreenSnapshotTests/testSidebarWithDataLayoutsCorrectOnIphoneX-style.light.png b/Modules/App/Tests/Tests/Snapshots/Screens/Sidebar/__Snapshots__/SidebarScreenSnapshotTests/testSidebarWithDataLayoutsCorrectOnIphoneX-style-upsellType.mailPlus_light.png similarity index 100% rename from Modules/App/Tests/Tests/Snapshots/Screens/Sidebar/__Snapshots__/SidebarScreenSnapshotTests/testSidebarWithDataLayoutsCorrectOnIphoneX-style.light.png rename to Modules/App/Tests/Tests/Snapshots/Screens/Sidebar/__Snapshots__/SidebarScreenSnapshotTests/testSidebarWithDataLayoutsCorrectOnIphoneX-style-upsellType.mailPlus_light.png diff --git a/Modules/App/Tests/Tests/Snapshots/Screens/Sidebar/__Snapshots__/SidebarScreenSnapshotTests/testSidebarWithDataLayoutsCorrectOnIphoneX-style-upsellType.unlimited_dark.png b/Modules/App/Tests/Tests/Snapshots/Screens/Sidebar/__Snapshots__/SidebarScreenSnapshotTests/testSidebarWithDataLayoutsCorrectOnIphoneX-style-upsellType.unlimited_dark.png new file mode 100644 index 0000000000..c0568401c1 Binary files /dev/null and b/Modules/App/Tests/Tests/Snapshots/Screens/Sidebar/__Snapshots__/SidebarScreenSnapshotTests/testSidebarWithDataLayoutsCorrectOnIphoneX-style-upsellType.unlimited_dark.png differ diff --git a/Modules/App/Tests/Tests/Snapshots/Screens/Sidebar/__Snapshots__/SidebarScreenSnapshotTests/testSidebarWithDataLayoutsCorrectOnIphoneX-style-upsellType.unlimited_light.png b/Modules/App/Tests/Tests/Snapshots/Screens/Sidebar/__Snapshots__/SidebarScreenSnapshotTests/testSidebarWithDataLayoutsCorrectOnIphoneX-style-upsellType.unlimited_light.png new file mode 100644 index 0000000000..6108fdc34e Binary files /dev/null and b/Modules/App/Tests/Tests/Snapshots/Screens/Sidebar/__Snapshots__/SidebarScreenSnapshotTests/testSidebarWithDataLayoutsCorrectOnIphoneX-style-upsellType.unlimited_light.png differ diff --git a/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/UpsellScreenSnapshotTests.swift b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/UpsellScreenSnapshotTests.swift index ccee242ffe..54d5fa6840 100644 --- a/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/UpsellScreenSnapshotTests.swift +++ b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/UpsellScreenSnapshotTests.swift @@ -25,12 +25,24 @@ import proton_app_uniffi @testable import InboxIAP @MainActor -@Suite(.disabled("Tests are failing on CI only, needs more investigation.")) struct UpsellScreenSnapshotTests { + enum SelectedCycle: String { + case yearly + case monthly + + var lengthInMonths: Int { + switch self { + case .yearly: 12 + case .monthly: 1 + } + } + } + struct TestCase { let label: String let config: ViewImageConfig let upsellType: UpsellType + let selectedCycle: SelectedCycle } nonisolated private static let testCases: [TestCase] = { @@ -42,15 +54,19 @@ struct UpsellScreenSnapshotTests { ] let upsellTypes: [UpsellType] = [.mailPlus, .unlimited] + let selectedCycles: [SelectedCycle] = [.yearly, .monthly] return orientations.flatMap { orientation in devices.flatMap { device in - upsellTypes.map { upsellType in - .init( - label: "\(device.label)_\(orientation)_\(upsellType.label)", - config: device.configFactory(orientation), - upsellType: upsellType - ) + upsellTypes.flatMap { upsellType in + selectedCycles.map { selectedCycle in + .init( + label: "\(device.label)_\(orientation)_\(upsellType.label)_\(selectedCycle.rawValue)", + config: device.configFactory(orientation), + upsellType: upsellType, + selectedCycle: selectedCycle + ) + } } } } @@ -58,12 +74,20 @@ struct UpsellScreenSnapshotTests { @Test(arguments: testCases) func upsellScreen(testCase: TestCase) { - let sut = UpsellScreen(model: .preview(entryPoint: .mailboxTopBar, upsellType: testCase.upsellType)) + let model = UpsellScreenModel.preview(entryPoint: .mailboxTopBar, upsellType: testCase.upsellType) + if let instance = model.planInstances.first(where: { $0.cycleInMonths == testCase.selectedCycle.lengthInMonths }) { + model.selectedInstanceId = instance.storeKitProductId + } + + let sut = UpsellScreen(model: model) let viewController = UIHostingController(rootView: sut) + viewController.view.backgroundColor = .black + viewController.overrideUserInterfaceStyle = .dark let strategy: Snapshotting = .image( on: testCase.config, - drawHierarchyInKeyWindow: true + drawHierarchyInKeyWindow: true, + precision: 0.99 ) assertSnapshot(of: viewController, as: strategy, named: testCase.label) diff --git a/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_landscape_mailPlus.png b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_landscape_mailPlus.png deleted file mode 100644 index 8129e8820c..0000000000 Binary files a/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_landscape_mailPlus.png and /dev/null differ diff --git a/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_landscape_mailPlus_monthly.png b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_landscape_mailPlus_monthly.png new file mode 100644 index 0000000000..5dcbb3ba9f Binary files /dev/null and b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_landscape_mailPlus_monthly.png differ diff --git a/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_landscape_mailPlus_yearly.png b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_landscape_mailPlus_yearly.png new file mode 100644 index 0000000000..e58f42c3f3 Binary files /dev/null and b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_landscape_mailPlus_yearly.png differ diff --git a/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_landscape_unlimited.png b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_landscape_unlimited.png deleted file mode 100644 index 8129e8820c..0000000000 Binary files a/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_landscape_unlimited.png and /dev/null differ diff --git a/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_landscape_unlimited_monthly.png b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_landscape_unlimited_monthly.png new file mode 100644 index 0000000000..d0b31830af Binary files /dev/null and b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_landscape_unlimited_monthly.png differ diff --git a/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_landscape_unlimited_yearly.png b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_landscape_unlimited_yearly.png new file mode 100644 index 0000000000..ef216995db Binary files /dev/null and b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_landscape_unlimited_yearly.png differ diff --git a/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_portrait_unlimited.png b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_portrait_mailPlus_monthly.png similarity index 53% rename from Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_portrait_unlimited.png rename to Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_portrait_mailPlus_monthly.png index 1f64506239..24bdc7d24c 100644 Binary files a/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_portrait_unlimited.png and b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_portrait_mailPlus_monthly.png differ diff --git a/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_portrait_mailPlus.png b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_portrait_mailPlus_yearly.png similarity index 54% rename from Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_portrait_mailPlus.png rename to Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_portrait_mailPlus_yearly.png index 1f64506239..034ae51a85 100644 Binary files a/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_portrait_mailPlus.png and b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_portrait_mailPlus_yearly.png differ diff --git a/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_portrait_unlimited_monthly.png b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_portrait_unlimited_monthly.png new file mode 100644 index 0000000000..d9d2580d7e Binary files /dev/null and b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_portrait_unlimited_monthly.png differ diff --git a/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_portrait_unlimited_yearly.png b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_portrait_unlimited_yearly.png new file mode 100644 index 0000000000..c8b4f7a53b Binary files /dev/null and b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.13-Pro-Max_portrait_unlimited_yearly.png differ diff --git a/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_landscape_mailPlus.png b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_landscape_mailPlus.png deleted file mode 100644 index e9c5fe80db..0000000000 Binary files a/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_landscape_mailPlus.png and /dev/null differ diff --git a/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_landscape_mailPlus_monthly.png b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_landscape_mailPlus_monthly.png new file mode 100644 index 0000000000..95ed673901 Binary files /dev/null and b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_landscape_mailPlus_monthly.png differ diff --git a/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_landscape_mailPlus_yearly.png b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_landscape_mailPlus_yearly.png new file mode 100644 index 0000000000..6c2628aa37 Binary files /dev/null and b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_landscape_mailPlus_yearly.png differ diff --git a/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_landscape_unlimited.png b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_landscape_unlimited.png deleted file mode 100644 index e9c5fe80db..0000000000 Binary files a/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_landscape_unlimited.png and /dev/null differ diff --git a/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_landscape_unlimited_monthly.png b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_landscape_unlimited_monthly.png new file mode 100644 index 0000000000..37a459ef67 Binary files /dev/null and b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_landscape_unlimited_monthly.png differ diff --git a/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_landscape_unlimited_yearly.png b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_landscape_unlimited_yearly.png new file mode 100644 index 0000000000..d0c770ff77 Binary files /dev/null and b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_landscape_unlimited_yearly.png differ diff --git a/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_portrait_mailPlus.png b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_portrait_mailPlus.png deleted file mode 100644 index 439f9849b6..0000000000 Binary files a/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_portrait_mailPlus.png and /dev/null differ diff --git a/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_portrait_mailPlus_monthly.png b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_portrait_mailPlus_monthly.png new file mode 100644 index 0000000000..15f6d48ce9 Binary files /dev/null and b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_portrait_mailPlus_monthly.png differ diff --git a/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_portrait_mailPlus_yearly.png b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_portrait_mailPlus_yearly.png new file mode 100644 index 0000000000..2902186a7a Binary files /dev/null and b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_portrait_mailPlus_yearly.png differ diff --git a/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_portrait_unlimited.png b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_portrait_unlimited.png deleted file mode 100644 index 439f9849b6..0000000000 Binary files a/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_portrait_unlimited.png and /dev/null differ diff --git a/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_portrait_unlimited_monthly.png b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_portrait_unlimited_monthly.png new file mode 100644 index 0000000000..343b7e22fb Binary files /dev/null and b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_portrait_unlimited_monthly.png differ diff --git a/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_portrait_unlimited_yearly.png b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_portrait_unlimited_yearly.png new file mode 100644 index 0000000000..25c69d091c Binary files /dev/null and b/Modules/App/Tests/Tests/Snapshots/Screens/Upsell/__Snapshots__/UpsellScreenSnapshotTests/upsellScreen-testCase.8_portrait_unlimited_yearly.png differ diff --git a/Modules/App/Tests/Tests/Snapshots/UI/MainToolbar/MainToolbarSnapshotTests.swift b/Modules/App/Tests/Tests/Snapshots/UI/MainToolbar/MainToolbarSnapshotTests.swift new file mode 100644 index 0000000000..1236a934ab --- /dev/null +++ b/Modules/App/Tests/Tests/Snapshots/UI/MainToolbar/MainToolbarSnapshotTests.swift @@ -0,0 +1,69 @@ +// Copyright (c) 2025 Proton Technologies AG +// +// This file is part of Proton Mail. +// +// Proton Mail is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Proton Mail is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Proton Mail. If not, see https://www.gnu.org/licenses/. + +import InboxDesignSystem +import InboxIAP +import InboxSnapshotTesting +import InboxTesting +import ProtonUIFoundations +import SwiftUI +import Testing +import UIKit +import proton_app_uniffi + +@testable import ProtonMail + +@MainActor +final class MainToolbarSnapshotTests { + @Test(arguments: [UpsellType.mailPlus, .unlimited]) + func mainToolbarWithUpsell(upsellType: UpsellType) { + let view = makeToolbarView(upsellEligibility: .eligible(upsellType)) + assertSnapshotsOnIPhoneX( + of: view, + named: "upsellType.\(upsellType)", + drawHierarchyInKeyWindow: true, + precision: 0.99 + ) + } + + @Test + func mainToolbarWithoutUpsell() { + let view = makeToolbarView(upsellEligibility: .notEligible) + assertSnapshotsOnIPhoneX(of: view, drawHierarchyInKeyWindow: true) + } + + private func makeToolbarView(upsellEligibility: UpsellEligibility) -> some View { + NavigationStack { + Color.clear + .mainToolbar( + title: "Inbox", + onEvent: { _ in }, + avatarView: { + Text("R") + .font(.subheadline.weight(.semibold)) + .frame(width: 32, height: 32) + .foregroundStyle(.white) + .background(.blue) + .clipShape(RoundedRectangle(cornerRadius: Theme.radius.large)) + } + ) + } + .environment(\.upsellEligibility, upsellEligibility) + .environmentObject(ToastStateStore(initialState: .initial)) + .environmentObject(UpsellCoordinator.dummy) + } +} diff --git a/Modules/App/Tests/Tests/Snapshots/UI/MainToolbar/__Snapshots__/MainToolbarSnapshotTests/mainToolbarWithUpsell-upsellType.upsellType-mailPlus_dark.png b/Modules/App/Tests/Tests/Snapshots/UI/MainToolbar/__Snapshots__/MainToolbarSnapshotTests/mainToolbarWithUpsell-upsellType.upsellType-mailPlus_dark.png new file mode 100644 index 0000000000..3d9df4b55a Binary files /dev/null and b/Modules/App/Tests/Tests/Snapshots/UI/MainToolbar/__Snapshots__/MainToolbarSnapshotTests/mainToolbarWithUpsell-upsellType.upsellType-mailPlus_dark.png differ diff --git a/Modules/App/Tests/Tests/Snapshots/UI/MainToolbar/__Snapshots__/MainToolbarSnapshotTests/mainToolbarWithUpsell-upsellType.upsellType-mailPlus_light.png b/Modules/App/Tests/Tests/Snapshots/UI/MainToolbar/__Snapshots__/MainToolbarSnapshotTests/mainToolbarWithUpsell-upsellType.upsellType-mailPlus_light.png new file mode 100644 index 0000000000..0976210ae3 Binary files /dev/null and b/Modules/App/Tests/Tests/Snapshots/UI/MainToolbar/__Snapshots__/MainToolbarSnapshotTests/mainToolbarWithUpsell-upsellType.upsellType-mailPlus_light.png differ diff --git a/Modules/App/Tests/Tests/Snapshots/UI/MainToolbar/__Snapshots__/MainToolbarSnapshotTests/mainToolbarWithUpsell-upsellType.upsellType-unlimited_dark.png b/Modules/App/Tests/Tests/Snapshots/UI/MainToolbar/__Snapshots__/MainToolbarSnapshotTests/mainToolbarWithUpsell-upsellType.upsellType-unlimited_dark.png new file mode 100644 index 0000000000..f4d29da7bc Binary files /dev/null and b/Modules/App/Tests/Tests/Snapshots/UI/MainToolbar/__Snapshots__/MainToolbarSnapshotTests/mainToolbarWithUpsell-upsellType.upsellType-unlimited_dark.png differ diff --git a/Modules/App/Tests/Tests/Snapshots/UI/MainToolbar/__Snapshots__/MainToolbarSnapshotTests/mainToolbarWithUpsell-upsellType.upsellType-unlimited_light.png b/Modules/App/Tests/Tests/Snapshots/UI/MainToolbar/__Snapshots__/MainToolbarSnapshotTests/mainToolbarWithUpsell-upsellType.upsellType-unlimited_light.png new file mode 100644 index 0000000000..12ad9c2c51 Binary files /dev/null and b/Modules/App/Tests/Tests/Snapshots/UI/MainToolbar/__Snapshots__/MainToolbarSnapshotTests/mainToolbarWithUpsell-upsellType.upsellType-unlimited_light.png differ diff --git a/Modules/App/Tests/Tests/Snapshots/UI/MainToolbar/__Snapshots__/MainToolbarSnapshotTests/mainToolbarWithoutUpsell.dark.png b/Modules/App/Tests/Tests/Snapshots/UI/MainToolbar/__Snapshots__/MainToolbarSnapshotTests/mainToolbarWithoutUpsell.dark.png new file mode 100644 index 0000000000..8c6e0b53e2 Binary files /dev/null and b/Modules/App/Tests/Tests/Snapshots/UI/MainToolbar/__Snapshots__/MainToolbarSnapshotTests/mainToolbarWithoutUpsell.dark.png differ diff --git a/Modules/App/Tests/Tests/Snapshots/UI/MainToolbar/__Snapshots__/MainToolbarSnapshotTests/mainToolbarWithoutUpsell.light.png b/Modules/App/Tests/Tests/Snapshots/UI/MainToolbar/__Snapshots__/MainToolbarSnapshotTests/mainToolbarWithoutUpsell.light.png new file mode 100644 index 0000000000..c47db4b452 Binary files /dev/null and b/Modules/App/Tests/Tests/Snapshots/UI/MainToolbar/__Snapshots__/MainToolbarSnapshotTests/mainToolbarWithoutUpsell.light.png differ diff --git a/Modules/App/Tests/Tests/Unit/App/SceneDelegateTests.swift b/Modules/App/Tests/Tests/Unit/App/SceneDelegateTests.swift index c9614da0b4..5fe910f8e3 100644 --- a/Modules/App/Tests/Tests/Unit/App/SceneDelegateTests.swift +++ b/Modules/App/Tests/Tests/Unit/App/SceneDelegateTests.swift @@ -35,7 +35,7 @@ final class SceneDelegateTests: BaseTestCase { sut = .init() mailSessionSpy = .init() sut.appProtectionStore = .init(mailSession: { self.mailSessionSpy }) - sut.mailSessionFactory = { .init(noHandle: .init()) } + sut.mailSessionFactory = { .init(noPointer: .init()) } sut.checkAutoLockSetting = { completion in completion(self.shouldAutoLockStub) } sut.transitionAnimation = { _, _, _, animation, completion in animation?() diff --git a/Modules/App/Tests/Tests/Unit/State/DeviceTokenRegistrarTests.swift b/Modules/App/Tests/Tests/Unit/State/DeviceTokenRegistrarTests.swift index 788e2e2e07..de5c634c8f 100644 --- a/Modules/App/Tests/Tests/Unit/State/DeviceTokenRegistrarTests.swift +++ b/Modules/App/Tests/Tests/Unit/State/DeviceTokenRegistrarTests.swift @@ -33,7 +33,7 @@ final class DeviceTokenRegistrarTests { var lastCreatedHandle: RegisterDeviceTaskHandleSpy? mailSession.stubbedRegisterDeviceTaskHandleFactory = { - let newHandle = RegisterDeviceTaskHandleSpy(noHandle: .init()) + let newHandle = RegisterDeviceTaskHandleSpy(noPointer: .init()) lastCreatedHandle = newHandle return newHandle } diff --git a/Modules/App/Tests/Tests/Unit/UI/Actions/DraftPresenter/DraftPresenterTests.swift b/Modules/App/Tests/Tests/Unit/UI/Actions/DraftPresenter/DraftPresenterTests.swift index b946c6c0bf..3c4d981e98 100644 --- a/Modules/App/Tests/Tests/Unit/UI/Actions/DraftPresenter/DraftPresenterTests.swift +++ b/Modules/App/Tests/Tests/Unit/UI/Actions/DraftPresenter/DraftPresenterTests.swift @@ -86,7 +86,7 @@ final class DraftPresenterTests: BaseTestCase, @unchecked Sendable { @MainActor func testOpenDraftWithContact_ItCreatesEmptyDraftAddRecipientAndOpensDraft() async throws { - let draftSpy = DraftSpy(noHandle: .init()) + let draftSpy = DraftSpy(noPointer: .init()) sut = makeSUT(stubbedNewDraftResult: .ok(draftSpy)) var capturedDraftToPresent: [DraftToPresent] = [] @@ -123,7 +123,7 @@ final class DraftPresenterTests: BaseTestCase, @unchecked Sendable { @MainActor func testOpenDraftWithContactGroup_ItCreatesEmptyDraftAddGroupAndOpensDraft() async throws { - let draftSpy = DraftSpy(noHandle: .init()) + let draftSpy = DraftSpy(noPointer: .init()) sut = makeSUT(stubbedNewDraftResult: .ok(draftSpy)) var capturedDraftToPresent: [DraftToPresent] = [] @@ -292,13 +292,13 @@ extension DraftPresenterTests { } private extension Draft { - static var dummyDraft: Draft { .init(noHandle: .init()) } + static var dummyDraft: Draft { .init(noPointer: .init()) } } private class DraftSpy: Draft, @unchecked Sendable { - let toRecipientsCalls: ComposerRecipientListSpy = .init(noHandle: .init()) - let ccRecipientsCalls: ComposerRecipientListSpy = .init(noHandle: .init()) - let bccRecipientsCalls: ComposerRecipientListSpy = .init(noHandle: .init()) + let toRecipientsCalls: ComposerRecipientListSpy = .init(noPointer: .init()) + let ccRecipientsCalls: ComposerRecipientListSpy = .init(noPointer: .init()) + let bccRecipientsCalls: ComposerRecipientListSpy = .init(noPointer: .init()) private(set) var setSubjectCalls: [String] = [] private(set) var setBodyCalls: [String] = [] diff --git a/Modules/App/Tests/Tests/Unit/UI/Actions/LabelAsSheetModelTests.swift b/Modules/App/Tests/Tests/Unit/UI/Actions/LabelAsSheetModelTests.swift index 5921d97c66..d2236ed6fb 100644 --- a/Modules/App/Tests/Tests/Unit/UI/Actions/LabelAsSheetModelTests.swift +++ b/Modules/App/Tests/Tests/Unit/UI/Actions/LabelAsSheetModelTests.swift @@ -31,7 +31,7 @@ final class LabelAsSheetModelTests { private var invokedAvailableActionsWithConversationIDs: [ID] = [] private var invokedDismissCount = 0 private var stubbedLabelAsActions: [LabelAsAction] = [] - private var stubbedLabelAsOutput: LabelAsOutput = .init(inputLabelIsEmpty: false, undo: UndoSpy(noHandle: .init())) + private var stubbedLabelAsOutput: LabelAsOutput = .init(inputLabelIsEmpty: false, undo: UndoSpy(noPointer: .init())) private var toastStateStore = ToastStateStore(initialState: .initial) private var invokedLabelMessage: [LabelAsExecutedWithData] = [] @@ -175,7 +175,7 @@ final class LabelAsSheetModelTests { private func sut(ids: [ID], type: MailboxItemType) -> LabelAsSheetModel { LabelAsSheetModel( input: .init(sheetType: .labelAs, ids: ids, mailboxItem: type.mailboxItem), - mailbox: .init(noHandle: .init()), + mailbox: .init(noPointer: .init()), availableLabelAsActions: .init( message: { _, ids in self.invokedAvailableActionsWithMessagesIDs = ids @@ -220,7 +220,7 @@ final class LabelAsSheetModelTests { itemType: MailboxItemType, spyToVerify: () -> [LabelAsExecutedWithData] ) async throws { - let undoSpy = UndoSpy(noHandle: .init()) + let undoSpy = UndoSpy(noPointer: .init()) let selectedLabelID: ID = .init(value: 2) let partiallySelectedLabelID: ID = .init(value: 4) stubbedLabelAsActions = [ @@ -257,7 +257,7 @@ final class LabelAsSheetModelTests { spyToVerify: () -> [LabelAsExecutedWithData], expectToastMessage: LocalizedStringResource ) async throws { - let undoSpy = UndoSpy(noHandle: .init()) + let undoSpy = UndoSpy(noPointer: .init()) let selectedLabelID: ID = .init(value: 2) let partiallySelectedLabelID: ID = .init(value: 4) stubbedLabelAsActions = [ diff --git a/Modules/App/Tests/Tests/Unit/UI/Actions/ListActionsToolbarStoreTests.swift b/Modules/App/Tests/Tests/Unit/UI/Actions/ListActionsToolbarStoreTests.swift index 49ab040543..b44b9efc43 100644 --- a/Modules/App/Tests/Tests/Unit/UI/Actions/ListActionsToolbarStoreTests.swift +++ b/Modules/App/Tests/Tests/Unit/UI/Actions/ListActionsToolbarStoreTests.swift @@ -259,7 +259,7 @@ class ListActionsToolbarStoreTests { func action_WhenMoveToInboxIsTappedUndoIsAvailbleAndTapped_ItTriggersUndoAndDismissesToast() async throws { let ids: [ID] = [.init(value: 7), .init(value: 77)] let systemFolder = MovableSystemFolderAction.testInbox - let undoSpy = UndoSpy(noHandle: .init()) + let undoSpy = UndoSpy(noPointer: .init()) let viewMode = ViewMode.messages moveToActionsSpy.stubbedMoveMessagesToOkResult = undoSpy sut = makeSUT(viewMode: viewMode) diff --git a/Modules/App/Tests/Tests/Unit/UI/Actions/MoveTo/MoveToActionPerformerTests.swift b/Modules/App/Tests/Tests/Unit/UI/Actions/MoveTo/MoveToActionPerformerTests.swift index 686238d5f2..f356cd9ae4 100644 --- a/Modules/App/Tests/Tests/Unit/UI/Actions/MoveTo/MoveToActionPerformerTests.swift +++ b/Modules/App/Tests/Tests/Unit/UI/Actions/MoveTo/MoveToActionPerformerTests.swift @@ -29,7 +29,7 @@ final class MoveToActionPerformerTests: BaseTestCase { super.setUp() sut = .init( - mailbox: .init(noHandle: .init()), + mailbox: .init(noPointer: .init()), moveToActions: .init( moveMessagesTo: { [unowned self] _, _, _ in stubbedResult }, moveConversationsTo: { [unowned self] _, _, _ in stubbedResult } diff --git a/Modules/App/Tests/Tests/Unit/UI/Actions/MoveTo/MoveToSheetStateStoreTests.swift b/Modules/App/Tests/Tests/Unit/UI/Actions/MoveTo/MoveToSheetStateStoreTests.swift index 23b436d09b..939e5e421c 100644 --- a/Modules/App/Tests/Tests/Unit/UI/Actions/MoveTo/MoveToSheetStateStoreTests.swift +++ b/Modules/App/Tests/Tests/Unit/UI/Actions/MoveTo/MoveToSheetStateStoreTests.swift @@ -108,7 +108,7 @@ final class MoveToSheetStateStoreTests { @Test func action_WhenInboxIsTappedUndoIsAvailableAndTapped_ItTriggersUndoAndDismissesToast() async throws { - let undoSpy = UndoSpy(noHandle: .init()) + let undoSpy = UndoSpy(noPointer: .init()) moveToActionsSpy.stubbedMoveMessagesToOkResult = undoSpy let sut = sut( @@ -155,7 +155,7 @@ final class MoveToSheetStateStoreTests { .init( state: .initial, input: input, - mailbox: .init(noHandle: .init()), + mailbox: .init(noPointer: .init()), availableMoveToActions: .init( message: { _, ids in self.invokedAvailableActionsWithMessagesIDs = ids diff --git a/Modules/App/Tests/Tests/Unit/UI/Screens/ConversationDetails/Body/MessageBodyStateStoreTests.swift b/Modules/App/Tests/Tests/Unit/UI/Screens/ConversationDetails/Body/MessageBodyStateStoreTests.swift index 8542dc456a..0be62a3b05 100644 --- a/Modules/App/Tests/Tests/Unit/UI/Screens/ConversationDetails/Body/MessageBodyStateStoreTests.swift +++ b/Modules/App/Tests/Tests/Unit/UI/Screens/ConversationDetails/Body/MessageBodyStateStoreTests.swift @@ -566,12 +566,12 @@ private final class DecryptedMessageSpy: DecryptedMessage, @unchecked Sendable { init(stubbedOptions: TransformOpts) { self.stubbedOptions = stubbedOptions - super.init(noHandle: .init()) + super.init(noPointer: .init()) } @available(*, unavailable) - required init(unsafeFromHandle handle: UInt64) { - fatalError("init(unsafeFromHandle:) has not been implemented") + required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + fatalError("init(unsafeFromRawPointer:) has not been implemented") } private(set) var bodyWithOptionsCalls: [TransformOpts] = [] diff --git a/Modules/App/Tests/Tests/Unit/UI/Views/MessageAddressActionView/MessageAddressActionViewStateStoreTests.swift b/Modules/App/Tests/Tests/Unit/UI/Views/MessageAddressActionView/MessageAddressActionViewStateStoreTests.swift index 5290139194..98fa94d23a 100644 --- a/Modules/App/Tests/Tests/Unit/UI/Views/MessageAddressActionView/MessageAddressActionViewStateStoreTests.swift +++ b/Modules/App/Tests/Tests/Unit/UI/Views/MessageAddressActionView/MessageAddressActionViewStateStoreTests.swift @@ -333,7 +333,7 @@ final class MessageAddressActionViewStateStoreTests { senderUnblocker: .init( mailbox: .dummy, wrapper: .init( - messageBody: { _, _ in .ok(.init(noHandle: .init())) }, + messageBody: { _, _ in .ok(.init(noPointer: .init())) }, markMessageHam: { _, _ in .ok }, unblockSender: { _, emailAddress in await self.unblockSpy.result(for: emailAddress) diff --git a/Modules/App/Tests/Tests/Unit/Utils/BackgroundExecution/RecurringBackgroundTaskSchedulerTests.swift b/Modules/App/Tests/Tests/Unit/Utils/BackgroundExecution/RecurringBackgroundTaskSchedulerTests.swift index 723f5c5365..6fdae95906 100644 --- a/Modules/App/Tests/Tests/Unit/Utils/BackgroundExecution/RecurringBackgroundTaskSchedulerTests.swift +++ b/Modules/App/Tests/Tests/Unit/Utils/BackgroundExecution/RecurringBackgroundTaskSchedulerTests.swift @@ -235,12 +235,12 @@ class BackgroundExecutionHandleStub: BackgroundExecutionHandle, @unchecked Senda private(set) var abortCalls: [Bool] = [] init() { - super.init(noHandle: .init()) + super.init(noPointer: .init()) } @available(*, unavailable) - required init(unsafeFromHandle handle: UInt64) { - fatalError("init(unsafeFromHandle:) has not been implemented") + required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + fatalError("init(unsafeFromRawPointer:) has not been implemented") } // MARK: - BackgroundExecutionHandle diff --git a/Modules/InboxComposer/Sources/Composer/ComposerScreen.swift b/Modules/InboxComposer/Sources/Composer/ComposerScreen.swift index 6819a0242a..dc697d1d86 100644 --- a/Modules/InboxComposer/Sources/Composer/ComposerScreen.swift +++ b/Modules/InboxComposer/Sources/Composer/ComposerScreen.swift @@ -127,7 +127,7 @@ struct ComposerLoadingView: View { ComposerScreen( draft: .emptyMock, draftOrigin: .new, - dependencies: .init(contactProvider: .mockInstance, userSession: .init(noHandle: .init())), + dependencies: .init(contactProvider: .mockInstance, userSession: .init(noPointer: .init())), onDismiss: { _ in } ) .environmentObject(toastStateStore) diff --git a/Modules/InboxComposer/Sources/Composer/UIKitComponents/Models/MockDraft.swift b/Modules/InboxComposer/Sources/Composer/UIKitComponents/Models/MockDraft.swift index 8606f2b50d..19f933d517 100644 --- a/Modules/InboxComposer/Sources/Composer/UIKitComponents/Models/MockDraft.swift +++ b/Modules/InboxComposer/Sources/Composer/UIKitComponents/Models/MockDraft.swift @@ -307,6 +307,6 @@ final class MockAttachmentList: AttachmentListProtocol, @unchecked Sendable { } func watcherStream() async -> AttachmentListWatcherStreamResult { - .ok(DraftAttachmentListUpdateStream.init(noHandle: .init())) + .ok(DraftAttachmentListUpdateStream.init(noPointer: .init())) } } diff --git a/Modules/InboxComposer/Tests/Unit/ComposerProtonContactsDatasourceTests.swift b/Modules/InboxComposer/Tests/Unit/ComposerProtonContactsDatasourceTests.swift index 4756a50a10..5c4843e8a0 100644 --- a/Modules/InboxComposer/Tests/Unit/ComposerProtonContactsDatasourceTests.swift +++ b/Modules/InboxComposer/Tests/Unit/ComposerProtonContactsDatasourceTests.swift @@ -141,7 +141,7 @@ private extension Array where Element == ContactSuggestion { private extension MailUserSession { static func empty() -> MailUserSession { - MailUserSession(noHandle: .init()) + MailUserSession(noPointer: .init()) } } @@ -150,12 +150,12 @@ private class ContactSuggestionsStub: ContactSuggestions, @unchecked Sendable { init(all: [ContactSuggestion]) { _all = all - super.init(noHandle: .init()) + super.init(noPointer: .init()) } @available(*, unavailable) - required init(unsafeFromHandle handle: UInt64) { - fatalError("init(unsafeFromHandle:) has not been implemented") + required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + fatalError("init(unsafeFromRawPointer:) has not been implemented") } override func all() -> [ContactSuggestion] { diff --git a/Modules/InboxContacts/Sources/Generated/Rust.generated.swift b/Modules/InboxContacts/Sources/Generated/Rust.generated.swift index b3a3d6d645..7617246301 100644 --- a/Modules/InboxContacts/Sources/Generated/Rust.generated.swift +++ b/Modules/InboxContacts/Sources/Generated/Rust.generated.swift @@ -394,6 +394,16 @@ public extension PasswordFlowSubmitTotpResult { } } } +public extension ResolveMessageFromPushNotificationResult { + func get() throws(ActionError) -> Message { + switch self { + case .ok(let value): + value + case .error(let error): + throw error + } + } +} public extension ResolveMessageIdResult { func get() throws(ActionError) -> Id { switch self { diff --git a/Modules/InboxContacts/Sources/Screens/Contacts/ContactsScreen.swift b/Modules/InboxContacts/Sources/Screens/Contacts/ContactsScreen.swift index 288892853c..c6c06f17e8 100644 --- a/Modules/InboxContacts/Sources/Screens/Contacts/ContactsScreen.swift +++ b/Modules/InboxContacts/Sources/Screens/Contacts/ContactsScreen.swift @@ -139,7 +139,7 @@ public struct ContactsScreen: View { #Preview { ContactsScreen( apiConfig: .debugPreview, - mailUserSession: .init(noHandle: .init()), + mailUserSession: .init(noPointer: .init()), contactsProvider: .previewInstance(), contactsWatcher: .previewInstance(), draftPresenter: ContactsDraftPresenterDummy() diff --git a/Modules/InboxContacts/Sources/Screens/Contacts/Extensions/ContactsWatcher+PreviewInstance.swift b/Modules/InboxContacts/Sources/Screens/Contacts/Extensions/ContactsWatcher+PreviewInstance.swift index 2b1a5b9c5c..5f8bb8393a 100644 --- a/Modules/InboxContacts/Sources/Screens/Contacts/Extensions/ContactsWatcher+PreviewInstance.swift +++ b/Modules/InboxContacts/Sources/Screens/Contacts/Extensions/ContactsWatcher+PreviewInstance.swift @@ -19,6 +19,6 @@ import proton_app_uniffi extension ContactsWatcher { static func previewInstance() -> Self { - .init(watch: { _, _ in .ok(.init(contactList: [], handle: .init(noHandle: .init()))) }) + .init(watch: { _, _ in .ok(.init(contactList: [], handle: .init(noPointer: .init()))) }) } } diff --git a/Modules/InboxContacts/Tests/Fixtures/MailUserSession+TestInstance.swift b/Modules/InboxContacts/Tests/Fixtures/MailUserSession+TestInstance.swift index 90e4a5f63a..cabaf929ad 100644 --- a/Modules/InboxContacts/Tests/Fixtures/MailUserSession+TestInstance.swift +++ b/Modules/InboxContacts/Tests/Fixtures/MailUserSession+TestInstance.swift @@ -19,6 +19,6 @@ import proton_app_uniffi extension MailUserSession { static func testInstance() -> MailUserSession { - .init(noHandle: .init()) + .init(noPointer: .init()) } } diff --git a/Modules/InboxContacts/Tests/Unit/DeviceContacts/ContactSuggestionsRepositoryTests.swift b/Modules/InboxContacts/Tests/Unit/DeviceContacts/ContactSuggestionsRepositoryTests.swift index 435cc9c253..9d20b69c22 100644 --- a/Modules/InboxContacts/Tests/Unit/DeviceContacts/ContactSuggestionsRepositoryTests.swift +++ b/Modules/InboxContacts/Tests/Unit/DeviceContacts/ContactSuggestionsRepositoryTests.swift @@ -39,7 +39,7 @@ final class ContactSuggestionsRepositoryTests { let result = ContactSuggestionsStub(all: self.stubbedAllContacts) return .ok(result) }), - mailUserSession: MailUserSession(noHandle: .init()) + mailUserSession: MailUserSession(noPointer: .init()) ) } @@ -349,12 +349,12 @@ private class ContactSuggestionsStub: ContactSuggestions, @unchecked Sendable { init(all: [ContactSuggestion]) { _all = all - super.init(noHandle: .init()) + super.init(noPointer: .init()) } @available(*, unavailable) - required init(unsafeFromHandle handle: UInt64) { - fatalError("init(unsafeFromHandle:) has not been implemented") + required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + fatalError("init(unsafeFromRawPointer:) has not been implemented") } override func all() -> [ContactSuggestion] { diff --git a/Modules/InboxContacts/Tests/Unit/Navigation/ContactViewFactoryTests.swift b/Modules/InboxContacts/Tests/Unit/Navigation/ContactViewFactoryTests.swift index 1fb0b443ae..6763befd76 100644 --- a/Modules/InboxContacts/Tests/Unit/Navigation/ContactViewFactoryTests.swift +++ b/Modules/InboxContacts/Tests/Unit/Navigation/ContactViewFactoryTests.swift @@ -26,7 +26,7 @@ import proton_app_uniffi final class ContactViewFactoryTests { let sut = ContactViewFactory( apiConfig: .testData, - mailUserSession: .init(noHandle: .init()), + mailUserSession: .init(noPointer: .init()), draftPresenter: ContactsDraftPresenterDummy() ) diff --git a/Modules/InboxContacts/Tests/Unit/Screens/Contacts/ContactsStateStoreTests.swift b/Modules/InboxContacts/Tests/Unit/Screens/Contacts/ContactsStateStoreTests.swift index 427817caa9..89b196f986 100644 --- a/Modules/InboxContacts/Tests/Unit/Screens/Contacts/ContactsStateStoreTests.swift +++ b/Modules/InboxContacts/Tests/Unit/Screens/Contacts/ContactsStateStoreTests.swift @@ -627,7 +627,7 @@ final class ContactsStateStoreTests { }, contactsWatcher: .init(watch: { [unowned self] _, callback in watchContactsCallback = callback - return WatchContactListResult.ok(.init(contactList: [], handle: .init(noHandle: .init()))) + return WatchContactListResult.ok(.init(contactList: [], handle: .init(noPointer: .init()))) }) ), makeContactsLiveQuery: { [unowned self] in diff --git a/Modules/InboxContacts/Tests/Unit/Screens/Contacts/GroupedContactsRepositoryTests.swift b/Modules/InboxContacts/Tests/Unit/Screens/Contacts/GroupedContactsRepositoryTests.swift index 5a7c4ec25c..18b550c484 100644 --- a/Modules/InboxContacts/Tests/Unit/Screens/Contacts/GroupedContactsRepositoryTests.swift +++ b/Modules/InboxContacts/Tests/Unit/Screens/Contacts/GroupedContactsRepositoryTests.swift @@ -23,7 +23,7 @@ import proton_app_uniffi final class GroupedContactsRepositoryTests { private lazy var sut: GroupedContactsRepository = .init( - mailUserSession: MailUserSession(noHandle: .init()), + mailUserSession: MailUserSession(noPointer: .init()), contactsProvider: .init(allContacts: { _ in .ok(self.stubbedContacts) }) ) private var stubbedContacts: [GroupedContacts] = [] diff --git a/Modules/InboxCoreUI/Sources/Generated/Rust.generated.swift b/Modules/InboxCoreUI/Sources/Generated/Rust.generated.swift index 8a00f17802..d76b502bdb 100644 --- a/Modules/InboxCoreUI/Sources/Generated/Rust.generated.swift +++ b/Modules/InboxCoreUI/Sources/Generated/Rust.generated.swift @@ -54,6 +54,26 @@ public extension ChallengeLoaderPutResult { } } } +public extension ConversationScrollerCategoryViewResult { + func get() throws(MailScrollerError) -> CategoryView { + switch self { + case .ok(let value): + value + case .error(let error): + throw error + } + } +} +public extension ConversationScrollerChangeCategoryViewResult { + func get() throws(MailScrollerError) { + switch self { + case .ok: + break + case .error(let error): + throw error + } + } +} public extension ConversationScrollerChangeFilterResult { func get() throws(MailScrollerError) { switch self { @@ -1184,6 +1204,26 @@ public extension MailboxWatchUnreadCountResult { } } } +public extension MessageScrollerCategoryViewResult { + func get() throws(MailScrollerError) -> CategoryView { + switch self { + case .ok(let value): + value + case .error(let error): + throw error + } + } +} +public extension MessageScrollerChangeCategoryViewResult { + func get() throws(MailScrollerError) { + switch self { + case .ok: + break + case .error(let error): + throw error + } + } +} public extension MessageScrollerChangeFilterResult { func get() throws(MailScrollerError) { switch self { @@ -1344,96 +1384,6 @@ public extension NewMailboxResult { } } } -public extension PasswordFlowChangeMboxPassResult { - func get() throws(PasswordError) -> SimplePasswordState { - switch self { - case .ok(let value): - value - case .error(let error): - throw error - } - } -} -public extension PasswordFlowChangePassResult { - func get() throws(PasswordError) -> SimplePasswordState { - switch self { - case .ok(let value): - value - case .error(let error): - throw error - } - } -} -public extension PasswordFlowFidoDetailsResult { - func get() throws(PasswordError) -> Fido2ResponseFfi? { - switch self { - case .ok(let value): - value - case .error(let error): - throw error - } - } -} -public extension PasswordFlowHasFidoResult { - func get() throws(PasswordError) -> Bool { - switch self { - case .ok(let value): - value - case .error(let error): - throw error - } - } -} -public extension PasswordFlowHasMbpResult { - func get() throws(PasswordError) -> Bool { - switch self { - case .ok(let value): - value - case .error(let error): - throw error - } - } -} -public extension PasswordFlowHasTotpResult { - func get() throws(PasswordError) -> Bool { - switch self { - case .ok(let value): - value - case .error(let error): - throw error - } - } -} -public extension PasswordFlowStepBackResult { - func get() throws(PasswordError) -> SimplePasswordState { - switch self { - case .ok(let value): - value - case .error(let error): - throw error - } - } -} -public extension PasswordFlowSubmitFidoResult { - func get() throws(PasswordError) -> SimplePasswordState { - switch self { - case .ok(let value): - value - case .error(let error): - throw error - } - } -} -public extension PasswordFlowSubmitTotpResult { - func get() throws(PasswordError) -> SimplePasswordState { - switch self { - case .ok(let value): - value - case .error(let error): - throw error - } - } -} public extension ResolveSystemLabelByIdResult { func get() throws(ProtonError) -> SystemLabel? { switch self { @@ -1564,116 +1514,6 @@ public extension SearchScrollerTotalResult { } } } -public extension SignupFlowAvailableCountriesResult { - func get() throws(SignupError) -> Countries { - switch self { - case .ok(let value): - value - case .error(let error): - throw error - } - } -} -public extension SignupFlowAvailableDomainsResult { - func get() throws(SignupError) -> [String] { - switch self { - case .ok(let value): - value - case .error(let error): - throw error - } - } -} -public extension SignupFlowCompleteResult { - func get() throws(SignupError) -> UserAddrId { - switch self { - case .ok(let value): - value - case .error(let error): - throw error - } - } -} -public extension SignupFlowCreateResult { - func get() throws(SignupError) -> SimpleSignupState { - switch self { - case .ok(let value): - value - case .error(let error): - throw error - } - } -} -public extension SignupFlowSkipRecoveryResult { - func get() throws(SignupError) -> SimpleSignupState { - switch self { - case .ok(let value): - value - case .error(let error): - throw error - } - } -} -public extension SignupFlowStepBackResult { - func get() throws(SignupError) -> SimpleSignupState { - switch self { - case .ok(let value): - value - case .error(let error): - throw error - } - } -} -public extension SignupFlowSubmitExternalUsernameResult { - func get() throws(SignupError) -> SimpleSignupState { - switch self { - case .ok(let value): - value - case .error(let error): - throw error - } - } -} -public extension SignupFlowSubmitInternalUsernameResult { - func get() throws(SignupError) -> SimpleSignupState { - switch self { - case .ok(let value): - value - case .error(let error): - throw error - } - } -} -public extension SignupFlowSubmitPasswordResult { - func get() throws(SignupError) -> SimpleSignupState { - switch self { - case .ok(let value): - value - case .error(let error): - throw error - } - } -} -public extension SignupFlowSubmitRecoveryEmailResult { - func get() throws(SignupError) -> SimpleSignupState { - switch self { - case .ok(let value): - value - case .error(let error): - throw error - } - } -} -public extension SignupFlowSubmitRecoveryPhoneResult { - func get() throws(SignupError) -> SimpleSignupState { - switch self { - case .ok(let value): - value - case .error(let error): - throw error - } - } -} public extension UpdateNextMessageOnMoveResult { func get() throws(UserSessionError) { switch self { diff --git a/Modules/InboxCoreUI/Sources/Resources/Localizable.xcstrings b/Modules/InboxCoreUI/Sources/Resources/Localizable.xcstrings index 885e486765..786c64edd7 100644 --- a/Modules/InboxCoreUI/Sources/Resources/Localizable.xcstrings +++ b/Modules/InboxCoreUI/Sources/Resources/Localizable.xcstrings @@ -4852,7 +4852,7 @@ "sl" : { "stringUnit" : { "state" : "translated", - "value" : "[Preverjanje pošiljatelja](https://proton.me/support/sender-verification-failed) je spodletelo, ker ni bilo podpisano. Prosimo, potrdite pristnost e-pošte pri svojemu stiku." + "value" : "[Preverjanje pošiljatelja](https://proton.me/support/sender-verification-failed) je spodletelo, ker ni bilo podpisano. Potrdite pristnost e-pošte pri svojemu stiku." } }, "sv" : { @@ -5001,7 +5001,7 @@ "sl" : { "stringUnit" : { "state" : "translated", - "value" : "[Preverjanje pošiljatelja](https://proton.me/support/sender-verification-failed) ni uspelo. Prosimo, potrdite pristnost e-pošte pri svojemu stiku." + "value" : "[Preverjanje pošiljatelja](https://proton.me/support/sender-verification-failed) ni uspelo. Potrdite pristnost e-pošte pri svojemu stiku." } }, "sv" : { @@ -5150,7 +5150,7 @@ "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Sporočilo je na strežnikih Proton shranjeno z [šifriranjem brez dostopa](https://proton.me/blog/zero-access-encryption). Niti Proton niti nihče drug ga ne more prebrati." + "value" : "Sporočilo je na strežnikih Proton shranjeno s [šifriranjem brez dostopa](https://proton.me/blog/zero-access-encryption). Niti Proton niti nihče drug ga ne more prebrati." } }, "tr" : { @@ -5293,7 +5293,7 @@ "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Prejemnik je [preverjen stik](https://proton.me/support/address-verification), kateremu ste zaupali šifrirne ključe." + "value" : "Prejemnik je [preverjen stik](https://proton.me/support/address-verification), njegove šifrirne ključe ste potrdili kot zaupanja vredne." } }, "sv" : { @@ -5442,7 +5442,7 @@ "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Prejemniki so [preverjeni stiki](https://proton.me/support/address-verification), katerim ste zaupali šifrirne ključe." + "value" : "Prejemniki so [preverjeni stiki](https://proton.me/support/address-verification), njihove šifrirne ključe ste potrdili kot zaupanja vredne." } }, "sv" : { @@ -5591,7 +5591,7 @@ "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Pošiljatelj je [preverjen stik](https://proton.me/support/address-verification), kateremu ste zaupali šifrirne ključe." + "value" : "Pošiljatelj je [preverjen stik](https://proton.me/support/address-verification), njegove šifrirne ključe ste potrdili kot zaupanja vredne." } }, "sv" : { @@ -5883,7 +5883,7 @@ "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Ta e-pošta je na strežnikih Proton shranjena z [šifriranjem brez dostopa](https://proton.me/blog/zero-access-encryption). Niti Proton niti kdorkoli drug je ne more prebrati." + "value" : "Ta e-pošta je na strežnikih Proton shranjena s [šifriranjem brez dostopa](https://proton.me/blog/zero-access-encryption). Niti Proton niti kdorkoli drug je ne more prebrati." } }, "tr" : { @@ -7016,7 +7016,7 @@ "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Šifrirano brez dostopa za preverjenega prejemnika" + "value" : "Šifrirano brez dostopa s preverjenim prejemnikom" } }, "sv" : { @@ -7159,7 +7159,7 @@ "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Šifrirano brez dostopa za preverjene prejemnike" + "value" : "Šifrirano brez dostopa s preverjenimi prejemniki" } }, "sv" : { diff --git a/Modules/InboxDesignSystem/Sources/Icons.swift b/Modules/InboxDesignSystem/Sources/Icons.swift index 71ca9b0f38..5e95186172 100644 --- a/Modules/InboxDesignSystem/Sources/Icons.swift +++ b/Modules/InboxDesignSystem/Sources/Icons.swift @@ -87,6 +87,7 @@ public extension DS.Icon { public extension DS.Icon { static let icBrandProtonMailUpsell = ImageResource.icBrandProtonMailUpsell static let icBrandProtonMailUpsellBlackAndWhite = ImageResource.icBrandProtonMailUpsellBw + static let icBrandProtonUnlimitedUpsellHeader = ImageResource.icBrandProtonUnlimitedUpsellHeader static let upsellBlackFridayHeaderButtonWave1 = ImageResource.upsellBlackFridayHeaderButtonWave1 static let upsellBlackFridayHeaderButtonWave2 = ImageResource.upsellBlackFridayHeaderButtonWave2 static let upsellBlackFridaySidebarItemWave1 = ImageResource.upsellBlackFridaySidebarItemWave1 @@ -98,6 +99,9 @@ public extension DS.Icon { public extension DS.Icon { static let icCode = ImageResource.icCode static let icDiamond = ImageResource.icDiamond + static let icInfinity = ImageResource.icInfinity + static let icInfinityUpsellHeader = ImageResource.icInfinityUpsellHeader + static let icInfinityUpsellRow = ImageResource.icInfinityUpsellRow static let icEnvelopeDot = ImageResource.icEnvelopeDot static let icEnvelopeOpen = ImageResource.icEnvelopeOpen static let icFileLines = ImageResource.icFileLines diff --git a/Modules/InboxDesignSystem/Sources/Images.swift b/Modules/InboxDesignSystem/Sources/Images.swift index bf7abbd621..064592d374 100644 --- a/Modules/InboxDesignSystem/Sources/Images.swift +++ b/Modules/InboxDesignSystem/Sources/Images.swift @@ -41,6 +41,7 @@ public extension DS.Images { public static let logoMobileSignature = ImageResource.upsellLogoMobileSignature public static let logoScheduleSend = ImageResource.upsellLogoScheduleSend public static let logoSnooze = ImageResource.upsellLogoSnooze + public static let logoUnlimited = ImageResource.upsellLogoUnlimited public enum BlackFriday { public static let background = ImageResource.upsellBlackFridayBackground diff --git a/Modules/InboxDesignSystem/Sources/Resources/Assets.xcassets/IconSet/ic-brand-proton-unlimited-upsell-header.imageset/Contents.json b/Modules/InboxDesignSystem/Sources/Resources/Assets.xcassets/IconSet/ic-brand-proton-unlimited-upsell-header.imageset/Contents.json new file mode 100644 index 0000000000..11545a1ccd --- /dev/null +++ b/Modules/InboxDesignSystem/Sources/Resources/Assets.xcassets/IconSet/ic-brand-proton-unlimited-upsell-header.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Frame 944441499.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/InboxDesignSystem/Sources/Resources/Assets.xcassets/IconSet/ic-brand-proton-unlimited-upsell-header.imageset/Frame 944441499.pdf b/Modules/InboxDesignSystem/Sources/Resources/Assets.xcassets/IconSet/ic-brand-proton-unlimited-upsell-header.imageset/Frame 944441499.pdf new file mode 100644 index 0000000000..90097ca659 Binary files /dev/null and b/Modules/InboxDesignSystem/Sources/Resources/Assets.xcassets/IconSet/ic-brand-proton-unlimited-upsell-header.imageset/Frame 944441499.pdf differ diff --git a/Modules/InboxDesignSystem/Sources/Resources/Assets.xcassets/IconSet/ic-infinity-upsell-header.imageset/Contents.json b/Modules/InboxDesignSystem/Sources/Resources/Assets.xcassets/IconSet/ic-infinity-upsell-header.imageset/Contents.json new file mode 100644 index 0000000000..71d2d81f96 --- /dev/null +++ b/Modules/InboxDesignSystem/Sources/Resources/Assets.xcassets/IconSet/ic-infinity-upsell-header.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Plan icon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/InboxDesignSystem/Sources/Resources/Assets.xcassets/IconSet/ic-infinity-upsell-header.imageset/Plan icon.pdf b/Modules/InboxDesignSystem/Sources/Resources/Assets.xcassets/IconSet/ic-infinity-upsell-header.imageset/Plan icon.pdf new file mode 100644 index 0000000000..c819a09a3f Binary files /dev/null and b/Modules/InboxDesignSystem/Sources/Resources/Assets.xcassets/IconSet/ic-infinity-upsell-header.imageset/Plan icon.pdf differ diff --git a/Modules/InboxDesignSystem/Sources/Resources/Assets.xcassets/IconSet/ic-infinity-upsell-row.imageset/Contents.json b/Modules/InboxDesignSystem/Sources/Resources/Assets.xcassets/IconSet/ic-infinity-upsell-row.imageset/Contents.json new file mode 100644 index 0000000000..bfbeb098c2 --- /dev/null +++ b/Modules/InboxDesignSystem/Sources/Resources/Assets.xcassets/IconSet/ic-infinity-upsell-row.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Icon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/InboxDesignSystem/Sources/Resources/Assets.xcassets/IconSet/ic-infinity-upsell-row.imageset/Icon.pdf b/Modules/InboxDesignSystem/Sources/Resources/Assets.xcassets/IconSet/ic-infinity-upsell-row.imageset/Icon.pdf new file mode 100644 index 0000000000..e4f039fd16 Binary files /dev/null and b/Modules/InboxDesignSystem/Sources/Resources/Assets.xcassets/IconSet/ic-infinity-upsell-row.imageset/Icon.pdf differ diff --git a/Modules/InboxDesignSystem/Sources/Resources/Assets.xcassets/IconSet/ic-infinity.imageset/Contents.json b/Modules/InboxDesignSystem/Sources/Resources/Assets.xcassets/IconSet/ic-infinity.imageset/Contents.json new file mode 100644 index 0000000000..dd9e55cfc5 --- /dev/null +++ b/Modules/InboxDesignSystem/Sources/Resources/Assets.xcassets/IconSet/ic-infinity.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "icon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/InboxDesignSystem/Sources/Resources/Assets.xcassets/IconSet/ic-infinity.imageset/icon.pdf b/Modules/InboxDesignSystem/Sources/Resources/Assets.xcassets/IconSet/ic-infinity.imageset/icon.pdf new file mode 100644 index 0000000000..51b1eb11ef Binary files /dev/null and b/Modules/InboxDesignSystem/Sources/Resources/Assets.xcassets/IconSet/ic-infinity.imageset/icon.pdf differ diff --git a/Modules/InboxDesignSystem/Sources/Resources/Assets.xcassets/Images/upsell/upsell_logo_unlimited.imageset/Contents.json b/Modules/InboxDesignSystem/Sources/Resources/Assets.xcassets/Images/upsell/upsell_logo_unlimited.imageset/Contents.json new file mode 100644 index 0000000000..bf6c9216a2 --- /dev/null +++ b/Modules/InboxDesignSystem/Sources/Resources/Assets.xcassets/Images/upsell/upsell_logo_unlimited.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "upsell_logo_unlimited.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/InboxDesignSystem/Sources/Resources/Assets.xcassets/Images/upsell/upsell_logo_unlimited.imageset/upsell_logo_unlimited.pdf b/Modules/InboxDesignSystem/Sources/Resources/Assets.xcassets/Images/upsell/upsell_logo_unlimited.imageset/upsell_logo_unlimited.pdf new file mode 100644 index 0000000000..9cfccc58cc Binary files /dev/null and b/Modules/InboxDesignSystem/Sources/Resources/Assets.xcassets/Images/upsell/upsell_logo_unlimited.imageset/upsell_logo_unlimited.pdf differ diff --git a/Modules/InboxIAP/Sources/Integration/UpsellCoordinator.swift b/Modules/InboxIAP/Sources/Integration/UpsellCoordinator.swift index 1bec09b6ee..46593d0cea 100644 --- a/Modules/InboxIAP/Sources/Integration/UpsellCoordinator.swift +++ b/Modules/InboxIAP/Sources/Integration/UpsellCoordinator.swift @@ -95,7 +95,7 @@ public final class UpsellCoordinator: ObservableObject { let availablePlans = try await fetchAvailablePlans() let model = try upsellScreenFactory.upsellScreenModel( - showingPlan: configuration.regularPlan, + showingPlan: upsellType.planVariant, basedOn: availablePlans, entryPoint: entryPoint, upsellType: upsellType diff --git a/Modules/InboxIAP/Sources/L10n.swift b/Modules/InboxIAP/Sources/L10n.swift index 469a762334..05ae74495a 100644 --- a/Modules/InboxIAP/Sources/L10n.swift +++ b/Modules/InboxIAP/Sources/L10n.swift @@ -114,6 +114,12 @@ enum L10n { return .init("\(measurement.formatted()) storage", bundle: .module, comment: "Amount of storage space available in a given plan, for example: 1 GB storage") } + static let hideMyEmailAliases = LocalizedStringResource("Hide My Email aliases", bundle: .module, comment: "Description of a feature of a paid subscription") + static let foldersAndLabels = LocalizedStringResource("Folders and labels", bundle: .module, comment: "Description of a feature of a paid subscription") + static let premiumVpnPasswordManagerCloudStorage = LocalizedStringResource( + "Premium VPN, password manager and cloud storage", bundle: .module, comment: "Description of a feature of a paid subscription") + static let darkWebMonitoring = LocalizedStringResource("Dark Web Monitoring", bundle: .module, comment: "Description of a feature of a paid subscription") + static func numberOfEmailAddresses(_ amount: UInt) -> LocalizedStringResource { .init("\(amount) email addresses", bundle: .module, comment: "Number of email addresses available in a given plan") } @@ -130,6 +136,15 @@ enum L10n { comment: "Notice at the bottom of the upsell page" ) } + + static func autoRenewalNotice(price: String, period: Product.SubscriptionPeriod.Unit) -> LocalizedStringResource { + let periodSuffix = "/\(period.localizedDescription.lowercased())" + return .init( + "Auto-renews at \(price)\(periodSuffix) unless canceled", + bundle: .module, + comment: "Disclaimer shown at the bottom of the upsell page; first placeholder is the localized price, second is the period suffix (/year or /month)" + ) + } } private extension LocalizedStringResource.BundleDescription { diff --git a/Modules/InboxIAP/Sources/Resources/Localizable.xcstrings b/Modules/InboxIAP/Sources/Resources/Localizable.xcstrings index eaa28b6400..348cabc551 100644 --- a/Modules/InboxIAP/Sources/Resources/Localizable.xcstrings +++ b/Modules/InboxIAP/Sources/Resources/Localizable.xcstrings @@ -758,6 +758,9 @@ } } }, + "Auto-renews at %@%@ unless canceled" : { + "comment" : "Disclaimer shown at the bottom of the upsell page; first placeholder is the localized price, second is the period suffix (/year or /month)" + }, "Auto-renews at the same price and terms unless canceled" : { "comment" : "Notice at the bottom", "localizations" : { @@ -2626,6 +2629,9 @@ } } }, + "Dark Web Monitoring" : { + "comment" : "Description of a feature of a paid subscription" + }, "Deletes spam and trash after 30 days. Get this and more with %@." : { "comment" : "Subtitle of the upsell page", "localizations" : { @@ -3169,6 +3175,9 @@ } } }, + "Folders and labels" : { + "comment" : "Description of a feature of a paid subscription" + }, "Free" : { "comment" : "Name of the free plan", "localizations" : { @@ -3742,6 +3751,9 @@ } } }, + "Hide My Email aliases" : { + "comment" : "Description of a feature of a paid subscription" + }, "Make your mobile signature your own. Enjoy this and more with %@." : { "comment" : "Subtitle of the upsell page", "localizations" : { @@ -5037,6 +5049,9 @@ } } }, + "Premium VPN, password manager and cloud storage" : { + "comment" : "Description of a feature of a paid subscription" + }, "Priority customer support" : { "comment" : "Description of a feature of a paid subscription", "localizations" : { diff --git a/Modules/InboxIAP/Sources/Telemetry/TelemetryReporter.swift b/Modules/InboxIAP/Sources/Telemetry/TelemetryReporter.swift index 3eb1a66746..6e41eb4962 100644 --- a/Modules/InboxIAP/Sources/Telemetry/TelemetryReporter.swift +++ b/Modules/InboxIAP/Sources/Telemetry/TelemetryReporter.swift @@ -64,7 +64,12 @@ final class TelemetryReporter: TelemetryReporting { } private func generalDimensions() -> GeneralDimensions { - .init(upsellEntryPoint: entryPoint!, planBeforeUpgrade: "free", modalVariant: .comparisonPlus) + .init( + upsellEntryPoint: entryPoint!, + planBeforeUpgrade: "free", + modalVariant: .comparisonPlus, + upsellFeatureFlags: upsellFeatureFlagsForIos() + ) } private func planSpecificDimensions(storeKitProductID: String) -> PlanSpecificDimensions { diff --git a/Modules/InboxIAP/Sources/UpsellScreen/Regular/Comparison/ComparisonItemType.swift b/Modules/InboxIAP/Sources/UpsellScreen/Regular/Comparison/ComparisonItemType.swift index c1e6fd6a4b..ac20259b58 100644 --- a/Modules/InboxIAP/Sources/UpsellScreen/Regular/Comparison/ComparisonItemType.swift +++ b/Modules/InboxIAP/Sources/UpsellScreen/Regular/Comparison/ComparisonItemType.swift @@ -16,8 +16,10 @@ // You should have received a copy of the GNU General Public License // along with Proton Mail. If not, see https://www.gnu.org/licenses/. +import SwiftUI + enum ComparisonItemType { case boolean - case string(free: String, plus: String) - case integer(free: Int, plus: Int) + case string(free: String, plan: String) + case stringAndIcon(free: String, plan: Image) } diff --git a/Modules/InboxIAP/Sources/UpsellScreen/Regular/Comparison/PlanComparisonGrid.swift b/Modules/InboxIAP/Sources/UpsellScreen/Regular/Comparison/PlanComparisonGrid.swift index 6d7a3ff2e4..5d4e632324 100644 --- a/Modules/InboxIAP/Sources/UpsellScreen/Regular/Comparison/PlanComparisonGrid.swift +++ b/Modules/InboxIAP/Sources/UpsellScreen/Regular/Comparison/PlanComparisonGrid.swift @@ -20,22 +20,24 @@ import InboxDesignSystem import SwiftUI struct PlanComparisonGrid: View { - private let items: [ComparisonItem] = [ - .init(title: \.storage, type: .string(free: gigabytesString(1), plus: gigabytesString(15))), - .init(title: \.emailAddresses, type: .integer(free: 1, plus: 10)), - .init(title: \.customEmailDomain, type: .boolean), - .init(title: \.accessToDesktopApp, type: .boolean), - .init(title: \.unlimitedFoldersAndLabels, type: .boolean), - .init(title: \.priorityCustomerSupport, type: .boolean), - ] + struct Configuration { + enum ColumnHeader { + case text(LocalizedStringResource) + case icon(Image) + } + let planColumnHeader: ColumnHeader + let headerStroke: LinearGradient? + let items: [ComparisonItem] + } + + private let configuration: Configuration private let highlightBorderWidth: CGFloat = 2 - private let highlightStroke: any ShapeStyle @State private var highlightedColumnWidth: CGFloat = 0 - init(highlightStroke: (any ShapeStyle)? = nil) { - self.highlightStroke = highlightStroke ?? LinearGradient.highlight + init(configuration: Configuration) { + self.configuration = configuration } var body: some View { @@ -45,13 +47,15 @@ struct PlanComparisonGrid: View { Text(L10n.PlanName.free) - Text(L10n.PlanName.plus) + planColumnHeaderView .padding(.vertical, DS.Spacing.compact) .padding(.horizontal, DS.Spacing.standard) .overlay { - RoundedRectangle(cornerRadius: DS.Radius.medium) - .stroke(AnyShapeStyle(highlightStroke), lineWidth: highlightBorderWidth) - .padding(highlightBorderWidth / 2) + if let headerStroke = configuration.headerStroke { + RoundedRectangle(cornerRadius: DS.Radius.medium) + .stroke(AnyShapeStyle(headerStroke), lineWidth: highlightBorderWidth) + .padding(highlightBorderWidth / 2) + } } .padding(.horizontal, DS.Spacing.small) .coordinatedMinWidth(using: _highlightedColumnWidth) @@ -59,10 +63,10 @@ struct PlanComparisonGrid: View { .font(.callout) .fontWeight(.semibold) - ForEach(items.indices, id: \.self) { itemIndex in - gridRow(for: items[itemIndex]) + ForEach(configuration.items.indices, id: \.self) { itemIndex in + gridRow(for: configuration.items[itemIndex]) - if itemIndex != items.indices.last { + if itemIndex != configuration.items.indices.last { Divider() .overlay(.white.opacity(0.12)) } @@ -82,6 +86,19 @@ struct PlanComparisonGrid: View { } } + @ViewBuilder + private var planColumnHeaderView: some View { + switch configuration.planColumnHeader { + case .text(let resource): + Text(resource) + case .icon(let image): + image + .resizable() + .scaledToFit() + .frame(height: 32) + } + } + private func gridRow(for item: ComparisonItem) -> some View { GridRow { Text(L10n.Perk.self[keyPath: item.title]) @@ -97,25 +114,24 @@ struct PlanComparisonGrid: View { .symbolRenderingMode(.palette) .foregroundStyle(.white, .black.opacity(0.2)) .coordinatedMinWidth(using: _highlightedColumnWidth) - case .integer(let valueForFreePlan, let valueForPlus): - Text("\(valueForFreePlan)") - - Text("\(valueForPlus)") - .coordinatedMinWidth(using: _highlightedColumnWidth) - case .string(let valueForFreePlan, let valueForPlus): + case .string(let valueForFreePlan, let valueForPlan): Text(valueForFreePlan) - Text(valueForPlus) + Text(valueForPlan) + .padding(.horizontal, DS.Spacing.small) + .coordinatedMinWidth(using: _highlightedColumnWidth) + case .stringAndIcon(let valueForFreePlan, let iconForPlan): + Text(valueForFreePlan) + + iconForPlan + .font(.system(size: 20)) + .padding(.horizontal, DS.Spacing.small) .coordinatedMinWidth(using: _highlightedColumnWidth) } } .fontWeight(.semibold) } } - - private static func gigabytesString(_ value: Double) -> String { - Measurement(value: value, unit: .gigabytes).formatted() - } } private extension View { @@ -128,7 +144,16 @@ private extension View { #Preview { ScrollView { - PlanComparisonGrid() + PlanComparisonGrid( + configuration: .init( + planColumnHeader: .text(L10n.PlanName.plus), + headerStroke: LinearGradient.highlight, + items: [ + .init(title: \.storage, type: .string(free: "1 GB", plan: "15 GB")), + .init(title: \.customEmailDomain, type: .boolean), + ] + ) + ) } .background(LinearGradient.screenBackground) .preferredColorScheme(.dark) diff --git a/Modules/InboxIAP/Sources/UpsellScreen/Regular/SubscriptionPeriodRadioGroup/DisplayablePlanInstance+Preview.swift b/Modules/InboxIAP/Sources/UpsellScreen/Regular/SubscriptionPeriodRadioGroup/DisplayablePlanInstance+Preview.swift index 5af1845347..54411c0992 100644 --- a/Modules/InboxIAP/Sources/UpsellScreen/Regular/SubscriptionPeriodRadioGroup/DisplayablePlanInstance+Preview.swift +++ b/Modules/InboxIAP/Sources/UpsellScreen/Regular/SubscriptionPeriodRadioGroup/DisplayablePlanInstance+Preview.swift @@ -21,7 +21,11 @@ extension DisplayablePlanInstance { .init( storeKitProductId: "iosmail_mail2022_12_usd_auto_renewing", cycleInMonths: 12, - pricing: .regular(monthlyPrice: "$3.99"), + pricing: .discountedYearlyPlan( + discountedMonthlyPrice: "$3.99", + discountedYearlyPrice: "$47.88", + renewalPrice: "$47.88" + ), discount: 20, ), .init( diff --git a/Modules/InboxIAP/Sources/UpsellScreen/Regular/SubscriptionPeriodRadioGroup/SubscriptionPeriodRadioButton.swift b/Modules/InboxIAP/Sources/UpsellScreen/Regular/SubscriptionPeriodRadioGroup/SubscriptionPeriodRadioButton.swift index cc61ce4f70..a86e843a4d 100644 --- a/Modules/InboxIAP/Sources/UpsellScreen/Regular/SubscriptionPeriodRadioGroup/SubscriptionPeriodRadioButton.swift +++ b/Modules/InboxIAP/Sources/UpsellScreen/Regular/SubscriptionPeriodRadioGroup/SubscriptionPeriodRadioButton.swift @@ -104,7 +104,7 @@ struct SubscriptionPeriodRadioButton: View { VStack(alignment: .trailing, spacing: .zero) { pricePerPeriod(formattedPrice: discountedYearlyPrice, unit: .year, isEmphasized: true) - pricingText(L10n.onlyXPerMonth(discountedMonthlyPrice).string, isEmphasized: false) + pricePerPeriod(formattedPrice: discountedMonthlyPrice, unit: .month, isEmphasized: false) } case .discountedMonthlyPlan(let discountedPrice, let renewalPrice): VStack(alignment: .trailing, spacing: .zero) { diff --git a/Modules/InboxIAP/Sources/UpsellScreen/Regular/UpsellScreen.swift b/Modules/InboxIAP/Sources/UpsellScreen/Regular/UpsellScreen.swift index fd8a656c8e..8094d99b2c 100644 --- a/Modules/InboxIAP/Sources/UpsellScreen/Regular/UpsellScreen.swift +++ b/Modules/InboxIAP/Sources/UpsellScreen/Regular/UpsellScreen.swift @@ -148,7 +148,7 @@ public struct UpsellScreen: View { Spacer.exactly(DS.Spacing.huge) } - PlanComparisonGrid(highlightStroke: model.highlightStroke) + PlanComparisonGrid(configuration: model.comparisonConfiguration) } .padding(.top, headerHeight) .padding(.bottom, DS.Spacing.extraLarge) diff --git a/Modules/InboxIAP/Sources/UpsellScreen/Regular/UpsellScreenModel+Preview.swift b/Modules/InboxIAP/Sources/UpsellScreen/Regular/UpsellScreenModel+Preview.swift index c2a4367339..59247f884e 100644 --- a/Modules/InboxIAP/Sources/UpsellScreen/Regular/UpsellScreenModel+Preview.swift +++ b/Modules/InboxIAP/Sources/UpsellScreen/Regular/UpsellScreenModel+Preview.swift @@ -27,8 +27,16 @@ extension UpsellScreenModel { planInstances = DisplayablePlanInstance.previews } + let planName: String + switch upsellType { + case .mailPlus: + planName = "Mail Plus" + case .unlimited: + planName = "Proton Unlimited" + } + return .init( - planName: "Mail Plus", + planName: planName, planInstances: planInstances, entryPoint: entryPoint, upsellType: upsellType, diff --git a/Modules/InboxIAP/Sources/UpsellScreen/Regular/UpsellScreenModel.swift b/Modules/InboxIAP/Sources/UpsellScreen/Regular/UpsellScreenModel.swift index 401733c2cf..a21f11b8f6 100644 --- a/Modules/InboxIAP/Sources/UpsellScreen/Regular/UpsellScreenModel.swift +++ b/Modules/InboxIAP/Sources/UpsellScreen/Regular/UpsellScreenModel.swift @@ -40,11 +40,17 @@ public final class UpsellScreenModel: Identifiable { var logo: ImageResource { switch upsellType { - case .mailPlus, .unlimited: + case .mailPlus: entryPoint.logo + case .unlimited: + upsellType.logo } } + var comparisonConfiguration: PlanComparisonGrid.Configuration { + upsellType.comparisonConfiguration + } + var logoHeight: CGFloat? { isPromo ? nil : 118 } @@ -70,13 +76,14 @@ public final class UpsellScreenModel: Identifiable { } var autoRenewalNotice: LocalizedStringResource { - switch planInstances[0].pricing { - case .regular: - L10n.autoRenewalNotice + let selectedPlan = planInstances.first { $0.storeKitProductId == selectedInstanceId } ?? planInstances[0] + switch selectedPlan.pricing { + case .regular(let monthlyPrice): + return L10n.autoRenewalNotice(price: monthlyPrice, period: .month) case .discountedYearlyPlan(_, _, let renewalPrice): - L10n.discountRenewalNotice(renewalPrice: renewalPrice, period: .year) + return L10n.autoRenewalNotice(price: renewalPrice, period: .year) case .discountedMonthlyPlan(_, let renewalPrice): - L10n.discountRenewalNotice(renewalPrice: renewalPrice, period: .month) + return L10n.discountRenewalNotice(renewalPrice: renewalPrice, period: .month) } } diff --git a/Modules/InboxIAP/Sources/UpsellScreen/Regular/UpsellType+UpsellScreen.swift b/Modules/InboxIAP/Sources/UpsellScreen/Regular/UpsellType+UpsellScreen.swift new file mode 100644 index 0000000000..ce1845e598 --- /dev/null +++ b/Modules/InboxIAP/Sources/UpsellScreen/Regular/UpsellType+UpsellScreen.swift @@ -0,0 +1,82 @@ +// +// Copyright (c) 2026 Proton Technologies AG +// +// This file is part of Proton Mail. +// +// Proton Mail is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Proton Mail is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Proton Mail. If not, see https://www.gnu.org/licenses/. + +import InboxDesignSystem +import SwiftUI +import proton_app_uniffi + +extension UpsellType { + var planVariant: String { + switch self { + case .mailPlus: + SubscriptionPlanVariant.plus + case .unlimited: + SubscriptionPlanVariant.unlimited + } + } + + var logo: ImageResource { + switch self { + case .mailPlus: + DS.Images.Upsell.logoDefault + case .unlimited: + DS.Images.Upsell.logoUnlimited + } + } + + var comparisonConfiguration: PlanComparisonGrid.Configuration { + switch self { + case .mailPlus: + .init( + planColumnHeader: .text(L10n.PlanName.plus), + headerStroke: LinearGradient.highlight, + items: Self.mailPlusComparisonItems + ) + case .unlimited: + .init( + planColumnHeader: .icon(Image(DS.Icon.icInfinityUpsellHeader)), + headerStroke: nil, + items: Self.unlimitedComparisonItems + ) + } + } + + private static let mailPlusComparisonItems: [ComparisonItem] = [ + .init(title: \.storage, type: .string(free: gigabytesString(1), plan: gigabytesString(15))), + .init(title: \.emailAddresses, type: .string(free: "1", plan: "10")), + .init(title: \.customEmailDomain, type: .boolean), + .init(title: \.accessToDesktopApp, type: .boolean), + .init(title: \.unlimitedFoldersAndLabels, type: .boolean), + .init(title: \.priorityCustomerSupport, type: .boolean), + ] + + private static let unlimitedComparisonItems: [ComparisonItem] = [ + .init(title: \.storage, type: .string(free: gigabytesString(1), plan: gigabytesString(500))), + .init(title: \.hideMyEmailAliases, type: .stringAndIcon(free: "10", plan: Image(DS.Icon.icInfinityUpsellRow))), + .init(title: \.foldersAndLabels, type: .stringAndIcon(free: "3", plan: Image(DS.Icon.icInfinityUpsellRow))), + .init(title: \.premiumVpnPasswordManagerCloudStorage, type: .boolean), + .init(title: \.darkWebMonitoring, type: .boolean), + .init(title: \.customEmailDomain, type: .boolean), + .init(title: \.accessToDesktopApp, type: .boolean), + .init(title: \.priorityCustomerSupport, type: .boolean), + ] + + private static func gigabytesString(_ value: Double) -> String { + Measurement(value: value, unit: .gigabytes).formatted() + } +} diff --git a/Modules/InboxIAP/Sources/UpsellScreen/UpsellScreenFactory.swift b/Modules/InboxIAP/Sources/UpsellScreen/UpsellScreenFactory.swift index e0d28ca04c..c85f0fa0a7 100644 --- a/Modules/InboxIAP/Sources/UpsellScreen/UpsellScreenFactory.swift +++ b/Modules/InboxIAP/Sources/UpsellScreen/UpsellScreenFactory.swift @@ -65,13 +65,25 @@ final class UpsellScreenFactory { .init( storeKitProductId: composedPlan.storeKitProductID ?? "", cycleInMonths: composedPlan.instance.cycle, - pricing: .regular(monthlyPrice: composedPlan.pricePerMonthLabel), + pricing: pricing(for: composedPlan), discount: composedPlan.discount(comparedTo: mostExpensiveInstance) ) } } } + private func pricing(for composedPlan: ComposedPlan) -> DisplayablePlanInstance.Pricing { + if composedPlan.instance.cycle > 1 { + .discountedYearlyPlan( + discountedMonthlyPrice: composedPlan.pricePerMonthLabel, + discountedYearlyPrice: composedPlan.product.displayPrice, + renewalPrice: composedPlan.product.displayPrice + ) + } else { + .regular(monthlyPrice: composedPlan.pricePerMonthLabel) + } + } + @MainActor func onboardingUpsellScreenModel( showingPlans planNames: [String], diff --git a/Modules/InboxIAP/Tests/TelemetryReporterTests.swift b/Modules/InboxIAP/Tests/TelemetryReporterTests.swift index 0e01f13c1c..dbdd26aa3a 100644 --- a/Modules/InboxIAP/Tests/TelemetryReporterTests.swift +++ b/Modules/InboxIAP/Tests/TelemetryReporterTests.swift @@ -31,7 +31,8 @@ final class TelemetryReporterTests { .init( upsellEntryPoint: entryPoint, planBeforeUpgrade: "free", - modalVariant: .comparisonPlus + modalVariant: .comparisonPlus, + upsellFeatureFlags: upsellFeatureFlagsForIos() ) } @@ -44,7 +45,7 @@ final class TelemetryReporterTests { } init() { - sut = .init(mailUserSession: .init(noHandle: .init()), telemetryActions: telemetryActions) + sut = .init(mailUserSession: .init(noPointer: .init()), telemetryActions: telemetryActions) sut.prepare(entryPoint: entryPoint) } diff --git a/Modules/InboxIAP/Tests/UpsellCoordinatorTests.swift b/Modules/InboxIAP/Tests/UpsellCoordinatorTests.swift index fb3213c1a6..dbc8385dd8 100644 --- a/Modules/InboxIAP/Tests/UpsellCoordinatorTests.swift +++ b/Modules/InboxIAP/Tests/UpsellCoordinatorTests.swift @@ -69,4 +69,16 @@ final class UpsellCoordinatorTests { _ = try await self.sut.presentUpsellScreen(entryPoint: .mailboxTopBar) #expect(telemetryReporting.upsellButtonTappedCalls == 1) } + + @Test + func unlimitedUpsellTypeFetchesUnlimitedPlan() async throws { + let model = try await sut.presentUpsellScreen(entryPoint: .mailboxTopBar, upsellType: .unlimited) + #expect(model.planName == "Proton Unlimited") + } + + @Test + func mailPlusUpsellTypeFetchesMailPlusPlan() async throws { + let model = try await sut.presentUpsellScreen(entryPoint: .mailboxTopBar, upsellType: .mailPlus) + #expect(model.planName == "Mail Plus") + } } diff --git a/Modules/InboxIAP/Tests/UpsellScreen/Regular/UpsellScreenModelTests.swift b/Modules/InboxIAP/Tests/UpsellScreen/Regular/UpsellScreenModelTests.swift index 0c12ca080d..a4c2678891 100644 --- a/Modules/InboxIAP/Tests/UpsellScreen/Regular/UpsellScreenModelTests.swift +++ b/Modules/InboxIAP/Tests/UpsellScreen/Regular/UpsellScreenModelTests.swift @@ -57,6 +57,67 @@ final class UpsellScreenModelTests { #expect(planPurchasing.purchaseInvocations.count == 1) } + @Test + func mailPlusLogoComesFromEntryPoint() { + let sut = makeSUT(upsellType: .mailPlus) + #expect(sut.logo == UpsellEntryPoint.mailboxTopBar.logo) + } + + @Test + func unlimitedLogoComesFromUpsellType() { + let sut = makeSUT(upsellType: .unlimited) + #expect(sut.logo == UpsellType.unlimited.logo) + } + + @Test + func mailPlusComparisonConfigurationHasSixItems() { + let sut = makeSUT(upsellType: .mailPlus) + #expect(sut.comparisonConfiguration.items.count == 6) + } + + @Test + func unlimitedComparisonConfigurationHasEightItems() { + let sut = makeSUT(upsellType: .unlimited) + #expect(sut.comparisonConfiguration.items.count == 8) + } + + @Test + func autoRenewalNoticeShowsYearlyTotalWhenYearlyPlanSelected() throws { + let sut = makeSUT() + let yearlyInstance = try #require(DisplayablePlanInstance.previews.first { $0.cycleInMonths == 12 }) + + sut.selectedInstanceId = yearlyInstance.storeKitProductId + + #expect(sut.autoRenewalNotice.string == "Auto-renews at $47.88/year unless canceled") + } + + @Test + func autoRenewalNoticeShowsMonthlyPriceWhenMonthlyPlanSelected() throws { + let sut = makeSUT() + let monthlyInstance = try #require(DisplayablePlanInstance.previews.first { $0.cycleInMonths == 1 }) + + sut.selectedInstanceId = monthlyInstance.storeKitProductId + + #expect(sut.autoRenewalNotice.string == "Auto-renews at $4.99/month unless canceled") + } + + @Test + func autoRenewalNoticeUpdatesWhenSelectionChanges() throws { + let sut = makeSUT() + let yearlyId = try #require(DisplayablePlanInstance.previews.first { $0.cycleInMonths == 12 }).storeKitProductId + let monthlyId = try #require(DisplayablePlanInstance.previews.first { $0.cycleInMonths == 1 }).storeKitProductId + + sut.selectedInstanceId = yearlyId + let yearlyNotice = sut.autoRenewalNotice.string + + sut.selectedInstanceId = monthlyId + let monthlyNotice = sut.autoRenewalNotice.string + + #expect(yearlyNotice.contains("/year")) + #expect(monthlyNotice.contains("/month")) + #expect(yearlyNotice != monthlyNotice) + } + private func makeSUT(upsellType: UpsellType = .mailPlus) -> UpsellScreenModel { .init( planName: "foo", diff --git a/Modules/InboxIAP/Tests/UpsellScreenFactoryTests.swift b/Modules/InboxIAP/Tests/UpsellScreenFactoryTests.swift index 25d11bdefc..21ff0aeecf 100644 --- a/Modules/InboxIAP/Tests/UpsellScreenFactoryTests.swift +++ b/Modules/InboxIAP/Tests/UpsellScreenFactoryTests.swift @@ -33,7 +33,7 @@ final class UpsellScreenFactoryTests { @Test func upsellScreenModelGeneration() throws { let upsellScreenModel = try sut.upsellScreenModel( - showingPlan: configuration.regularPlan, + showingPlan: SubscriptionPlanVariant.plus, basedOn: availablePlans, entryPoint: entryPoint, upsellType: .mailPlus @@ -43,6 +43,68 @@ final class UpsellScreenFactoryTests { #expect(upsellScreenModel.planInstances == DisplayablePlanInstance.previews) } + @Test + func yearlyMailPlusInstanceCarriesYearlyTotalAndMonthlyEquivalent() throws { + let upsellScreenModel = try sut.upsellScreenModel( + showingPlan: SubscriptionPlanVariant.plus, + basedOn: availablePlans, + entryPoint: entryPoint, + upsellType: .mailPlus + ) + + let yearlyInstance = try #require(upsellScreenModel.planInstances.first { $0.cycleInMonths == 12 }) + + #expect(yearlyInstance.pricing == .discountedYearlyPlan( + discountedMonthlyPrice: "$3.99", + discountedYearlyPrice: "$47.88", + renewalPrice: "$47.88" + )) + } + + @Test + func monthlyMailPlusInstanceUsesRegularPricing() throws { + let upsellScreenModel = try sut.upsellScreenModel( + showingPlan: SubscriptionPlanVariant.plus, + basedOn: availablePlans, + entryPoint: entryPoint, + upsellType: .mailPlus + ) + + let monthlyInstance = try #require(upsellScreenModel.planInstances.first { $0.cycleInMonths == 1 }) + + #expect(monthlyInstance.pricing == .regular(monthlyPrice: "$4.99")) + } + + @Test + func unlimitedUpsellScreenModelGeneration() throws { + let upsellScreenModel = try sut.upsellScreenModel( + showingPlan: SubscriptionPlanVariant.unlimited, + basedOn: availablePlans, + entryPoint: entryPoint, + upsellType: .unlimited + ) + + #expect(upsellScreenModel.planName == "Proton Unlimited") + } + + @Test + func yearlyUnlimitedInstanceCarriesYearlyTotalAndMonthlyEquivalent() throws { + let upsellScreenModel = try sut.upsellScreenModel( + showingPlan: SubscriptionPlanVariant.unlimited, + basedOn: availablePlans, + entryPoint: entryPoint, + upsellType: .unlimited + ) + + let yearlyInstance = try #require(upsellScreenModel.planInstances.first { $0.cycleInMonths == 12 }) + + #expect(yearlyInstance.pricing == .discountedYearlyPlan( + discountedMonthlyPrice: "$9.99", + discountedYearlyPrice: "$119.88", + renewalPrice: "$119.88" + )) + } + @Test func onboardingUpsellScreenModelGeneration() throws { let upsellScreenModel = try sut.onboardingUpsellScreenModel( diff --git a/Modules/InboxRSVP/Tests/Unit/RSVPStateStoreTests.swift b/Modules/InboxRSVP/Tests/Unit/RSVPStateStoreTests.swift index a8c3ad174d..4a778be057 100644 --- a/Modules/InboxRSVP/Tests/Unit/RSVPStateStoreTests.swift +++ b/Modules/InboxRSVP/Tests/Unit/RSVPStateStoreTests.swift @@ -30,9 +30,9 @@ import proton_app_uniffi @MainActor final class RSVPStateStoreTests { private let pasteboard = UIPasteboard.testInstance - private let serviceSpy = RsvpEventServiceSpy(noHandle: .init()) + private let serviceSpy = RsvpEventServiceSpy(noPointer: .init()) private let openURLSpy = EnvironmentURLOpenerSpy() - private var serviceProviderSpy = RsvpEventServiceProviderSpy(noHandle: .init()) + private var serviceProviderSpy = RsvpEventServiceProviderSpy(noPointer: .init()) private let toastStateStore = ToastStateStore(initialState: .initial) private let draftPresenterSpy = RecipientDraftPresenterSpy() private(set) lazy var sut = RSVPStateStore( diff --git a/Modules/InboxTesting/Sources/MailSessionSpy.swift b/Modules/InboxTesting/Sources/MailSessionSpy.swift index 7e62035921..3c92571197 100644 --- a/Modules/InboxTesting/Sources/MailSessionSpy.swift +++ b/Modules/InboxTesting/Sources/MailSessionSpy.swift @@ -316,7 +316,7 @@ public final class MailSessionSpy: MailSessionProtocol { public func watchSessionsAsync(callback: any AsyncLiveQueryCallback) async -> MailSessionWatchSessionsAsyncResult { watchSessionsAsyncCallback = callback - return .ok(.init(sessions: storedSessions, handle: WatchHandleDummy(noHandle: .init()))) + return .ok(.init(sessions: storedSessions, handle: WatchHandleDummy(noPointer: .init()))) } public func isFeatureEnabled(featureId: String) async -> MailSessionIsFeatureEnabledResult { diff --git a/Modules/InboxTesting/Sources/MailUserSessionSpy.swift b/Modules/InboxTesting/Sources/MailUserSessionSpy.swift index 7f4efd9bef..32f5071342 100644 --- a/Modules/InboxTesting/Sources/MailUserSessionSpy.swift +++ b/Modules/InboxTesting/Sources/MailUserSessionSpy.swift @@ -30,12 +30,12 @@ public final class MailUserSessionSpy: MailUserSession, @unchecked Sendable { public init(id: String) { self.id = id - super.init(noHandle: .init()) + super.init(noPointer: .init()) } @available(*, unavailable) - required init(unsafeFromHandle handle: UInt64) { - fatalError("init(unsafeFromHandle:) has not been implemented") + required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + fatalError("init(unsafeFromRawPointer:) has not been implemented") } public override func accountDetails() async -> MailUserSessionAccountDetailsResult { @@ -87,6 +87,6 @@ public final class MailUserSessionSpy: MailUserSession, @unchecked Sendable { public override func watchUserSettings(callback: any AsyncLiveQueryCallback) -> MailUserSessionWatchUserSettingsResult { watchUserSettingsCallback.append(callback) - return .ok(.init(noHandle: .init())) + return .ok(.init(noPointer: .init())) } } diff --git a/Modules/InboxTesting/Sources/PasswordFlowStub.swift b/Modules/InboxTesting/Sources/PasswordFlowStub.swift index a013984745..1176275ce1 100644 --- a/Modules/InboxTesting/Sources/PasswordFlowStub.swift +++ b/Modules/InboxTesting/Sources/PasswordFlowStub.swift @@ -19,12 +19,12 @@ import proton_app_uniffi final class PasswordFlowStub: PasswordFlow, @unchecked Sendable { init() { - super.init(noHandle: .init()) + super.init(noPointer: .init()) } @available(*, unavailable) - required init(unsafeFromHandle handle: UInt64) { - fatalError("init(unsafeFromHandle:) has not been implemented") + required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + fatalError("init(unsafeFromRawPointer:) has not been implemented") } override func hasMbp() -> PasswordFlowHasMbpResult { diff --git a/Modules/InboxTesting/Sources/StoredSessionStub.swift b/Modules/InboxTesting/Sources/StoredSessionStub.swift index e684ed6ffd..87d8a8b1d2 100644 --- a/Modules/InboxTesting/Sources/StoredSessionStub.swift +++ b/Modules/InboxTesting/Sources/StoredSessionStub.swift @@ -27,14 +27,14 @@ public final class StoredSessionStub: StoredSession, @unchecked Sendable { userIdValue = userId stateValue = state - super.init(noHandle: .init()) + super.init(noPointer: .init()) } @available(*, unavailable) - required init(unsafeFromHandle handle: UInt64) { - fatalError("init(unsafeFromHandle:) has not been implemented") + required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + fatalError("init(unsafeFromRawPointer:) has not been implemented") } - + public override func sessionId() -> String { id } diff --git a/mail-sdk-version b/mail-sdk-version index b9e8417dc8..0099ed8717 100644 --- a/mail-sdk-version +++ b/mail-sdk-version @@ -1 +1 @@ -0.164.5 +0.165.2 diff --git a/project.yml b/project.yml index cc6849261b..f823a7f20f 100644 --- a/project.yml +++ b/project.yml @@ -92,7 +92,7 @@ settings: CODE_SIGN_STYLE: Manual CURRENT_PROJECT_VERSION: 1 DEVELOPMENT_TEAM: 2SB5Z68H26 - MARKETING_VERSION: 7.9.0 + MARKETING_VERSION: 7.10.0 SWIFT_STRICT_CONCURRENCY: complete configs: Debug: