diff --git a/BookPlayer.xcodeproj/project.pbxproj b/BookPlayer.xcodeproj/project.pbxproj index 435c918bd..70b88b704 100644 --- a/BookPlayer.xcodeproj/project.pbxproj +++ b/BookPlayer.xcodeproj/project.pbxproj @@ -331,6 +331,10 @@ 6309F1272B0CF658002B86A4 /* BookPlaybackToggleIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6309F1252B0CF1C1002B86A4 /* BookPlaybackToggleIntent.swift */; }; 630F115E2AE7EEBA000A997A /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 419B375B23B8D6DB00128A8F /* Localizable.strings */; }; 630F115F2AE7EECA000A997A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4140EA34227288EF0009F794 /* Assets.xcassets */; }; + 63125D0E2C36D84E00D35533 /* EventsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63125D0D2C36D84E00D35533 /* EventsAPI.swift */; }; + 63125D0F2C36D84E00D35533 /* EventsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63125D0D2C36D84E00D35533 /* EventsAPI.swift */; }; + 63125D122C36D97400D35533 /* EventsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63125D112C36D97400D35533 /* EventsService.swift */; }; + 63125D132C36D97400D35533 /* EventsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63125D112C36D97400D35533 /* EventsService.swift */; }; 631B360C2ABE8ACC001F4C1C /* BPModalOnlyPresentationFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 631B360B2ABE8ACC001F4C1C /* BPModalOnlyPresentationFlow.swift */; }; 631C75C72AB92BE20013E7E5 /* BPCoordinatorPresentationFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 631C75C62AB92BE20013E7E5 /* BPCoordinatorPresentationFlow.swift */; }; 631C75C92AB92C540013E7E5 /* BPPushPresentationFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 631C75C82AB92C540013E7E5 /* BPPushPresentationFlow.swift */; }; @@ -346,6 +350,17 @@ 634BA5462C0BD0190015314D /* pride@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 634BA5442C0BD0180015314D /* pride@3x.png */; }; 634BA5492C0BD06B0015314D /* pride-ipad@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 634BA5472C0BD06B0015314D /* pride-ipad@3x.png */; }; 634BA54A2C0BD06B0015314D /* pride-ipad@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 634BA5482C0BD06B0015314D /* pride-ipad@2x.png */; }; + 634BA54C2C0C21AF0015314D /* SecondOnboardingCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 634BA54B2C0C21AF0015314D /* SecondOnboardingCoordinator.swift */; }; + 634BA58D2C14D5330015314D /* SecondOnboardingType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 634BA58C2C14D5330015314D /* SecondOnboardingType.swift */; }; + 634BA5902C14FB730015314D /* StoryViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 634BA58F2C14FB730015314D /* StoryViewer.swift */; }; + 634BA5932C160B890015314D /* LoadingBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 634BA5922C160B890015314D /* LoadingBar.swift */; }; + 634BA5952C1611230015314D /* StoryProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 634BA5942C1611230015314D /* StoryProgress.swift */; }; + 634BA5972C161FBE0015314D /* StoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 634BA5962C161FBE0015314D /* StoryView.swift */; }; + 634BA5A12C174F9D0015314D /* StoryViewerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 634BA5A02C174F9D0015314D /* StoryViewerViewModel.swift */; }; + 634BA5A32C17661D0015314D /* StoryBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 634BA5A22C17661C0015314D /* StoryBackgroundView.swift */; }; + 634BA5A52C176B5A0015314D /* StoryActionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 634BA5A42C176B5A0015314D /* StoryActionView.swift */; }; + 634BA5A72C1777BB0015314D /* PricingBoxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 634BA5A62C1777BA0015314D /* PricingBoxView.swift */; }; + 634BA5AD2C180F5E0015314D /* StoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 634BA5AC2C180F5E0015314D /* StoryViewModel.swift */; }; 634E67462AFC7DEF00595BAC /* BookStartPlaybackIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 634E67432AFB2DF500595BAC /* BookStartPlaybackIntent.swift */; }; 6354CD9C2B4902CE006D9551 /* DebugInformationActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6354CD9B2B4902CE006D9551 /* DebugInformationActivityItemSource.swift */; }; 6356F9B52AC7CC5600B7A027 /* CancelSleepTimerIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6356F9B42AC7CC5600B7A027 /* CancelSleepTimerIntent.swift */; }; @@ -388,7 +403,12 @@ 63B230452B8CCF1800AEECED /* SyncJobType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63B230442B8CCF1800AEECED /* SyncJobType.swift */; }; 63B230462B8CCF1800AEECED /* SyncJobType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63B230442B8CCF1800AEECED /* SyncJobType.swift */; }; 63B3B6902B1F625B007A367C /* StorageCloudDeletedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63B3B68F2B1F625B007A367C /* StorageCloudDeletedView.swift */; }; + 63B407432C1B0CC1000A3B19 /* StorySkipControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63B407422C1B0CC1000A3B19 /* StorySkipControlsView.swift */; }; 63B50F052B692E4200BCABBA /* ListSyncRefreshService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63B50F042B692E4200BCABBA /* ListSyncRefreshService.swift */; }; + 63B760E82C31FFC600AA98C7 /* SupportFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63B760E72C31FFC600AA98C7 /* SupportFlowCoordinator.swift */; }; + 63B760F72C32734000AA98C7 /* SecondOnboardingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63B760F62C32734000AA98C7 /* SecondOnboardingResponse.swift */; }; + 63B760F92C32738E00AA98C7 /* StoryAccountSubscriptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63B760F82C32738E00AA98C7 /* StoryAccountSubscriptionService.swift */; }; + 63B760FC2C33B77F00AA98C7 /* SupportProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63B760FB2C33B77F00AA98C7 /* SupportProfileView.swift */; }; 63C1A8AF2B09158600C4B418 /* BookStartPlaybackIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 634E67432AFB2DF500595BAC /* BookStartPlaybackIntent.swift */; }; 63C1A8B02B0915EE00C4B418 /* WidgetUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 418445C2258AE11E0072DD13 /* WidgetUtils.swift */; }; 63C1A8B12B09165400C4B418 /* RecentBooksWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41ADD6D92570AC6300660C64 /* RecentBooksWidgetView.swift */; }; @@ -1070,6 +1090,8 @@ 6308260F2AF6C9B0002ACE0D /* SharedIconWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedIconWidget.swift; sourceTree = ""; }; 630826132AF6CA81002ACE0D /* SharedIconWidgetEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedIconWidgetEntry.swift; sourceTree = ""; }; 6309F1252B0CF1C1002B86A4 /* BookPlaybackToggleIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookPlaybackToggleIntent.swift; sourceTree = ""; }; + 63125D0D2C36D84E00D35533 /* EventsAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsAPI.swift; sourceTree = ""; }; + 63125D112C36D97400D35533 /* EventsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsService.swift; sourceTree = ""; }; 631B360B2ABE8ACC001F4C1C /* BPModalOnlyPresentationFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BPModalOnlyPresentationFlow.swift; sourceTree = ""; }; 631C75C62AB92BE20013E7E5 /* BPCoordinatorPresentationFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BPCoordinatorPresentationFlow.swift; sourceTree = ""; }; 631C75C82AB92C540013E7E5 /* BPPushPresentationFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BPPushPresentationFlow.swift; sourceTree = ""; }; @@ -1084,6 +1106,17 @@ 634BA5442C0BD0180015314D /* pride@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "pride@3x.png"; sourceTree = ""; }; 634BA5472C0BD06B0015314D /* pride-ipad@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "pride-ipad@3x.png"; sourceTree = ""; }; 634BA5482C0BD06B0015314D /* pride-ipad@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "pride-ipad@2x.png"; sourceTree = ""; }; + 634BA54B2C0C21AF0015314D /* SecondOnboardingCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondOnboardingCoordinator.swift; sourceTree = ""; }; + 634BA58C2C14D5330015314D /* SecondOnboardingType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondOnboardingType.swift; sourceTree = ""; }; + 634BA58F2C14FB730015314D /* StoryViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryViewer.swift; sourceTree = ""; }; + 634BA5922C160B890015314D /* LoadingBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingBar.swift; sourceTree = ""; }; + 634BA5942C1611230015314D /* StoryProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryProgress.swift; sourceTree = ""; }; + 634BA5962C161FBE0015314D /* StoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryView.swift; sourceTree = ""; }; + 634BA5A02C174F9D0015314D /* StoryViewerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryViewerViewModel.swift; sourceTree = ""; }; + 634BA5A22C17661C0015314D /* StoryBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryBackgroundView.swift; sourceTree = ""; }; + 634BA5A42C176B5A0015314D /* StoryActionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryActionView.swift; sourceTree = ""; }; + 634BA5A62C1777BA0015314D /* PricingBoxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PricingBoxView.swift; sourceTree = ""; }; + 634BA5AC2C180F5E0015314D /* StoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryViewModel.swift; sourceTree = ""; }; 634E67432AFB2DF500595BAC /* BookStartPlaybackIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookStartPlaybackIntent.swift; sourceTree = ""; }; 6354CD9B2B4902CE006D9551 /* DebugInformationActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugInformationActivityItemSource.swift; sourceTree = ""; }; 6356F9B42AC7CC5600B7A027 /* CancelSleepTimerIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelSleepTimerIntent.swift; sourceTree = ""; }; @@ -1131,7 +1164,12 @@ 63B230412B8CCE8600AEECED /* Realm+BookPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Realm+BookPlayer.swift"; sourceTree = ""; }; 63B230442B8CCF1800AEECED /* SyncJobType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncJobType.swift; sourceTree = ""; }; 63B3B68F2B1F625B007A367C /* StorageCloudDeletedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageCloudDeletedView.swift; sourceTree = ""; }; + 63B407422C1B0CC1000A3B19 /* StorySkipControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorySkipControlsView.swift; sourceTree = ""; }; 63B50F042B692E4200BCABBA /* ListSyncRefreshService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListSyncRefreshService.swift; sourceTree = ""; }; + 63B760E72C31FFC600AA98C7 /* SupportFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportFlowCoordinator.swift; sourceTree = ""; }; + 63B760F62C32734000AA98C7 /* SecondOnboardingResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondOnboardingResponse.swift; sourceTree = ""; }; + 63B760F82C32738E00AA98C7 /* StoryAccountSubscriptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryAccountSubscriptionService.swift; sourceTree = ""; }; + 63B760FB2C33B77F00AA98C7 /* SupportProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportProfileView.swift; sourceTree = ""; }; 63C6C2E52B5029BC00FFE0D8 /* SettingsAutolockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAutolockView.swift; sourceTree = ""; }; 63C6C2E72B5029FE00FFE0D8 /* SettingsAutolockViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAutolockViewModel.swift; sourceTree = ""; }; 63C6C30B2B538B7A00FFE0D8 /* SyncTasksStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncTasksStorage.swift; sourceTree = ""; }; @@ -1649,6 +1687,7 @@ 418B6CFA1D2707F800F974FB /* BookPlayer */ = { isa = PBXGroup; children = ( + 634BA58B2C14D5100015314D /* SecondOnboarding */, 4151A6DE26E4A13E00E49DBE /* Coordinators */, 69343D3121338440000C425E /* Services */, C3A479112094C8A800D92122 /* Utils */, @@ -2049,6 +2088,7 @@ 62AAE22B274AA3EB001EB9FF /* LibraryService.swift */, 9FDDD2E0289BFCE20020C428 /* LibraryService+Sync.swift */, 41EB071A2752FA6B00EFEE13 /* PlaybackService.swift */, + 63125D102C36D96800D35533 /* Events */, 9FC1E4612814F68F00522FA8 /* Account */, 9FD8FE4C286566FF00EB2C3D /* Sync */, ); @@ -2095,6 +2135,15 @@ path = SharedIconWidget; sourceTree = ""; }; + 63125D102C36D96800D35533 /* Events */ = { + isa = PBXGroup; + children = ( + 63125D0D2C36D84E00D35533 /* EventsAPI.swift */, + 63125D112C36D97400D35533 /* EventsService.swift */, + ); + path = Events; + sourceTree = ""; + }; 631C75CA2AB92C5C0013E7E5 /* PresentationFlow */ = { isa = PBXGroup; children = ( @@ -2106,6 +2155,44 @@ path = PresentationFlow; sourceTree = ""; }; + 634BA58B2C14D5100015314D /* SecondOnboarding */ = { + isa = PBXGroup; + children = ( + 634BA58E2C14FB010015314D /* Support */, + 634BA58C2C14D5330015314D /* SecondOnboardingType.swift */, + 63B760F62C32734000AA98C7 /* SecondOnboardingResponse.swift */, + 63B760F82C32738E00AA98C7 /* StoryAccountSubscriptionService.swift */, + 634BA54B2C0C21AF0015314D /* SecondOnboardingCoordinator.swift */, + ); + path = SecondOnboarding; + sourceTree = ""; + }; + 634BA58E2C14FB010015314D /* Support */ = { + isa = PBXGroup; + children = ( + 63B760E72C31FFC600AA98C7 /* SupportFlowCoordinator.swift */, + 63B760FB2C33B77F00AA98C7 /* SupportProfileView.swift */, + ); + path = Support; + sourceTree = ""; + }; + 634BA5912C160B720015314D /* StoryViewer */ = { + isa = PBXGroup; + children = ( + 634BA5A02C174F9D0015314D /* StoryViewerViewModel.swift */, + 634BA5AC2C180F5E0015314D /* StoryViewModel.swift */, + 634BA58F2C14FB730015314D /* StoryViewer.swift */, + 634BA5A22C17661C0015314D /* StoryBackgroundView.swift */, + 634BA5962C161FBE0015314D /* StoryView.swift */, + 63B407422C1B0CC1000A3B19 /* StorySkipControlsView.swift */, + 634BA5A42C176B5A0015314D /* StoryActionView.swift */, + 634BA5A62C1777BA0015314D /* PricingBoxView.swift */, + 634BA5922C160B890015314D /* LoadingBar.swift */, + 634BA5942C1611230015314D /* StoryProgress.swift */, + ); + path = StoryViewer; + sourceTree = ""; + }; 6356F9C82AC89F1600B7A027 /* AppIntents */ = { isa = PBXGroup; children = ( @@ -2585,6 +2672,7 @@ 9F1804B727A4AEC500FEDFE5 /* AccessibleSliderView.swift */, 9F22DE3F288D8BC800056FCD /* BaseLabel.swift */, 9FB20EB629A423410021663B /* InterfaceUpdater.swift */, + 634BA5912C160B720015314D /* StoryViewer */, ); path = Views; sourceTree = ""; @@ -3233,6 +3321,7 @@ 41A90C4927564DAA00C30394 /* BookPlayerError.swift in Sources */, 41C23402272E1960006BC7B8 /* SimpleTheme.swift in Sources */, 638E64CF2B8E1CFD00DCFA3B /* SyncTasksCountService.swift in Sources */, + 63125D132C36D97400D35533 /* EventsService.swift in Sources */, 4140EA7D227289CB0009F794 /* LibraryItem+CoreDataProperties.swift in Sources */, 63B230462B8CCF1800AEECED /* SyncJobType.swift in Sources */, 4140EA73227289A80009F794 /* Notification+BookPlayerKit.swift in Sources */, @@ -3265,6 +3354,7 @@ 4140EA7A227289C20009F794 /* Book+CoreDataClass.swift in Sources */, 9F49072D2903663800054AD6 /* SortType.swift in Sources */, 6357F11A2A8BA084007947FC /* BPURLSession.swift in Sources */, + 63125D0F2C36D84E00D35533 /* EventsAPI.swift in Sources */, 9FBDBC7E287940D900D315A2 /* ContentsResponse.swift in Sources */, 63B2303E2B8CCDFD00AEECED /* MigrationStoredSyncTasks.swift in Sources */, 4140EA7C227289C70009F794 /* LibraryItem+CoreDataClass.swift in Sources */, @@ -3349,12 +3439,14 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 634BA58D2C14D5330015314D /* SecondOnboardingType.swift in Sources */, 9F82DF9C27DFE46B001B0EA8 /* PhoneWatchConnectivityService.swift in Sources */, 9F00A600295001C0005EA316 /* ItemDetailsArtworkSectionView.swift in Sources */, 410D0FF11EDF659900A52EB9 /* PlayerManager.swift in Sources */, 41AD3DAF221E678600DC41E1 /* PlusViewController.swift in Sources */, 9F4691F02800C97000A8F0E8 /* CompleteAccountViewModel.swift in Sources */, 9F2681AD2888B26100359BD3 /* LoginBenefitView.swift in Sources */, + 634BA5A72C1777BB0015314D /* PricingBoxView.swift in Sources */, 9F588DBF2902C798000DA799 /* ComposedButton.swift in Sources */, 41B2A5F121CD857800917584 /* AddCellView.swift in Sources */, 4151A6B326E491A800E49DBE /* NibLoadableView.swift in Sources */, @@ -3390,6 +3482,7 @@ 63C1A8B02B0915EE00C4B418 /* WidgetUtils.swift in Sources */, 41B2A5ED21CCC6D100917584 /* AppNavigationController.swift in Sources */, 9F22DE40288D8BC800056FCD /* BaseLabel.swift in Sources */, + 63B407432C1B0CC1000A3B19 /* StorySkipControlsView.swift in Sources */, 41B2AC8E1D43CCE8005382A9 /* ChaptersViewController.swift in Sources */, 9FBDDB8927DC454B005FB447 /* AppTabBarController.swift in Sources */, 9F4A0F782839574B00F9C959 /* PlayerSettingsViewModel.swift in Sources */, @@ -3410,6 +3503,7 @@ C375AF1920DD99E000AC034D /* ArtworkControl.swift in Sources */, 41544A1421BAF41400740AD2 /* ItemSelectionViewController.swift in Sources */, 9F5011F92A6580800075FEBA /* ShakeMotionService.swift in Sources */, + 634BA5932C160B890015314D /* LoadingBar.swift in Sources */, 9F64C6242793C3DA00B2493C /* PlayerControlsViewModel.swift in Sources */, 6356F9B52AC7CC5600B7A027 /* CancelSleepTimerIntent.swift in Sources */, 6356F9C12AC823EE00B7A027 /* LastBookStartPlaybackIntent.swift in Sources */, @@ -3420,16 +3514,20 @@ 41188D2826ED4D5C0017124E /* ItemListViewController.swift in Sources */, 41188D2426ED2BA30017124E /* LoadingCoordinator.swift in Sources */, 4151A6DD26E4A13A00E49DBE /* MainCoordinator.swift in Sources */, + 634BA54C2C0C21AF0015314D /* SecondOnboardingCoordinator.swift in Sources */, 63C6C2E62B5029BC00FFE0D8 /* SettingsAutolockView.swift in Sources */, 41964C832220B4F700FF1A2F /* ContributorCellView.swift in Sources */, 4124122826D19A8700B099DB /* StorageViewModel.swift in Sources */, 4158387926EB8D8800F4A12B /* LoadingViewController.swift in Sources */, 6356F9D02ACB01C700B7A027 /* PausePlaybackIntent.swift in Sources */, + 634BA5952C1611230015314D /* StoryProgress.swift in Sources */, 4151A6E026E4A17900E49DBE /* Coordinator.swift in Sources */, 9FF710BB2A215558006490E0 /* QueuedSyncTasksViewModel.swift in Sources */, 418EA78B268D6EF100F6BAEB /* ImportTableViewCell.swift in Sources */, 9F2DC9D92A008B19006CDF1F /* PricingOptionsView.swift in Sources */, + 634BA5902C14FB730015314D /* StoryViewer.swift in Sources */, 41188D2026ECDDD50017124E /* BookmarkCoordinator.swift in Sources */, + 634BA5A12C174F9D0015314D /* StoryViewerViewModel.swift in Sources */, 4142964921F2E2BA004356DA /* ThemeCellView.swift in Sources */, 9F4691F72800F85600A8F0E8 /* AccountViewModel.swift in Sources */, 4160A0A123F304530039166B /* LocalizableLabel.swift in Sources */, @@ -3441,6 +3539,7 @@ D6BA8F162A4CA94800C2BD9A /* StorageRowView.swift in Sources */, 9F3C436A284181690066D99A /* DataInitializerCoordinator.swift in Sources */, 9F3C436B284181C70066D99A /* AlertPresenter.swift in Sources */, + 63B760FC2C33B77F00AA98C7 /* SupportProfileView.swift in Sources */, 9F00A6212950F44B005EA316 /* ImagePicker.swift in Sources */, 9F00A6242951F2F3005EA316 /* ItemDetailsFormViewModel.swift in Sources */, 41DA44BD26FAEC4F00F3A05D /* BookActivityItemProvider.swift in Sources */, @@ -3448,6 +3547,7 @@ 4138CE1C26E5B42F0014F11E /* BookmarksViewModel.swift in Sources */, 9F3D0CE528C2BF5C00E9E8A3 /* ButtonFreeViewController.swift in Sources */, 4197240021874D5F00AB1190 /* UserActivityManager.swift in Sources */, + 634BA5972C161FBE0015314D /* StoryView.swift in Sources */, 41D4F2EF21053944009F1B1E /* IndexPath+BookPlayer.swift in Sources */, 6356F9C52AC86D9200B7A027 /* BPAppShortcuts.swift in Sources */, 69343D332133844D000C425E /* VoiceOverService.swift in Sources */, @@ -3465,6 +3565,7 @@ 418CABB125EF28FC00D8C878 /* MappingModel_v3_to_v4.xcmappingmodel in Sources */, 63C6C2E82B5029FE00FFE0D8 /* SettingsAutolockViewModel.swift in Sources */, 410D00FA26DDCE6C00D11A45 /* ChaptersViewModel.swift in Sources */, + 63B760F72C32734000AA98C7 /* SecondOnboardingResponse.swift in Sources */, 41640A3724416EE8004FB97B /* Intents.intentdefinition in Sources */, 41AD3DA1221C7CAB00DC41E1 /* Icon.swift in Sources */, 9F22DE3C288D7D4300056FCD /* AccountSectionContainerView.swift in Sources */, @@ -3480,6 +3581,7 @@ 9FEC87B027FA9F0F006C71D5 /* LoginViewController.swift in Sources */, 9F00A5FA294F8BFE005EA316 /* ClearableTextField.swift in Sources */, 9F5FBB08293EDCD8009F4B0E /* ItemDetailsViewController.swift in Sources */, + 634BA5AD2C180F5E0015314D /* StoryViewModel.swift in Sources */, 9F2681B628898A7300359BD3 /* LoginDisclaimerView.swift in Sources */, 63C1A8B12B09165400C4B418 /* RecentBooksWidgetView.swift in Sources */, 9FD8D95829DC53750074C2D8 /* CoreServices.swift in Sources */, @@ -3513,11 +3615,13 @@ 9F2DA27F27F0C68D00C8EF2B /* CarPlaySceneDelegate.swift in Sources */, 4193E202243A91AE004D6A82 /* ActionParserService.swift in Sources */, 41A359C3276232E00020D5F5 /* MappingModel_v7_to_v8.xcmappingmodel in Sources */, + 63B760F92C32738E00AA98C7 /* StoryAccountSubscriptionService.swift in Sources */, 9F5F12DB2976E8CD00F061A0 /* ProfileView.swift in Sources */, 41C8ABD126836F50003B67D1 /* ImportViewModel.swift in Sources */, 41188D1E26ECDAA30017124E /* MiniPlayerViewModel.swift in Sources */, 41B2A5DE21CAF20E00917584 /* ThemesViewController.swift in Sources */, 41D20DAF25D5F5A100AAEE30 /* MappingModel_v1_to_v2.xcmappingmodel in Sources */, + 634BA5A32C17661D0015314D /* StoryBackgroundView.swift in Sources */, 9FF383D12A40F97000BBAC11 /* MappingModel_v8_to_v9.xcmappingmodel in Sources */, 63B3B6902B1F625B007A367C /* StorageCloudDeletedView.swift in Sources */, 9F134605293D0A410089B1DE /* ThemeViewModel.swift in Sources */, @@ -3529,9 +3633,11 @@ 63B50F052B692E4200BCABBA /* ListSyncRefreshService.swift in Sources */, 416A297D2568671F00605395 /* AVPlayer+BookPlayer.swift in Sources */, 41E79BEB26C60DC600EA9FFF /* PlayerViewModel.swift in Sources */, + 634BA5A52C176B5A0015314D /* StoryActionView.swift in Sources */, 635907A02B161B14002FA524 /* DebugInformationFileActivityItemProvider.swift in Sources */, 9F5FBB0A293EE0C2009F4B0E /* ItemDetailsViewModel.swift in Sources */, 6309F1262B0CF1C1002B86A4 /* BookPlaybackToggleIntent.swift in Sources */, + 63B760E82C31FFC600AA98C7 /* SupportFlowCoordinator.swift in Sources */, 9FC1A29F28C0D8CC00F25906 /* BookmarksActivityItemProvider.swift in Sources */, D6BA8F182A4D66CD00C2BD9A /* StorageView.swift in Sources */, 9F64C6212793C31600B2493C /* PlayerControlsCoordinator.swift in Sources */, @@ -3588,6 +3694,7 @@ 4124AB2125DFE1A60007C839 /* CoreDataStack.swift in Sources */, 41A1B11F226F88C500EA0400 /* Chapter+CoreDataClass.swift in Sources */, 4138CE1626E584B60014F11E /* BookmarkType.swift in Sources */, + 63125D0E2C36D84E00D35533 /* EventsAPI.swift in Sources */, 412AB7092701421600969618 /* ManualOrderMigrationUtils.swift in Sources */, 9F1345B82938DF360089B1DE /* UIFont+BookPlayer.swift in Sources */, 639AC98C2AD9F2840053AFC6 /* BPTaskDownloadDelegate.swift in Sources */, @@ -3608,6 +3715,7 @@ 639E12DB2B8AC65B00C875F7 /* RealmMigrationManager.swift in Sources */, 41A1B124226F88C500EA0400 /* Book+CoreDataProperties.swift in Sources */, 41A1B108226E9DF800EA0400 /* DataManager.swift in Sources */, + 63125D122C36D97400D35533 /* EventsService.swift in Sources */, 41A1B131226FE33500EA0400 /* Constants.swift in Sources */, 4124AB1725DFE07E0007C839 /* DataMigrationManager.swift in Sources */, 9FDDD2DE289BEE590020C428 /* SyncJobScheduler.swift in Sources */, diff --git a/BookPlayer/AppDelegate.swift b/BookPlayer/AppDelegate.swift index f8327a1c3..2f8d8f445 100644 --- a/BookPlayer/AppDelegate.swift +++ b/BookPlayer/AppDelegate.swift @@ -130,7 +130,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, BPLogger { syncService = sharedSyncService } else { syncService = SyncService( - isActive: accountService.hasActiveSubscription(), + isActive: accountService.hasSyncEnabled(), libraryService: libraryService ) AppDelegate.shared?.syncService = syncService diff --git a/BookPlayer/Assets.xcassets/small-family-pic.imageset/Contents.json b/BookPlayer/Assets.xcassets/small-family-pic.imageset/Contents.json new file mode 100644 index 000000000..cce15b503 --- /dev/null +++ b/BookPlayer/Assets.xcassets/small-family-pic.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "small-family-picture.jpg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/BookPlayer/Assets.xcassets/small-family-pic.imageset/small-family-picture.jpg b/BookPlayer/Assets.xcassets/small-family-pic.imageset/small-family-picture.jpg new file mode 100644 index 000000000..ec35f1f58 Binary files /dev/null and b/BookPlayer/Assets.xcassets/small-family-pic.imageset/small-family-picture.jpg differ diff --git a/BookPlayer/Coordinators/LibraryListCoordinator.swift b/BookPlayer/Coordinators/LibraryListCoordinator.swift index 1a157ad2f..8f22401a3 100644 --- a/BookPlayer/Coordinators/LibraryListCoordinator.swift +++ b/BookPlayer/Coordinators/LibraryListCoordinator.swift @@ -19,9 +19,35 @@ class LibraryListCoordinator: ItemListCoordinator, UINavigationControllerDelegat weak var importCoordinator: ImportCoordinator? /// Reference to ongoing library fetch task var contentsFetchTask: Task<(), Error>? + /// Account service + let accountService: AccountServiceProtocol private var disposeBag = Set() + /// Initializer + init( + flow: BPCoordinatorPresentationFlow, + playerManager: PlayerManagerProtocol, + libraryService: LibraryServiceProtocol, + playbackService: PlaybackServiceProtocol, + syncService: SyncServiceProtocol, + importManager: ImportManager, + listRefreshService: ListSyncRefreshService, + accountService: AccountServiceProtocol + ) { + self.accountService = accountService + + super.init( + flow: flow, + playerManager: playerManager, + libraryService: libraryService, + playbackService: playbackService, + syncService: syncService, + importManager: importManager, + listRefreshService: listRefreshService + ) + } + // swiftlint:disable:next function_body_length override func start() { let vc = ItemListViewController.instantiate(from: .Main) @@ -84,6 +110,7 @@ class LibraryListCoordinator: ItemListCoordinator, UINavigationControllerDelegat func handleLibraryLoaded() { loadLastBookIfNeeded() syncList() + showSecondOnboarding() bindImportObserverIfNeeded() bindDownloadErrorObserver() @@ -94,6 +121,21 @@ class LibraryListCoordinator: ItemListCoordinator, UINavigationControllerDelegat } } + func showSecondOnboarding() { + guard let anonymousId = accountService.getAnonymousId() else { return } + + let coordinator = SecondOnboardingCoordinator( + flow: .modalOnlyFlow( + presentingController: flow.navigationController, + modalPresentationStyle: .fullScreen + ), + anonymousId: anonymousId, + accountService: accountService, + eventsService: EventsService() + ) + coordinator.start() + } + func bindImportObserverIfNeeded() { guard fileSubscription == nil, diff --git a/BookPlayer/Coordinators/LoginCoordinator.swift b/BookPlayer/Coordinators/LoginCoordinator.swift index 855473666..e0d2f68cd 100644 --- a/BookPlayer/Coordinators/LoginCoordinator.swift +++ b/BookPlayer/Coordinators/LoginCoordinator.swift @@ -29,7 +29,7 @@ class LoginCoordinator: Coordinator, AlertPresenter { func start() { let viewModel = LoginViewModel(accountService: self.accountService) - viewModel.coordinator = self + viewModel.alertPresenter = self viewModel.onTransition = { routes in switch routes { case .completeAccount: diff --git a/BookPlayer/Coordinators/MainCoordinator.swift b/BookPlayer/Coordinators/MainCoordinator.swift index 206d1cefa..dc16b8612 100644 --- a/BookPlayer/Coordinators/MainCoordinator.swift +++ b/BookPlayer/Coordinators/MainCoordinator.swift @@ -91,7 +91,8 @@ class MainCoordinator: NSObject { listRefreshService: ListSyncRefreshService( playerManager: playerManager, syncService: syncService - ) + ), + accountService: self.accountService ) playerManager.syncProgressDelegate = libraryCoordinator self.libraryCoordinator = libraryCoordinator @@ -127,17 +128,19 @@ class MainCoordinator: NSObject { .sink(receiveValue: { [weak self] _ in guard let self = self, - let account = self.accountService.getAccount() + self.accountService.hasAccount() else { return } - if account.hasSubscription, !account.id.isEmpty { + if self.accountService.hasSyncEnabled() { if !self.syncService.isActive { self.syncService.isActive = true self.getLibraryCoordinator()?.syncList() } } else { - self.syncService.isActive = false - self.syncService.cancelAllJobs() + if self.syncService.isActive { + self.syncService.isActive = false + self.syncService.cancelAllJobs() + } } }) diff --git a/BookPlayer/Library/ItemList Screen/ItemListViewController.swift b/BookPlayer/Library/ItemList Screen/ItemListViewController.swift index 6ca6f6e60..eefbbe7dd 100644 --- a/BookPlayer/Library/ItemList Screen/ItemListViewController.swift +++ b/BookPlayer/Library/ItemList Screen/ItemListViewController.swift @@ -217,7 +217,7 @@ class ItemListViewController: UIViewController, MVVMControllerProtocol, Storyboa do { try await viewModel.refreshAppState() tableView.refreshControl?.endRefreshing() - } catch { + } catch BPSyncRefreshError.scheduledTasks { tableView.refreshControl?.endRefreshing() /// Allow the refresh animation to complete and avoid jumping when showing the alert @@ -235,6 +235,8 @@ class ItemListViewController: UIViewController, MVVMControllerProtocol, Storyboa ] )) } + } catch { + tableView.refreshControl?.endRefreshing() } } } diff --git a/BookPlayer/Library/ItemList Screen/ItemListViewModel.swift b/BookPlayer/Library/ItemList Screen/ItemListViewModel.swift index f80535a78..e5fa6a8f3 100644 --- a/BookPlayer/Library/ItemList Screen/ItemListViewModel.swift +++ b/BookPlayer/Library/ItemList Screen/ItemListViewModel.swift @@ -979,6 +979,10 @@ class ItemListViewModel: ViewModelProtocol { /// Check if there's any pending file to import await coordinator.getMainCoordinator()?.getLibraryCoordinator()?.notifyPendingFiles() + guard syncService.isActive else { + throw BPSyncRefreshError.disabled + } + guard await syncService.queuedJobsCount() == 0 else { throw BPSyncRefreshError.scheduledTasks } diff --git a/BookPlayer/Profile/Login Screen/LoginViewController.swift b/BookPlayer/Profile/Login Screen/LoginViewController.swift index 4a6115ae5..dee5a826b 100644 --- a/BookPlayer/Profile/Login Screen/LoginViewController.swift +++ b/BookPlayer/Profile/Login Screen/LoginViewController.swift @@ -11,7 +11,7 @@ import BookPlayerKit import Foundation import Themeable -class LoginViewController: UIViewController, MVVMControllerProtocol { +class LoginViewController: UIViewController { var viewModel: LoginViewModel! // MARK: - UI components diff --git a/BookPlayer/Profile/Login Screen/LoginViewModel.swift b/BookPlayer/Profile/Login Screen/LoginViewModel.swift index e362cddcc..668c2a238 100644 --- a/BookPlayer/Profile/Login Screen/LoginViewModel.swift +++ b/BookPlayer/Profile/Login Screen/LoginViewModel.swift @@ -10,7 +10,12 @@ import AuthenticationServices import BookPlayerKit import Foundation -class LoginViewModel: ViewModelProtocol { +protocol LoginViewModelProtocol: ObservableObject { + func handleSignIn(authorization: ASAuthorization) + func dismiss() +} + +class LoginViewModel: LoginViewModelProtocol { enum Routes { case completeAccount case dismiss @@ -18,7 +23,7 @@ class LoginViewModel: ViewModelProtocol { var onTransition: BPTransition? - weak var coordinator: LoginCoordinator! + weak var alertPresenter: AlertPresenter! let accountService: AccountServiceProtocol init(accountService: AccountServiceProtocol) { @@ -27,14 +32,27 @@ class LoginViewModel: ViewModelProtocol { /// This should only be used when running the app in the simulator func setupTestAccount() { - do { - let token: String = Bundle.main.configurationValue(for: .mockedBearerToken) - try self.accountService.loginTestAccount(token: token) - } catch { - self.coordinator.showError(error) - } + Task { + await MainActor.run { [weak self] in + self?.alertPresenter.showLoader() + } + do { + let token: String = Bundle.main.configurationValue(for: .mockedBearerToken) + try await self.accountService.loginTestAccount(token: token) + await MainActor.run { [weak self] in + self?.alertPresenter.stopLoader() + } + } catch { + await MainActor.run { [weak self, error] in + self?.alertPresenter.stopLoader() + self?.handleError(error) + } + } - onTransition?(.completeAccount) + await MainActor.run { [weak self] in + self?.onTransition?(.completeAccount) + } + } } func handleSignIn(authorization: ASAuthorization) { @@ -44,13 +62,13 @@ class LoginViewModel: ViewModelProtocol { let tokenData = appleIDCredential.identityToken, let token = String(data: tokenData, encoding: .utf8) else { - self.coordinator.showError(AccountError.missingToken) + handleError(AccountError.missingToken) return } Task { [weak self, accountService, token, appleIDCredential] in await MainActor.run { [weak self] in - self?.coordinator.showLoader() + self?.alertPresenter.showLoader() } do { @@ -60,7 +78,7 @@ class LoginViewModel: ViewModelProtocol { ) await MainActor.run { [weak self, account] in - self?.coordinator.stopLoader() + self?.alertPresenter.stopLoader() if let account = account, !account.hasSubscription { @@ -71,8 +89,8 @@ class LoginViewModel: ViewModelProtocol { } } catch { await MainActor.run { [weak self, error] in - self?.coordinator.stopLoader() - self?.coordinator.showError(error) + self?.alertPresenter.stopLoader() + self?.handleError(error) } } } @@ -83,6 +101,14 @@ class LoginViewModel: ViewModelProtocol { } func handleError(_ error: Error) { - self.coordinator.showError(error) + alertPresenter.showAlert( + "error_title".localized, + message: error.localizedDescription, + completion: nil + ) + } + + func dismiss() { + onTransition?(.dismiss) } } diff --git a/BookPlayer/SecondOnboarding/SecondOnboardingCoordinator.swift b/BookPlayer/SecondOnboarding/SecondOnboardingCoordinator.swift new file mode 100644 index 000000000..aab15e591 --- /dev/null +++ b/BookPlayer/SecondOnboarding/SecondOnboardingCoordinator.swift @@ -0,0 +1,79 @@ +// +// SecondOnboardingCoordinator.swift +// BookPlayer +// +// Created by Gianni Carlo on 1/6/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import BookPlayerKit +import Foundation +import SwiftUI + +/// Handle second onboarding flows +class SecondOnboardingCoordinator: Coordinator { + let anonymousId: String + let accountService: AccountServiceProtocol + let eventsService: EventsServiceProtocol + let flow: BPCoordinatorPresentationFlow + unowned var presentedController: UIViewController? + + init( + flow: BPCoordinatorPresentationFlow, + anonymousId: String, + accountService: AccountServiceProtocol, + eventsService: EventsServiceProtocol + ) { + self.flow = flow + self.anonymousId = anonymousId + self.accountService = accountService + self.eventsService = eventsService + } + + func start() { + Task { + let response: SecondOnboardingResponse = try await accountService.getSecondOnboarding() + + await showOnboarding(data: response) + } + } + + @MainActor + func showOnboarding(data: SecondOnboardingResponse) { + switch data.type { + case .support: + let coordinator = SupportFlowCoordinator( + flow: flow, + anonymousId: anonymousId, + onboardingId: data.onboardingId, + stories: data.support, + accountService: accountService, + eventsService: eventsService + ) + coordinator.start() + } + } + + func showAlert(_ content: BPAlertContent) { + presentedController?.showAlert(content) + } + + func showLoader() { + if let vc = presentedController { + LoadingUtils.loadAndBlock(in: vc) + } + } + + func stopLoader() { + if let vc = presentedController { + LoadingUtils.stopLoading(in: vc) + } + } + + func showCongrats() { + presentedController?.view.startConfetti() + presentedController?.showAlert("thanks_amazing_title".localized, message: nil) { [weak self] in + self?.flow.finishPresentation(animated: true) + } + } +} diff --git a/BookPlayer/SecondOnboarding/SecondOnboardingResponse.swift b/BookPlayer/SecondOnboarding/SecondOnboardingResponse.swift new file mode 100644 index 000000000..8ddd6dccf --- /dev/null +++ b/BookPlayer/SecondOnboarding/SecondOnboardingResponse.swift @@ -0,0 +1,20 @@ +// +// SecondOnboardingResponse.swift +// BookPlayer +// +// Created by Gianni Carlo on 1/7/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import Foundation + +struct SecondOnboardingResponse: Codable { + let onboardingId: String + let type: SecondOnboardingType + let support: [StoryViewModel] + + enum CodingKeys: String, CodingKey { + case type, support + case onboardingId = "onboarding_id" + } +} diff --git a/BookPlayer/SecondOnboarding/SecondOnboardingType.swift b/BookPlayer/SecondOnboarding/SecondOnboardingType.swift new file mode 100644 index 000000000..a438ce2da --- /dev/null +++ b/BookPlayer/SecondOnboarding/SecondOnboardingType.swift @@ -0,0 +1,13 @@ +// +// SecondOnboardingType.swift +// BookPlayer +// +// Created by Gianni Carlo on 8/6/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import Foundation + +enum SecondOnboardingType: String, Codable { + case support +} diff --git a/BookPlayer/SecondOnboarding/StoryAccountSubscriptionService.swift b/BookPlayer/SecondOnboarding/StoryAccountSubscriptionService.swift new file mode 100644 index 000000000..dc64903eb --- /dev/null +++ b/BookPlayer/SecondOnboarding/StoryAccountSubscriptionService.swift @@ -0,0 +1,32 @@ +// +// StoryAccountSubscriptionService.swift +// BookPlayer +// +// Created by Gianni Carlo on 1/7/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import BookPlayerKit +import Foundation + +protocol StoryAccountSubscriptionProtocol { + func hasAccount() -> Bool + func subscribe(option: PricingOption) async throws -> Bool + func getSecondOnboarding() async throws -> T +} + +struct StoryAccountSubscriptionService: StoryAccountSubscriptionProtocol { + var accountService: AccountServiceProtocol + + func hasAccount() -> Bool { + return accountService.hasAccount() + } + + func subscribe(option: PricingOption) async throws -> Bool { + return try await accountService.subscribe(option: option) + } + + func getSecondOnboarding() async throws -> T { + return try await accountService.getSecondOnboarding() + } +} diff --git a/BookPlayer/SecondOnboarding/Support/SupportFlowCoordinator.swift b/BookPlayer/SecondOnboarding/Support/SupportFlowCoordinator.swift new file mode 100644 index 000000000..8831bfac7 --- /dev/null +++ b/BookPlayer/SecondOnboarding/Support/SupportFlowCoordinator.swift @@ -0,0 +1,132 @@ +// +// SupportFlowCoordinator.swift +// BookPlayer +// +// Created by Gianni Carlo on 30/6/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import BookPlayerKit +import Foundation +import SwiftUI + +/// Handle second onboarding flows +class SupportFlowCoordinator: Coordinator, AlertPresenter { + let accountService: AccountServiceProtocol + let eventsService: EventsServiceProtocol + let stories: [StoryViewModel] + let flow: BPCoordinatorPresentationFlow + let anonymousId: String + let onboardingId: String + unowned var presentedController: UIViewController? + + init( + flow: BPCoordinatorPresentationFlow, + anonymousId: String, + onboardingId: String, + stories: [StoryViewModel], + accountService: AccountServiceProtocol, + eventsService: EventsServiceProtocol + ) { + self.flow = flow + self.anonymousId = anonymousId + self.onboardingId = onboardingId + self.stories = stories + self.accountService = accountService + self.eventsService = eventsService + } + + func start() { + let subscriptionService = StoryAccountSubscriptionService(accountService: accountService) + let viewModel = StoryViewerViewModel( + subscriptionService: subscriptionService, + stories: stories + ) + + viewModel.onTransition = { route in + switch route { + case .dismiss: + self.dismiss() + case .showAlert(let model): + self.showAlert(model) + case .showLoader(let flag): + if flag { + self.showLoader() + } else { + self.stopLoader() + } + case .success: + self.showCongrats() + } + } + + let vc = UIHostingController(rootView: StoryViewer(viewModel: viewModel)) + presentedController = vc + flow.startPresentation(vc, animated: true) + eventsService.sendEvent( + "second_onboarding_start", + payload: [ + "rc_id": anonymousId, + "onboarding_id": onboardingId, + ] + ) + } + + func dismiss() { + eventsService.sendEvent( + "second_onboarding_skip", + payload: [ + "rc_id": anonymousId, + "onboarding_id": onboardingId, + ] + ) + flow.finishPresentation(animated: true) + } + + func showAlert(_ content: BPAlertContent) { + presentedController?.showAlert(content) + } + + func showLoader() { + if let vc = presentedController { + LoadingUtils.loadAndBlock(in: vc) + } + } + + func stopLoader() { + if let vc = presentedController { + LoadingUtils.stopLoading(in: vc) + } + } + + func showCongrats() { + eventsService.sendEvent( + "second_onboarding_subscription", + payload: [ + "rc_id": anonymousId, + "onboarding_id": onboardingId, + ] + ) + presentedController?.view.startConfetti() + presentedController?.showAlert("thanks_amazing_title".localized, message: nil) { [weak self] in + if self?.accountService.getAccountId() != nil { + self?.flow.finishPresentation(animated: true) + } else { + self?.showCreateProfile() + } + } + } + + func showCreateProfile() { + let viewModel = LoginViewModel(accountService: accountService) + viewModel.alertPresenter = self + viewModel.onTransition = { _ in + self.flow.finishPresentation(animated: true) + } + + let vc = UIHostingController(rootView: SupportProfileView(viewModel: viewModel)) + vc.modalPresentationStyle = .overFullScreen + presentedController?.present(vc, animated: true) + } +} + diff --git a/BookPlayer/SecondOnboarding/Support/SupportProfileView.swift b/BookPlayer/SecondOnboarding/Support/SupportProfileView.swift new file mode 100644 index 000000000..fb81ab64e --- /dev/null +++ b/BookPlayer/SecondOnboarding/Support/SupportProfileView.swift @@ -0,0 +1,95 @@ +// +// SupportProfileView.swift +// BookPlayer +// +// Created by Gianni Carlo on 1/7/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import AuthenticationServices +import BookPlayerKit +import SwiftUI + +struct SupportProfileView: View { + @StateObject var themeViewModel = ThemeViewModel() + @ObservedObject var viewModel: Model + + var body: some View { + ZStack { + themeViewModel.systemBackgroundColor + .ignoresSafeArea() + VStack { + HStack { + Button(action: { + viewModel.dismiss() + }, label: { + Image(systemName: "xmark") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 23) + .foregroundColor(themeViewModel.linkColor) + }) + Spacer() + } + .frame(height: 56) + .accessibilityHidden(true) + Spacer() + Image(systemName: "person.crop.circle") + .resizable() + .foregroundColor(themeViewModel.secondaryColor.opacity(0.5)) + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 150) + .padding([.bottom], Spacing.M) + .accessibilityHidden(true) + + Group { + Text("Create your profile") + .font(Font(Fonts.titleStory)) + .foregroundColor(themeViewModel.primaryColor) + .padding() + Text("To enable cloud sync, sign into your profile. You can always do this later from within the Profile tab") + .font(Font(Fonts.bodyStory)) + .foregroundColor(themeViewModel.primaryColor) + .multilineTextAlignment(.center) + } + .padding([.bottom], Spacing.M) + + Spacer() + SignInWithAppleButton { request in + request.requestedScopes = [.email] + } onCompletion: { result in + switch result { + case .success(let authorization): + viewModel.handleSignIn(authorization: authorization) + default: + break + } + } + .signInWithAppleButtonStyle(themeViewModel.useDarkVariant ? .white : .black) + .frame(height: 45) + Button(action: { + viewModel.dismiss() + }, label: { + Text("Not now") + .underline() + .font(Font(Fonts.body)) + .foregroundColor(themeViewModel.secondaryColor) + }) + .padding([.top], Spacing.S5) + } + .padding([.horizontal], Spacing.M) + } + } +} + +private class MockLoginViewModelProtocol: LoginViewModelProtocol { + func handleSignIn(authorization: ASAuthorization) { + print("Sign in") + } + + func dismiss() {} +} + +#Preview { + SupportProfileView(viewModel: MockLoginViewModelProtocol()) +} diff --git a/BookPlayer/Services/ListSyncRefreshService.swift b/BookPlayer/Services/ListSyncRefreshService.swift index 7019db367..9bbd703a3 100644 --- a/BookPlayer/Services/ListSyncRefreshService.swift +++ b/BookPlayer/Services/ListSyncRefreshService.swift @@ -12,6 +12,7 @@ import Foundation enum BPSyncRefreshError: Error { /// There are queued tasks and can't fetch remote data case scheduledTasks + case disabled } class ListSyncRefreshService: BPLogger { diff --git a/BookPlayer/Settings/Icons Screen/IconsViewModel.swift b/BookPlayer/Settings/Icons Screen/IconsViewModel.swift index addf4bdb2..8b3e5f200 100644 --- a/BookPlayer/Settings/Icons Screen/IconsViewModel.swift +++ b/BookPlayer/Settings/Icons Screen/IconsViewModel.swift @@ -27,7 +27,7 @@ final class IconsViewModel { @Published var account: Account? var hasSubscription: Bool { - return account?.hasSubscription == true + return accountService.hasSyncEnabled() } /// Callback to handle actions on this screen @@ -65,7 +65,7 @@ final class IconsViewModel { } func hasMadeDonation() -> Bool { - return (self.account?.donationMade ?? false) || account?.hasSubscription == true + return accountService.hasPlusAccess() } func showPro() { diff --git a/BookPlayer/Settings/SettingsViewModel.swift b/BookPlayer/Settings/SettingsViewModel.swift index c8e3a83f2..33ba2d952 100644 --- a/BookPlayer/Settings/SettingsViewModel.swift +++ b/BookPlayer/Settings/SettingsViewModel.swift @@ -76,7 +76,7 @@ class SettingsViewModel: ViewModelProtocol { } func hasMadeDonation() -> Bool { - return account?.hasSubscription == true + return accountService.hasSyncEnabled() } /// Handle registering the value in `UserDefaults` diff --git a/BookPlayer/Settings/Themes Screen/ThemesViewModel.swift b/BookPlayer/Settings/Themes Screen/ThemesViewModel.swift index f52a39890..aa3083748 100644 --- a/BookPlayer/Settings/Themes Screen/ThemesViewModel.swift +++ b/BookPlayer/Settings/Themes Screen/ThemesViewModel.swift @@ -27,7 +27,7 @@ final class ThemesViewModel { @Published var account: Account? var hasSubscription: Bool { - return account?.hasSubscription == true + return accountService.hasSyncEnabled() } /// Callback to handle actions on this screen @@ -65,7 +65,7 @@ final class ThemesViewModel { } func hasMadeDonation() -> Bool { - return (self.account?.donationMade ?? false) || account?.hasSubscription == true + return accountService.hasPlusAccess() } func showPro() { diff --git a/BookPlayer/Utils/AlertPresenter.swift b/BookPlayer/Utils/AlertPresenter.swift index 4d678be16..d4a1e0872 100644 --- a/BookPlayer/Utils/AlertPresenter.swift +++ b/BookPlayer/Utils/AlertPresenter.swift @@ -8,7 +8,7 @@ import Foundation -protocol AlertPresenter { +protocol AlertPresenter: AnyObject { func showAlert(_ title: String?, message: String?, completion: (() -> Void)?) func showLoader() func stopLoader() @@ -17,7 +17,7 @@ protocol AlertPresenter { /// Empty implementation of `AlertPresenter` /// - Note: the need of this means that the `AppDelegate.loadPlayer` could benefit from having an async version /// so we don't need to pass an `AlertPresenter` as a parameter -struct VoidAlertPresenter: AlertPresenter { +class VoidAlertPresenter: AlertPresenter { func showAlert(_ title: String?, message: String?, completion: (() -> Void)?) {} func showLoader() {} diff --git a/BookPlayer/Utils/Views/StoryViewer/LoadingBar.swift b/BookPlayer/Utils/Views/StoryViewer/LoadingBar.swift new file mode 100644 index 000000000..b66348697 --- /dev/null +++ b/BookPlayer/Utils/Views/StoryViewer/LoadingBar.swift @@ -0,0 +1,36 @@ +// +// LoadingBar.swift +// BookPlayer +// +// Created by Gianni Carlo on 9/6/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import SwiftUI + +struct LoadingBar: View { + var progress: CGFloat + + var body: some View { + GeometryReader { geometry in + ZStack(alignment: .leading) { + Rectangle() + .foregroundColor(Color.gray.opacity(0.9)) + .cornerRadius(5) + + Rectangle() + .frame(width: geometry.size.width * progress, height: nil, alignment: .leading) + .foregroundColor(Color.white.opacity(0.9)) + .cornerRadius(5) + } + } + } +} + +#Preview { + VStack { + LoadingBar(progress: 0.7) + .frame(height: 2) + .padding() + }.background(Color.black) +} diff --git a/BookPlayer/Utils/Views/StoryViewer/PricingBoxView.swift b/BookPlayer/Utils/Views/StoryViewer/PricingBoxView.swift new file mode 100644 index 000000000..6d6a0f4c0 --- /dev/null +++ b/BookPlayer/Utils/Views/StoryViewer/PricingBoxView.swift @@ -0,0 +1,67 @@ +// +// PricingBoxView.swift +// BookPlayer +// +// Created by Gianni Carlo on 10/6/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import BookPlayerKit +import SwiftUI + +struct PricingBoxView: View { + @Binding var title: String + @Binding var isSelected: Bool + + var imageLength: CGFloat = 16 + var imageName: String { + isSelected ? "checkmark.circle" : "circle" + } + var foregroundColor: Color { + isSelected + ? Color(UIColor(hex: "3488D1")) + : Color(UIColor(hex: "334046")) + } + var backgroundColor: Color { + isSelected + ? Color.white + : Color(UIColor(hex: "F8F8F8")) + } + + var body: some View { + VStack(spacing: 0) { + HStack { + Spacer() + Image(systemName: imageName) + .resizable() + .frame(width: imageLength, height: imageLength) + .foregroundColor(foregroundColor) + .padding([.trailing, .top], Spacing.S3) + } + Text(title) + .font(Font(Fonts.titleLarge)) + .foregroundColor(foregroundColor) + Text("/month") + .font(Font(Fonts.titleRegular)) + .foregroundColor(foregroundColor.opacity(0.7)) + } + .padding([.bottom]) + .background(backgroundColor) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .contentShape(Rectangle()) + .accessibilityElement(children: .combine) + .accessibilityAddTraits(.isButton) + .frame(maxWidth: 88) + + } +} + +#Preview { + ZStack { + StoryBackgroundView() + PricingBoxView( + title: .constant("$1.99"), + isSelected: .constant(true) + ) + } +} diff --git a/BookPlayer/Utils/Views/StoryViewer/StoryActionView.swift b/BookPlayer/Utils/Views/StoryViewer/StoryActionView.swift new file mode 100644 index 000000000..cecb2f618 --- /dev/null +++ b/BookPlayer/Utils/Views/StoryViewer/StoryActionView.swift @@ -0,0 +1,147 @@ +// +// StoryActionView.swift +// BookPlayer +// +// Created by Gianni Carlo on 10/6/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import BookPlayerKit +import SwiftUI + +struct StoryActionView: View { + @Binding var action: StoryActionType + @State private var selected: PricingOption + @State private var showSlider = false + @State private var sliderValue: Double + private var sliderSelectedOption: PricingOption? { + let intValue = Int(ceil(sliderValue)) + + return PricingOption.parseValue(intValue) + } + var onSubscription: (PricingOption) -> Void + var onDismiss: () -> Void + + init( + action: Binding, + onSubscription: @escaping (PricingOption) -> Void, + onDismiss: @escaping () -> Void + ) { + self._action = action + self.selected = action.wrappedValue.defaultOption + self.sliderValue = action.wrappedValue.defaultOption.cost + self.onSubscription = onSubscription + self.onDismiss = onDismiss + } + + var body: some View { + VStack { + if showSlider, + let sliderOptions = action.sliderOptions { + VStack { + Text(String(format: "$%.0f/mo", sliderValue)) + .font(Font(Fonts.pricingTitle)) + .foregroundColor(.white) + .accessibilityHidden(true) + Slider( + value: $sliderValue, + in: sliderOptions.min...sliderOptions.max, + step: 1.0 + ) { + Text("Pay what you think is fair") + } minimumValueLabel: { + Text(String(format: "$%.0f", action.options.first!.cost)) + .font(Font(Fonts.title)) + .foregroundColor(.white) + .accessibilityHidden(true) + } maximumValueLabel: { + Text(String(format: "$%.0f", action.options.last!.cost)) + .font(Font(Fonts.title)) + .foregroundColor(.white) + .accessibilityHidden(true) + } + + Text("Pay what you think is fair") + .multilineTextAlignment(.center) + .font(Font(Fonts.title)) + .foregroundColor(.white) + .opacity(0.8) + .accessibilityHidden(true) + } + .padding([.bottom], Spacing.L1) + + } else { + HStack(spacing: Spacing.S1) { + Spacer() + ForEach(action.options) { option in + PricingBoxView( + title: .constant(option.title), + isSelected: .constant(selected == option) + ) + .onTapGesture { + selected = option + } + } + Spacer() + } + if action.sliderOptions != nil { + Button(action: { + showSlider.toggle() + }, label: { + Text("Choose custom amount") + .font(Font(Fonts.title)) + .foregroundColor(.white) + .underline() + .padding([.top], Spacing.S4) + }) + } + } + + Button(action: { + if showSlider, + let option = sliderSelectedOption { + onSubscription(option) + } else { + onSubscription(selected) + } + }, label: { + Text(action.button) + .contentShape(Rectangle()) + .font(Font(Fonts.headline)) + .frame(height: 45) + .frame(maxWidth: .infinity) + .foregroundColor(Color(UIColor(hex: "334046"))) + .background(Color.white) + .cornerRadius(6) + }) + .padding([.top], Spacing.L1) + if let dismiss = action.dismiss { + Button(action: { + onDismiss() + }, label: { + Text(dismiss) + .underline() + .font(Font(Fonts.body)) + .foregroundColor(.white) + }) + .padding([.top], Spacing.S5) + } + } + } +} + +#Preview { + ZStack { + StoryBackgroundView() + StoryActionView( + action: .constant(.init( + options: [.supportTier4, .supportTier7, .supportTier10], + defaultOption: .proMonthly, + sliderOptions: .init(min: 3.99, max: 9.99), + button: "Continue" + )), + onSubscription: { option in print(option.title) }, + onDismiss: {} + ) + } +} diff --git a/BookPlayer/Utils/Views/StoryViewer/StoryBackgroundView.swift b/BookPlayer/Utils/Views/StoryViewer/StoryBackgroundView.swift new file mode 100644 index 000000000..6920c0624 --- /dev/null +++ b/BookPlayer/Utils/Views/StoryViewer/StoryBackgroundView.swift @@ -0,0 +1,28 @@ +// +// StoryBackgroundView.swift +// BookPlayer +// +// Created by Gianni Carlo on 10/6/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import SwiftUI + +struct StoryBackgroundView: View { + var body: some View { + Rectangle() + .fill(LinearGradient( + gradient: Gradient(colors: [ + Color(UIColor(hex: "4285C5")), + Color(UIColor(hex: "3D4494")) + ]), + startPoint: .bottomLeading, + endPoint: .topTrailing + )) + .ignoresSafeArea() + } +} + +#Preview { + StoryBackgroundView() +} diff --git a/BookPlayer/Utils/Views/StoryViewer/StoryProgress.swift b/BookPlayer/Utils/Views/StoryViewer/StoryProgress.swift new file mode 100644 index 000000000..5feafba2b --- /dev/null +++ b/BookPlayer/Utils/Views/StoryViewer/StoryProgress.swift @@ -0,0 +1,37 @@ +// +// StoryProgress.swift +// BookPlayer +// +// Created by Gianni Carlo on 9/6/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import SwiftUI + +struct StoryProgress: View { + @Binding var storiesCount: Int + @Binding var progress: Double + + var body: some View { + HStack(alignment: .center, spacing: 4) { + ForEach(0.. Void + var onPause: () -> Void + var onResume: () -> Void + @State var touchDateReference: Date? + + var longPressSkip: some Gesture { + DragGesture(minimumDistance: 0) + .onChanged { _ in + if touchDateReference == nil { + touchDateReference = Date() + onPause() + } + } + .onEnded { _ in + guard let touchDateReference else { return } + + let time = touchDateReference.distance(to: Date()) + + defer { + self.touchDateReference = nil + } + + if time <= 0.3 { + onSkip() + } + + onResume() + } + } + + var body: some View { + HStack(alignment: .center, spacing: 0) { + Rectangle() + .foregroundColor(.clear) + .contentShape(Rectangle()) + .gesture(longPressSkip) + .accessibilityAction { + onSkip() + } + .accessibilityValue("Previous") + + Rectangle() + .foregroundColor(.clear) + .accessibilityHidden(true) + } + .accessibilityElement(children: .contain) + } +} + +struct StoryForwardControlView: View { + var onSkip: () -> Void + var onPause: () -> Void + var onResume: () -> Void + @State var touchDateReference: Date? + + var longPressSkip: some Gesture { + DragGesture(minimumDistance: 0) + .onChanged { _ in + if touchDateReference == nil { + touchDateReference = Date() + onPause() + } + } + .onEnded { _ in + guard let touchDateReference else { return } + + let time = touchDateReference.distance(to: Date()) + + defer { + self.touchDateReference = nil + } + + if time <= 0.3 { + onSkip() + } + + onResume() + } + } + + var body: some View { + HStack(alignment: .center, spacing: 0) { + Rectangle() + .foregroundColor(.clear) + .accessibilityHidden(true) + Rectangle() + .foregroundColor(.clear) + .contentShape(Rectangle()) + .gesture(longPressSkip) + .accessibilityAction { + onSkip() + } + .accessibilityValue("Next") + } + .accessibilityElement(children: .contain) + } +} + +#Preview { + Group { + StoryRewindControlView(onSkip: { + print("onSkip") + }, onPause: { + print("onPause") + }, onResume: { + print("onResume") + }) + StoryForwardControlView(onSkip: { + print("onSkip") + }, onPause: { + print("onPause") + }, onResume: { + print("onResume") + }) + } +} diff --git a/BookPlayer/Utils/Views/StoryViewer/StoryView.swift b/BookPlayer/Utils/Views/StoryViewer/StoryView.swift new file mode 100644 index 000000000..2f9e9bfa9 --- /dev/null +++ b/BookPlayer/Utils/Views/StoryViewer/StoryView.swift @@ -0,0 +1,155 @@ +// +// StoryView.swift +// BookPlayer +// +// Created by Gianni Carlo on 9/6/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import BookPlayerKit +import SwiftUI + +struct StoryView: View { + @Binding var model: StoryViewModel + var onPrevious: () -> Void + var onNext: () -> Void + var onPause: () -> Void + var onResume: () -> Void + var onSubscription: (PricingOption) -> Void + var onDismiss: () -> Void + + var body: some View { + ZStack { + StoryRewindControlView( + onSkip: onPrevious, + onPause: onPause, + onResume: onResume + ) + .accessibilityHidden(true) + VStack { + VStack { + Text(model.title) + .shadow(radius: 2, y: 3) + .font(Font(Fonts.titleStory)) + .padding() + .allowsHitTesting(false) + + Text(model.body) + .font(Font(Fonts.bodyStory)) + .padding([.bottom, .leading, .trailing]) + .multilineTextAlignment(.center) + .allowsHitTesting(false) + + if let image = model.image { + switch image { + case "family-pic": + Image(.smallFamilyPic) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 300) + .cornerRadius(9) + .allowsHitTesting(false) + .padding() + .accessibilityHidden(true) + case "app-icon": + HStack(alignment: .center) { + VStack { + Image(uiImage: UIImage(named: "retro-icon@3x")!) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 90, height: 90) + .cornerRadius(9) + Text("2016") + .font(.callout.weight(.bold)) + } + + Image(systemName: "arrow.forward") + .resizable() + .aspectRatio(contentMode: .fit) + .opacity(0.6) + .font(.largeTitle.weight(.bold)) + .frame(width: 40, height: 20) + .padding([.leading, .trailing], Spacing.S3) + .offset(y: -Spacing.S1) + VStack { + Image(uiImage: UIImage(named: "default-icon@3x")!) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 90, height: 90) + .cornerRadius(9) + Text("Now") + .font(.callout.weight(.bold)) + } + } + .allowsHitTesting(false) + .padding([.top], Spacing.L1) + .padding([.leading, .trailing]) + .accessibilityHidden(true) + default: + EmptyView() + } + + } + } + .accessibilityElement(children: .combine) + + if let action = Binding($model.action) { + StoryActionView( + action: action, + onSubscription: onSubscription, + onDismiss: onDismiss + ) + .padding([.leading, .trailing]) + .padding([.top], Spacing.L1) + } + + Spacer() + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .animation(.smooth, value: model.title) + .accessibilityElement(children: .contain) + .accessibilitySortPriority(1) + .zIndex(1) + StoryForwardControlView( + onSkip: onNext, + onPause: onPause, + onResume: onResume + ) + .accessibilityHidden(model.action != nil) + } + } +} + +#Preview { + ZStack { + StoryBackgroundView() + StoryView(model: .constant( + StoryViewModel( + title: "Story title", + body: "Story body", + duration: 2, + action: .init( + options: [ + .supportTier3, + .proMonthly, + .supportTier10 + ], + defaultOption: .proMonthly, + button: "" + ) + )), onPrevious: { + print("Previous") + }, onNext: { + print("Next") + }, onPause: { + print("Pause") + }, onResume: { + print("Resume") + }, onSubscription: { option in + print(option.title) + }, onDismiss: { + print("Dismiss") + }) + .foregroundColor(.white) + } +} diff --git a/BookPlayer/Utils/Views/StoryViewer/StoryViewModel.swift b/BookPlayer/Utils/Views/StoryViewer/StoryViewModel.swift new file mode 100644 index 000000000..d9071cb41 --- /dev/null +++ b/BookPlayer/Utils/Views/StoryViewer/StoryViewModel.swift @@ -0,0 +1,42 @@ +// +// StoryViewModel.swift +// BookPlayer +// +// Created by Gianni Carlo on 10/6/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import BookPlayerKit +import Foundation + +struct StoryActionType: Codable { + var options: [PricingOption] + var defaultOption: PricingOption + var sliderOptions: SliderOptions? + var button: String + var dismiss: String? + + enum CodingKeys: String, CodingKey { + case options, button, dismiss + case defaultOption = "default_option" + case sliderOptions = "slider_options" + } +} + +struct SliderOptions: Codable { + var min: Double + var max: Double +} + +struct StoryViewModel: Identifiable, Equatable, Codable { + static func == (lhs: StoryViewModel, rhs: StoryViewModel) -> Bool { + lhs.id == rhs.id + } + + var id: String { title } + var title: String + var body: String + var image: String? + var duration: TimeInterval + var action: StoryActionType? +} diff --git a/BookPlayer/Utils/Views/StoryViewer/StoryViewer.swift b/BookPlayer/Utils/Views/StoryViewer/StoryViewer.swift new file mode 100644 index 000000000..b5c69de5f --- /dev/null +++ b/BookPlayer/Utils/Views/StoryViewer/StoryViewer.swift @@ -0,0 +1,82 @@ +// +// StoryViewer.swift +// BookPlayer +// +// Created by Gianni Carlo on 8/6/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import BookPlayerKit +import SwiftUI + +struct StoryViewer: View { + @State var firstSeen = false + @ObservedObject var viewModel: StoryViewerViewModel + + var body: some View { + ZStack(alignment: .top) { + StoryBackgroundView() + .accessibilityHidden(true) + StoryProgress( + storiesCount: .constant(viewModel.storiesCount), + progress: $viewModel.progress + ) + .padding([.trailing, .leading, .bottom]) + .accessibilityHidden(true) + StoryView( + model: $viewModel.currentModel, + onPrevious: viewModel.previous, + onNext: viewModel.next, + onPause: viewModel.pause, + onResume: viewModel.start, + onSubscription: viewModel.handleSubscription(option:), + onDismiss: viewModel.handleDismiss + ) + .foregroundColor(Color.white) + .padding() + .offset(y: Spacing.L1) + } + .onAppear { viewModel.start() } + .onChange( + of: viewModel.currentModel, + perform: { _ in + UIAccessibility.post(notification: .screenChanged, argument: nil) + }) + } +} + +private class PreviewSubscriptionServiceMock: StoryAccountSubscriptionProtocol { + func hasAccount() -> Bool { + return true + } + + func getSecondOnboarding() async throws -> T { + throw BPSyncRefreshError.disabled + } + + func subscribe(option: PricingOption) async throws -> Bool { + return true + } +} + +#Preview { + StoryViewer( + viewModel: StoryViewerViewModel( + subscriptionService: PreviewSubscriptionServiceMock(), + stories: [ + StoryViewModel( + title: "Story 1", + body: + "Body 1", + image: "app-icon", + duration: 10, action: .none), + StoryViewModel( + title: "Story 2", + body: + "Body 2", + duration: 5, + action: .init( + options: [.supportTier4, .supportTier7, .supportTier10], defaultOption: .supportTier7, + sliderOptions: .init(min: 3.99, max: 9.99), button: "Continue", dismiss: "Not now")), + ])) +} diff --git a/BookPlayer/Utils/Views/StoryViewer/StoryViewerViewModel.swift b/BookPlayer/Utils/Views/StoryViewer/StoryViewerViewModel.swift new file mode 100644 index 000000000..0eccd2e1e --- /dev/null +++ b/BookPlayer/Utils/Views/StoryViewer/StoryViewerViewModel.swift @@ -0,0 +1,102 @@ +// +// StoryTimer.swift +// BookPlayer +// +// Created by Gianni Carlo on 10/6/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import BookPlayerKit +import Combine +import Foundation + +class StoryViewerViewModel: ObservableObject { + enum Routes { + case showLoader(Bool) + case showAlert(BPAlertContent) + case success + case dismiss + } + + @Published var currentModel: StoryViewModel + @Published var progress: Double = 0 + @Published var isOnLastStory: Bool = false + + let subscriptionService: StoryAccountSubscriptionProtocol + public var storiesCount: Int { stories.count } + + /// Callback to handle actions on this screen + var onTransition: BPTransition? + private var stories: [StoryViewModel] + private let publisher: Timer.TimerPublisher + private var cancellable: Cancellable? + + init( + subscriptionService: StoryAccountSubscriptionProtocol, + stories: [StoryViewModel] + ) { + self.subscriptionService = subscriptionService + self.stories = stories + self.currentModel = stories.first! + self.publisher = Timer.publish(every: 0.1, on: .main, in: .default) + } + + func start() { + guard !UIAccessibility.isVoiceOverRunning else { return } + + cancellable?.cancel() + cancellable = publisher.autoconnect().sink(receiveValue: { [weak self] _ in + guard let self else { return } + var newProgress = self.progress + (0.1 / self.currentModel.duration) + if Int(newProgress) >= self.storiesCount { newProgress = Double(self.storiesCount) - 0.01 } + self.progress = newProgress + self.currentModel = self.stories[Int(newProgress)] + }) + } + + func next() { + guard min(Int(progress) + 1, storiesCount) != storiesCount else { + return + } + let newProgress = max((Int(progress) + 1) % storiesCount, 0) + progress = Double(newProgress) + isOnLastStory = stories.count - Int(progress) == 1 + currentModel = stories[newProgress] + } + + func previous() { + let newProgress = max((Int(self.progress) - 1) % storiesCount, 0) + progress = Double(newProgress) + isOnLastStory = stories.count - Int(progress) == 1 + currentModel = stories[newProgress] + } + + func pause() { + cancellable?.cancel() + } + + func handleSubscription(option: PricingOption) { + Task { @MainActor [weak self] in + guard let self = self else { return } + + self.onTransition?(.showLoader(true)) + + do { + let userCancelled = try await self.subscriptionService.subscribe(option: option) + self.onTransition?(.showLoader(false)) + if !userCancelled { + self.onTransition?(.success) + } + } catch { + self.onTransition?(.showLoader(false)) + self.onTransition?(.showAlert( + BPAlertContent.errorAlert(message: error.localizedDescription) + )) + } + } + } + + func handleDismiss() { + onTransition?(.dismiss) + } +} diff --git a/BookPlayerTests/Coordinators/ItemListCoordinatorTests.swift b/BookPlayerTests/Coordinators/ItemListCoordinatorTests.swift index 93c23bf9b..0de8dab83 100644 --- a/BookPlayerTests/Coordinators/ItemListCoordinatorTests.swift +++ b/BookPlayerTests/Coordinators/ItemListCoordinatorTests.swift @@ -40,7 +40,8 @@ class LibraryListCoordinatorTests: XCTestCase { listRefreshService: ListSyncRefreshService( playerManager: playerManagerMock, syncService: syncServiceMock - ) + ), + accountService: coreServices.accountService ) self.libraryListCoordinator.start() diff --git a/BookPlayerTests/Mocks/AccountServiceMock.swift b/BookPlayerTests/Mocks/AccountServiceMock.swift index 7b09cd02b..ac16a243b 100644 --- a/BookPlayerTests/Mocks/AccountServiceMock.swift +++ b/BookPlayerTests/Mocks/AccountServiceMock.swift @@ -11,8 +11,20 @@ import Foundation import RevenueCat class AccountServiceMock: AccountServiceProtocol { - func hasActiveSubscription() -> Bool { - return account?.hasSubscription == true + func getAnonymousId() -> String? { + return nil + } + + func hasSyncEnabled() -> Bool { + return false + } + + func hasPlusAccess() -> Bool { + return false + } + + func getSecondOnboarding() async throws -> T { + throw BookPlayerError.cancelledTask } var account: Account? @@ -81,6 +93,11 @@ class AccountServiceMock: AccountServiceProtocol { return true } + func subscribe(option: BookPlayerKit.PricingOption) async throws -> Bool { + self.account?.hasSubscription = true + return true + } + func restorePurchases() async throws -> CustomerInfo { self.account?.hasSubscription = true return try await Purchases.shared.customerInfo() @@ -93,7 +110,7 @@ class AccountServiceMock: AccountServiceProtocol { return self.account } - func loginTestAccount(token: String) throws {} + func loginTestAccount(token: String) async throws {} func logout() throws {} diff --git a/IAP-Configuration.storekit b/IAP-Configuration.storekit index 5e53a93f5..1993f28e2 100644 --- a/IAP-Configuration.storekit +++ b/IAP-Configuration.storekit @@ -96,7 +96,59 @@ } ], "settings" : { - "_compatibilityTimeRate" : 6, + "_compatibilityTimeRate" : { + "3" : 6 + }, + "_failTransactionsEnabled" : false, + "_locale" : "en_US", + "_storefront" : "USA", + "_storeKitErrors" : [ + { + "current" : null, + "enabled" : false, + "name" : "Load Products" + }, + { + "current" : null, + "enabled" : false, + "name" : "Purchase" + }, + { + "current" : null, + "enabled" : false, + "name" : "Verification" + }, + { + "current" : null, + "enabled" : false, + "name" : "App Store Sync" + }, + { + "current" : null, + "enabled" : false, + "name" : "Subscription Status" + }, + { + "current" : null, + "enabled" : false, + "name" : "App Transaction" + }, + { + "current" : null, + "enabled" : false, + "name" : "Manage Subscriptions Sheet" + }, + { + "current" : null, + "enabled" : false, + "name" : "Refund Request Sheet" + }, + { + "current" : null, + "enabled" : false, + "name" : "Offer Code Redeem Sheet" + } + ], "_timeRate" : 15 }, "subscriptionGroups" : [ @@ -156,12 +208,237 @@ "referenceName" : "pro.subscription.yearly", "subscriptionGroupID" : "652B5FC0", "type" : "RecurringSubscription" + }, + { + "adHocOffers" : [ + + ], + "codeOffers" : [ + + ], + "displayPrice" : "0.99", + "familyShareable" : false, + "groupNumber" : 1, + "internalID" : "EE383201", + "introductoryOffer" : null, + "localizations" : [ + { + "description" : "", + "displayName" : "", + "locale" : "en_US" + } + ], + "productID" : "com.tortugapower.audiobookplayer.subscription.support.1", + "recurringSubscriptionPeriod" : "P1M", + "referenceName" : "subscription.support.1", + "subscriptionGroupID" : "652B5FC0", + "type" : "RecurringSubscription" + }, + { + "adHocOffers" : [ + + ], + "codeOffers" : [ + + ], + "displayPrice" : "1.99", + "familyShareable" : false, + "groupNumber" : 1, + "internalID" : "FCF99805", + "introductoryOffer" : null, + "localizations" : [ + { + "description" : "", + "displayName" : "", + "locale" : "en_US" + } + ], + "productID" : "com.tortugapower.audiobookplayer.subscription.support.2", + "recurringSubscriptionPeriod" : "P1M", + "referenceName" : "subscription.support.2", + "subscriptionGroupID" : "652B5FC0", + "type" : "RecurringSubscription" + }, + { + "adHocOffers" : [ + + ], + "codeOffers" : [ + + ], + "displayPrice" : "2.99", + "familyShareable" : false, + "groupNumber" : 1, + "internalID" : "B7B02088", + "introductoryOffer" : null, + "localizations" : [ + { + "description" : "", + "displayName" : "", + "locale" : "en_US" + } + ], + "productID" : "com.tortugapower.audiobookplayer.subscription.support.3", + "recurringSubscriptionPeriod" : "P1M", + "referenceName" : "subscription.support.3", + "subscriptionGroupID" : "652B5FC0", + "type" : "RecurringSubscription" + }, + { + "adHocOffers" : [ + + ], + "codeOffers" : [ + + ], + "displayPrice" : "3.99", + "familyShareable" : false, + "groupNumber" : 1, + "internalID" : "570A9BCB", + "introductoryOffer" : null, + "localizations" : [ + { + "description" : "", + "displayName" : "", + "locale" : "en_US" + } + ], + "productID" : "com.tortugapower.audiobookplayer.subscription.support.4", + "recurringSubscriptionPeriod" : "P1M", + "referenceName" : "subscription.support.4", + "subscriptionGroupID" : "652B5FC0", + "type" : "RecurringSubscription" + }, + { + "adHocOffers" : [ + + ], + "codeOffers" : [ + + ], + "displayPrice" : "5.99", + "familyShareable" : false, + "groupNumber" : 1, + "internalID" : "4F02881E", + "introductoryOffer" : null, + "localizations" : [ + { + "description" : "", + "displayName" : "", + "locale" : "en_US" + } + ], + "productID" : "com.tortugapower.audiobookplayer.subscription.support.6", + "recurringSubscriptionPeriod" : "P1M", + "referenceName" : "subscription.support.6", + "subscriptionGroupID" : "652B5FC0", + "type" : "RecurringSubscription" + }, + { + "adHocOffers" : [ + + ], + "codeOffers" : [ + + ], + "displayPrice" : "6.99", + "familyShareable" : false, + "groupNumber" : 1, + "internalID" : "2DCC769A", + "introductoryOffer" : null, + "localizations" : [ + { + "description" : "", + "displayName" : "", + "locale" : "en_US" + } + ], + "productID" : "com.tortugapower.audiobookplayer.subscription.support.7", + "recurringSubscriptionPeriod" : "P1M", + "referenceName" : "subscription.support.7", + "subscriptionGroupID" : "652B5FC0", + "type" : "RecurringSubscription" + }, + { + "adHocOffers" : [ + + ], + "codeOffers" : [ + + ], + "displayPrice" : "7.99", + "familyShareable" : false, + "groupNumber" : 1, + "internalID" : "A977972B", + "introductoryOffer" : null, + "localizations" : [ + { + "description" : "", + "displayName" : "", + "locale" : "en_US" + } + ], + "productID" : "com.tortugapower.audiobookplayer.subscription.support.8", + "recurringSubscriptionPeriod" : "P1M", + "referenceName" : "subscription.support.8", + "subscriptionGroupID" : "652B5FC0", + "type" : "RecurringSubscription" + }, + { + "adHocOffers" : [ + + ], + "codeOffers" : [ + + ], + "displayPrice" : "8.99", + "familyShareable" : false, + "groupNumber" : 1, + "internalID" : "9410B8C0", + "introductoryOffer" : null, + "localizations" : [ + { + "description" : "", + "displayName" : "", + "locale" : "en_US" + } + ], + "productID" : "com.tortugapower.audiobookplayer.subscription.support.9", + "recurringSubscriptionPeriod" : "P1M", + "referenceName" : "subscription.support.9", + "subscriptionGroupID" : "652B5FC0", + "type" : "RecurringSubscription" + }, + { + "adHocOffers" : [ + + ], + "codeOffers" : [ + + ], + "displayPrice" : "9.99", + "familyShareable" : false, + "groupNumber" : 1, + "internalID" : "0DEF0B7A", + "introductoryOffer" : null, + "localizations" : [ + { + "description" : "", + "displayName" : "", + "locale" : "en_US" + } + ], + "productID" : "com.tortugapower.audiobookplayer.subscription.support.10", + "recurringSubscriptionPeriod" : "P1M", + "referenceName" : "subscription.support.10", + "subscriptionGroupID" : "652B5FC0", + "type" : "RecurringSubscription" } ] } ], "version" : { - "major" : 2, + "major" : 3, "minor" : 0 } } diff --git a/Shared/Services/Account/AccountAPI.swift b/Shared/Services/Account/AccountAPI.swift index ec63ca421..05aff5fc5 100644 --- a/Shared/Services/Account/AccountAPI.swift +++ b/Shared/Services/Account/AccountAPI.swift @@ -11,6 +11,7 @@ import Foundation public enum AccountAPI { case login(token: String) case delete + case secondOnboarding(anonymousId: String, firstSeen: Double, region: String) } extension AccountAPI: Endpoint { @@ -20,6 +21,8 @@ extension AccountAPI: Endpoint { return "/v1/user/login" case .delete: return "/v1/user/delete" + case .secondOnboarding: + return "/v1/user/second_onboarding" } } @@ -29,6 +32,8 @@ extension AccountAPI: Endpoint { return .post case .delete: return .delete + case .secondOnboarding: + return .post } } @@ -38,6 +43,12 @@ extension AccountAPI: Endpoint { return ["token_id": token] case .delete: return nil + case .secondOnboarding(let anonymousId, let firstSeen, let region): + return [ + "rc_id": anonymousId, + "first_seen": firstSeen, + "region": region + ] } } } diff --git a/Shared/Services/Account/AccountService.swift b/Shared/Services/Account/AccountService.swift index 95fbd94cf..5bf545477 100644 --- a/Shared/Services/Account/AccountService.swift +++ b/Shared/Services/Account/AccountService.swift @@ -21,6 +21,10 @@ public enum AccountError: Error { case missingToken } +public enum SecondOnboardingError: Error { + case notApplicable +} + extension AccountError: LocalizedError { public var errorDescription: String? { switch self { @@ -38,9 +42,11 @@ extension AccountError: LocalizedError { public protocol AccountServiceProtocol { func getAccountId() -> String? + func getAnonymousId() -> String? func getAccount() -> Account? func hasAccount() -> Bool - func hasActiveSubscription() -> Bool + func hasSyncEnabled() -> Bool + func hasPlusAccess() -> Bool func createAccount(donationMade: Bool) @@ -57,9 +63,10 @@ public protocol AccountServiceProtocol { func getSubscriptionOptions() async throws -> [PricingModel] func subscribe(option: PricingModel) async throws -> Bool + func subscribe(option: PricingOption) async throws -> Bool func restorePurchases() async throws -> CustomerInfo - func loginTestAccount(token: String) throws + func loginTestAccount(token: String) async throws func login( with token: String, userId: String @@ -70,6 +77,8 @@ public protocol AccountServiceProtocol { func logout() throws func deleteAccount() async throws -> String + + func getSecondOnboarding() async throws -> T } public final class AccountService: AccountServiceProtocol { @@ -104,6 +113,10 @@ public final class AccountService: AccountServiceProtocol { } } + public func getAnonymousId() -> String? { + return Purchases.shared.cachedCustomerInfo?.id + } + public func getAccount() -> Account? { let context = self.dataManager.getContext() let fetch: NSFetchRequest = Account.fetchRequest() @@ -123,8 +136,16 @@ public final class AccountService: AccountServiceProtocol { return false } - public func hasActiveSubscription() -> Bool { - return getAccount()?.hasSubscription == true + public func hasSyncEnabled() -> Bool { + return Purchases.shared.cachedCustomerInfo?.entitlements.all["pro"]?.isActive == true + } + + public func hasPlusAccess() -> Bool { + let entitlements = Purchases.shared.cachedCustomerInfo?.entitlements.all + + return entitlements?["plus"]?.isActive == true || + entitlements?["pro"]?.isActive == true || + getAccount()?.donationMade == true } public func createAccount(donationMade: Bool) { @@ -208,7 +229,15 @@ public final class AccountService: AccountServiceProtocol { } public func subscribe(option: PricingModel) async throws -> Bool { - let products = await Purchases.shared.products([option.id]) + return try await subscribe(productId: option.id) + } + + public func subscribe(option: PricingOption) async throws -> Bool { + return try await subscribe(productId: option.rawValue) + } + + private func subscribe(productId: String) async throws -> Bool { + let products = await Purchases.shared.products([productId]) guard let product = products.first else { throw AccountError.emptyProducts @@ -227,15 +256,18 @@ public final class AccountService: AccountServiceProtocol { return try await Purchases.shared.restorePurchases() } - public func loginTestAccount(token: String) throws { + public func loginTestAccount(token: String) async throws { + let userId = "001918.a2d23624056d45618b7c2699d98c535e.2333" self.updateAccount( - id: "001918.a2d23624056d45618b7c2699d98c535e.2333", + id: userId, email: "gcarlo89@hotmail.com", donationMade: true, hasSubscription: true ) try self.keychain.setAccessToken(token) + + _ = try await Purchases.shared.logIn(userId) } public func login( @@ -301,4 +333,20 @@ public final class AccountService: AccountServiceProtocol { return response.message } + + public func getSecondOnboarding() async throws -> T { + guard + let customerInfo = Purchases.shared.cachedCustomerInfo, + customerInfo.activeSubscriptions.isEmpty, + let countryCode = await Storefront.currentStorefront?.countryCode + else { + throw SecondOnboardingError.notApplicable + } + + return try await provider.request(.secondOnboarding( + anonymousId: customerInfo.id, + firstSeen: customerInfo.firstSeen.timeIntervalSince1970, + region: countryCode + )) + } } diff --git a/Shared/Services/Account/PricingModel.swift b/Shared/Services/Account/PricingModel.swift index f54094a83..d053f75e2 100644 --- a/Shared/Services/Account/PricingModel.swift +++ b/Shared/Services/Account/PricingModel.swift @@ -17,3 +17,86 @@ public struct PricingModel: Identifiable, Equatable { self.title = title } } + +public enum PricingOption: String, Identifiable, Codable { + public var id: Self { self } + + public static func parseValue(_ value: Int) -> Self? { + switch value { + case 3: + return .supportTier3 + case 4: + return .supportTier4 + case 5: + return .proMonthly + case 6: + return .supportTier6 + case 7: + return .supportTier7 + case 8: + return .supportTier8 + case 9: + return .supportTier9 + case 10: + return .supportTier10 + default: + return nil + } + } + + case proMonthly = "com.tortugapower.audiobookplayer.subscription.pro" + case proYearly = "com.tortugapower.audiobookplayer.subscription.pro.yearly" + case supportTier3 = "com.tortugapower.audiobookplayer.subscription.support.3" + case supportTier4 = "com.tortugapower.audiobookplayer.subscription.support.4" + case supportTier6 = "com.tortugapower.audiobookplayer.subscription.support.6" + case supportTier7 = "com.tortugapower.audiobookplayer.subscription.support.7" + case supportTier8 = "com.tortugapower.audiobookplayer.subscription.support.8" + case supportTier9 = "com.tortugapower.audiobookplayer.subscription.support.9" + case supportTier10 = "com.tortugapower.audiobookplayer.subscription.support.10" + + public var title: String { + switch self { + case .proMonthly: + return "$4.99" + case .proYearly: + return "$49.99" + case .supportTier3: + return "$2.99" + case .supportTier4: + return "$3.99" + case .supportTier6: + return "$5.99" + case .supportTier7: + return "$6.99" + case .supportTier8: + return "$7.99" + case .supportTier9: + return "$8.99" + case .supportTier10: + return "$9.99" + } + } + + public var cost: Double { + switch self { + case .proMonthly: + return 4.99 + case .proYearly: + return 49.99 + case .supportTier3: + return 2.99 + case .supportTier4: + return 3.99 + case .supportTier6: + return 5.99 + case .supportTier7: + return 6.99 + case .supportTier8: + return 7.99 + case .supportTier9: + return 8.99 + case .supportTier10: + return 9.99 + } + } +} diff --git a/Shared/Services/Events/EventsAPI.swift b/Shared/Services/Events/EventsAPI.swift new file mode 100644 index 000000000..514ae50bc --- /dev/null +++ b/Shared/Services/Events/EventsAPI.swift @@ -0,0 +1,36 @@ +// +// EventsAPI.swift +// BookPlayer +// +// Created by Gianni Carlo on 4/7/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import Foundation + +public enum EventsAPI { + case sendEvent(event: String, payload: [String: Any]) +} + +extension EventsAPI: Endpoint { + public var path: String { + switch self { + case .sendEvent: + return "/v1/user/events" + } + } + + public var method: HTTPMethod { + switch self { + case .sendEvent: + return .post + } + } + + public var parameters: [String: Any]? { + switch self { + case .sendEvent(let event, let payload): + return ["event": event, "event_data": payload] + } + } +} diff --git a/Shared/Services/Events/EventsService.swift b/Shared/Services/Events/EventsService.swift new file mode 100644 index 000000000..c7cb5f3e0 --- /dev/null +++ b/Shared/Services/Events/EventsService.swift @@ -0,0 +1,33 @@ +// +// EventsService.swift +// BookPlayer +// +// Created by Gianni Carlo on 4/7/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import Foundation + +public protocol EventsServiceProtocol { + func sendEvent(_ event: String, payload: [String: Any]) +} + +public class EventsService: EventsServiceProtocol, BPLogger { + let client: NetworkClientProtocol + private let provider: NetworkProvider + + public init(client: NetworkClientProtocol = NetworkClient()) { + self.client = client + self.provider = NetworkProvider(client: client) + } + + public func sendEvent(_ event: String, payload: [String : Any]) { + Task { + do { + let _: Empty = try await provider.request(.sendEvent(event: event, payload: payload)) + } catch { + Self.logger.trace("Failed to send event \(event), error: \(error.localizedDescription)") + } + } + } +} diff --git a/Shared/Services/Sync/SyncService.swift b/Shared/Services/Sync/SyncService.swift index 8754f7a8b..9835f3050 100644 --- a/Shared/Services/Sync/SyncService.swift +++ b/Shared/Services/Sync/SyncService.swift @@ -148,8 +148,7 @@ public final class SyncService: SyncServiceProtocol, BPLogger { func bindObservers() { NotificationCenter.default.publisher(for: .logout, object: nil) - .sink(receiveValue: { [weak self] _ in - self?.isActive = false + .sink(receiveValue: { _ in UserDefaults.standard.set( false, forKey: Constants.UserDefaults.hasScheduledLibraryContents diff --git a/Shared/Styleguide/Fonts.swift b/Shared/Styleguide/Fonts.swift index 6d0903964..ba79a3b30 100644 --- a/Shared/Styleguide/Fonts.swift +++ b/Shared/Styleguide/Fonts.swift @@ -11,7 +11,13 @@ import UIKit public struct Fonts { public static let title = UIFont.preferredFont(with: 16, style: .headline, weight: .semibold) public static let titleRegular = UIFont.preferredFont(with: 16, style: .headline, weight: .regular) + public static let titleLarge = UIFont.preferredFont(with: 20, style: .largeTitle, weight: .semibold) public static let body = UIFont.preferredFont(with: 14, style: .body, weight: .regular) public static let headline = UIFont.preferredFont(forTextStyle: .headline) public static let subheadline = UIFont.preferredFont(forTextStyle: .subheadline) + + public static let titleStory = UIFont.preferredFont(with: 24, style: .largeTitle, weight: .heavy) + public static let bodyStory = UIFont.preferredFont(with: 20, style: .title1, weight: .regular) + + public static let pricingTitle = UIFont.preferredFont(with: 40, style: .largeTitle, weight: .heavy) } diff --git a/Shared/Styleguide/Spacing.swift b/Shared/Styleguide/Spacing.swift index 94bb8ff51..f0d1cee48 100644 --- a/Shared/Styleguide/Spacing.swift +++ b/Shared/Styleguide/Spacing.swift @@ -9,6 +9,8 @@ import Foundation public struct Spacing { + /// Value: 2 + public static let S5: CGFloat = 2 /// Value: 4 public static let S4: CGFloat = 4 /// Value: 6 @@ -23,4 +25,6 @@ public struct Spacing { public static let M: CGFloat = 24 /// Value: 32 public static let L: CGFloat = 32 + /// Value: 44 + public static let L1: CGFloat = 44 }