diff --git a/AppExample/Example.xcodeproj/project.pbxproj b/AppExample/Example.xcodeproj/project.pbxproj index d0a2ce7..6f0490c 100644 --- a/AppExample/Example.xcodeproj/project.pbxproj +++ b/AppExample/Example.xcodeproj/project.pbxproj @@ -22,7 +22,6 @@ 840CD68E2AC0E39D00C6AAD0 /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840CD68D2AC0E39D00C6AAD0 /* ExampleApp.swift */; }; 840CD6902AC0E3A600C6AAD0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 840CD68F2AC0E3A600C6AAD0 /* Assets.xcassets */; }; 840CD6932AC0E3A600C6AAD0 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 840CD6922AC0E3A600C6AAD0 /* Preview Assets.xcassets */; }; - 840CD6AF2AC0E44E00C6AAD0 /* Factory in Frameworks */ = {isa = PBXBuildFile; productRef = 840CD6AE2AC0E44E00C6AAD0 /* Factory */; }; 840CD6B12AC0E6E200C6AAD0 /* Products.storekit in Resources */ = {isa = PBXBuildFile; fileRef = 840CD6B02AC0E6E200C6AAD0 /* Products.storekit */; }; 84664C312BF9FD6400A24148 /* Example (watchOS) Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 84664C302BF9FD6400A24148 /* Example (watchOS) Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 84664C362BF9FD6400A24148 /* Example__watchOS_App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84664C352BF9FD6400A24148 /* Example__watchOS_App.swift */; }; @@ -47,6 +46,7 @@ 848479602BF9FFA4003FC3FC /* OversizeOnboardingKit in Frameworks */ = {isa = PBXBuildFile; productRef = 8484795F2BF9FFA4003FC3FC /* OversizeOnboardingKit */; }; 848479622BF9FFA4003FC3FC /* OversizePhotoKit in Frameworks */ = {isa = PBXBuildFile; productRef = 848479612BF9FFA4003FC3FC /* OversizePhotoKit */; }; 848479652BFA0E64003FC3FC /* TestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848479642BFA0E64003FC3FC /* TestView.swift */; }; + 84A876512E129B0F00CC6DDA /* FactoryKit in Frameworks */ = {isa = PBXBuildFile; productRef = 84A876502E129B0F00CC6DDA /* FactoryKit */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -115,7 +115,7 @@ 848479622BF9FFA4003FC3FC /* OversizePhotoKit in Frameworks */, 848479542BF9FFA4003FC3FC /* OversizeCalendarKit in Frameworks */, 8484795E2BF9FFA4003FC3FC /* OversizeNotificationKit in Frameworks */, - 840CD6AF2AC0E44E00C6AAD0 /* Factory in Frameworks */, + 84A876512E129B0F00CC6DDA /* FactoryKit in Frameworks */, 848479582BF9FFA4003FC3FC /* OversizeKit in Frameworks */, 8484795A2BF9FFA4003FC3FC /* OversizeLocationKit in Frameworks */, 8484795C2BF9FFA4003FC3FC /* OversizeNoticeKit in Frameworks */, @@ -309,7 +309,6 @@ ); name = Example; packageProductDependencies = ( - 840CD6AE2AC0E44E00C6AAD0 /* Factory */, 848479532BF9FFA4003FC3FC /* OversizeCalendarKit */, 848479552BF9FFA4003FC3FC /* OversizeContactsKit */, 848479572BF9FFA4003FC3FC /* OversizeKit */, @@ -318,6 +317,7 @@ 8484795D2BF9FFA4003FC3FC /* OversizeNotificationKit */, 8484795F2BF9FFA4003FC3FC /* OversizeOnboardingKit */, 848479612BF9FFA4003FC3FC /* OversizePhotoKit */, + 84A876502E129B0F00CC6DDA /* FactoryKit */, ); productName = Example; productReference = 840CD6632AC0E39D00C6AAD0 /* Example.app */; @@ -834,11 +834,6 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 840CD6AE2AC0E44E00C6AAD0 /* Factory */ = { - isa = XCSwiftPackageProductDependency; - package = 840CD6AD2AC0E44E00C6AAD0 /* XCRemoteSwiftPackageReference "Factory" */; - productName = Factory; - }; 84664C462BF9FDC200A24148 /* Factory */ = { isa = XCSwiftPackageProductDependency; package = 840CD6AD2AC0E44E00C6AAD0 /* XCRemoteSwiftPackageReference "Factory" */; @@ -908,6 +903,11 @@ isa = XCSwiftPackageProductDependency; productName = OversizePhotoKit; }; + 84A876502E129B0F00CC6DDA /* FactoryKit */ = { + isa = XCSwiftPackageProductDependency; + package = 840CD6AD2AC0E44E00C6AAD0 /* XCRemoteSwiftPackageReference "Factory" */; + productName = FactoryKit; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 840CD6592AC0E39D00C6AAD0 /* Project object */; diff --git a/AppExample/Example.xcodeproj/project.xcworkspace/xcuserdata/admin.xcuserdatad/UserInterfaceState.xcuserstate b/AppExample/Example.xcodeproj/project.xcworkspace/xcuserdata/admin.xcuserdatad/UserInterfaceState.xcuserstate index df3e6d4..a0e6426 100644 Binary files a/AppExample/Example.xcodeproj/project.xcworkspace/xcuserdata/admin.xcuserdatad/UserInterfaceState.xcuserstate and b/AppExample/Example.xcodeproj/project.xcworkspace/xcuserdata/admin.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/AppExample/Example/ExampleApp.swift b/AppExample/Example/ExampleApp.swift index 32eedf3..b4eb754 100644 --- a/AppExample/Example/ExampleApp.swift +++ b/AppExample/Example/ExampleApp.swift @@ -3,7 +3,7 @@ // ExampleApp.swift, created on 25.09.2023 // -import Factory +import FactoryKit import OversizeKit import OversizeServices import OversizeUI diff --git a/AppExample/Example/Resources/AppConfig.plist b/AppExample/Example/Resources/AppConfig.plist index 55a1ad6..aad8e82 100644 --- a/AppExample/Example/Resources/AppConfig.plist +++ b/AppExample/Example/Resources/AppConfig.plist @@ -21,7 +21,7 @@ romanov.cc Email hello@oversize.app - Fecebook + Facebook Telegram @@ -54,7 +54,7 @@ Onboarding - Apperance + Appearance StoreKit @@ -68,7 +68,7 @@ Notifications - Lookscreen + Lockscreen Vibration @@ -92,7 +92,7 @@ BlurMinimize - Lookscreen + Lockscreen diff --git a/AppExample/Example/Screens/Main/MainView.swift b/AppExample/Example/Screens/Main/MainView.swift index 38db6a3..cc89ea9 100644 --- a/AppExample/Example/Screens/Main/MainView.swift +++ b/AppExample/Example/Screens/Main/MainView.swift @@ -3,7 +3,7 @@ // MainView.swift, created on 25.09.2023 // -import Factory +import FactoryKit import OversizeKit import OversizeLocalizable import OversizeServices diff --git a/AppExample/Example/Screens/Onboarding/OnboardingView.swift b/AppExample/Example/Screens/Onboarding/OnboardingView.swift index 19d572e..e1b165e 100644 --- a/AppExample/Example/Screens/Onboarding/OnboardingView.swift +++ b/AppExample/Example/Screens/Onboarding/OnboardingView.swift @@ -3,7 +3,7 @@ // OnboardingView.swift, created on 25.09.2023 // -import Factory +import FactoryKit import OversizeServices import OversizeUI import SwiftUI diff --git a/Package.swift b/Package.swift index ea43d46..a4f7399 100644 --- a/Package.swift +++ b/Package.swift @@ -4,7 +4,13 @@ import Foundation import PackageDescription -let remoteDependencies: [PackageDescription.Package.Dependency] = [ +let commonDependencies: [PackageDescription.Package.Dependency] = [ + .package(url: "https://github.com/lorenzofiamingo/swiftui-cached-async-image.git", .upToNextMajor(from: "2.1.1")), + .package(url: "https://github.com/hmlongco/Factory.git", .upToNextMajor(from: "2.1.3")), + .package(url: "https://github.com/hmlongco/Navigator.git", .upToNextMajor(from: "1.0.0")), +] + +let remoteDependencies: [PackageDescription.Package.Dependency] = commonDependencies + [ .package(url: "https://github.com/oversizedev/OversizeUI.git", .upToNextMajor(from: "3.0.2")), .package(url: "https://github.com/oversizedev/OversizeCore.git", .upToNextMajor(from: "1.3.0")), .package(url: "https://github.com/oversizedev/OversizeServices.git", .upToNextMajor(from: "1.4.0")), @@ -14,12 +20,10 @@ let remoteDependencies: [PackageDescription.Package.Dependency] = [ .package(url: "https://github.com/oversizedev/OversizeNetwork.git", .upToNextMajor(from: "1.0.0")), .package(url: "https://github.com/oversizedev/OversizeModels.git", .upToNextMajor(from: "0.1.0")), .package(url: "https://github.com/oversizedev/OversizeRouter.git", .upToNextMajor(from: "0.1.0")), - .package(url: "https://github.com/hmlongco/Factory.git", .upToNextMajor(from: "2.1.3")), - .package(url: "https://github.com/lorenzofiamingo/swiftui-cached-async-image.git", .upToNextMajor(from: "2.1.1")), - .package(url: "https://github.com/hmlongco/Navigator.git", .upToNextMajor(from: "1.0.0")), + .package(url: "https://github.com/oversizedev/OversizeNavigation.git", .upToNextMajor(from: "0.1.0")), ] -let localDependencies: [PackageDescription.Package.Dependency] = [ +let localDependencies: [PackageDescription.Package.Dependency] = commonDependencies + [ .package(name: "OversizeUI", path: "../OversizeUI"), .package(name: "OversizeServices", path: "../OversizeServices"), .package(name: "OversizeLocalizable", path: "../OversizeLocalizable"), @@ -29,9 +33,7 @@ let localDependencies: [PackageDescription.Package.Dependency] = [ .package(name: "OversizeNetwork", path: "../OversizeNetwork"), .package(name: "OversizeModels", path: "../OversizeModels"), .package(name: "OversizeRouter", path: "../OversizeRouter"), - .package(url: "https://github.com/lorenzofiamingo/swiftui-cached-async-image.git", .upToNextMajor(from: "2.1.1")), - .package(url: "https://github.com/hmlongco/Factory.git", .upToNextMajor(from: "2.1.3")), - .package(url: "https://github.com/hmlongco/Navigator.git", .upToNextMajor(from: "1.0.0")), + .package(name: "OversizeNavigation", path: "../OversizeNavigation"), ] let dependencies: [PackageDescription.Package.Dependency] = remoteDependencies @@ -73,6 +75,7 @@ let package = Package( .product(name: "FactoryKit", package: "Factory"), .product(name: "CachedAsyncImage", package: "swiftui-cached-async-image"), .product(name: "NavigatorUI", package: "Navigator"), + .product(name: "OversizeNavigation", package: "OversizeNavigation"), ] ), .target( diff --git a/README.md b/README.md index 9ee9667..5789881 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,120 @@ -# OversizeModules +# OversizeKit -A description of this package. +[![Swift 6.0](https://img.shields.io/badge/Swift-6.0-orange.svg?style=flat)](https://swift.org) +[![License](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/oversizedev/OversizeKit/blob/main/LICENSE) + +**OversizeKit** is a comprehensive Swift package that provides a collection of high-level UI components, services, and utilities for building modern SwiftUI applications. It's designed to accelerate iOS, macOS, tvOS, and watchOS app development with pre-built, customizable components and powerful functionality. + +## Features + +- **Rich UI Components** - Pre-built SwiftUI components for rapid development +- **Modular Architecture** - Use only what you need with granular module imports +- **Multi-Platform Support** - iOS, macOS, tvOS, and watchOS compatibility +- **Powerful Services** - Network, storage, location, and notification services +- **Ready-to-Use Kits** - Calendar, contacts, photo, and onboarding functionality +- **Modern Swift** - Built with Swift 6.0 and latest SwiftUI features + +## 🚀 Installation + +### Swift Package Manager + +Add OversizeKit to your project using Xcode or by adding it to your `Package.swift`: + +```swift +dependencies: [ + .package(url: "https://github.com/oversizedev/OversizeKit.git", .upToNextMajor(from: "2.4.2")) +] +``` + +Then add the specific modules you need to your target: + +```swift +.target( + name: "YourApp", + dependencies: [ + .product(name: "OversizeKit", package: "OversizeKit"), + .product(name: "OversizeCalendarKit", package: "OversizeKit"), + .product(name: "OversizeLocationKit", package: "OversizeKit"), + // Add other modules as needed + ] +) +``` + +### Xcode Integration + +1. Open your project in Xcode +2. Go to **File** → **Add Package Dependencies** +3. Enter the repository URL: `https://github.com/oversizedev/OversizeKit.git` +4. Select the modules you want to use +5. Click **Add Package** + +## 🏃‍♂️ Quick Start + +### Basic Setup + +```swift +import SwiftUI +import OversizeKit + +@main +struct MyApp: App { + var body: some Scene { + WindowGroup { + ContentView() + .appLaunch(onboarding: { + VStack { + Text("Welcome") + Button("Complete") { + appStateService.completedOnbarding() + } + } + }) + } + } +} +``` + +## 🎯 Examples + +### Example Application + +Check out the complete example application in the [`AppExample`](./AppExample) directory. It demonstrates: + +- Integration of multiple OversizeKit modules +- Best practices for app architecture +- Real-world usage scenarios +- Platform-specific implementations + +## 📋 Requirements + +### System Requirements + +- **iOS**: 17.0+ +- **macOS**: 14.0+ +- **tvOS**: 17.0+ +- **watchOS**: 10.0+ + +### Development Requirements + +- **Xcode**: 16.4+ +- **Swift**: 6.0+ + +### Dependencies + +OversizeKit uses the following external dependencies: + +- [Factory](https://github.com/hmlongco/Factory) - Dependency injection +- [Navigator](https://github.com/hmlongco/Navigator) - Advanced navigation +- [CachedAsyncImage](https://github.com/lorenzofiamingo/swiftui-cached-async-image) - Async image loading + +## License + +OversizeUI is released under the **MIT License**. See [LICENSE](LICENSE) for details. + +--- + +
+ +**Made with ❤️ by the Oversize** + +
diff --git a/Sources/OversizeKit/AdsKit/AdView.swift b/Sources/OversizeKit/AdsKit/AdView.swift index b1e82bb..38ef15f 100644 --- a/Sources/OversizeKit/AdsKit/AdView.swift +++ b/Sources/OversizeKit/AdsKit/AdView.swift @@ -24,7 +24,7 @@ public struct AdView: View { public var body: some View { switch viewModel.state { case .initial: - VStack {} + EmptyView() .task { if !isPremium { await viewModel.fetchAd() @@ -86,7 +86,7 @@ public struct AdView: View { .subheadline(.bold) .onSurfacePrimaryForeground() - Bage(color: .warning) { + Badge(color: .warning) { Text("Our app") .bold() } diff --git a/Sources/OversizeKit/DebugKit/DebugInfoView/DebugInfoView.swift b/Sources/OversizeKit/DebugKit/DebugInfoView/DebugInfoView.swift new file mode 100644 index 0000000..98e17a4 --- /dev/null +++ b/Sources/OversizeKit/DebugKit/DebugInfoView/DebugInfoView.swift @@ -0,0 +1,81 @@ +// +// Copyright © 2023 Alexander Romanov +// DebugInfoView.swift +// + +import OversizeCore +import OversizeLocalizable +import OversizeNavigation +import OversizeServices +import OversizeUI +import SwiftUI + +public struct DebugInfoView: View { + @StateObject private var viewModel = DebugInfoViewModel() + + public init() {} + + public var body: some View { + NavigationLayoutView("Information") { + contentView + } background: { + Color.backgroundSecondary + } + .toolbarTitleDisplayMode(.inline) + } + + var contentView: some View { + LeadingVStack { + SectionView("App State") { + Row( + "App Run Count", + subtitle: "\(viewModel.appStateService.appRunCount) runs" + ) + Separator() + Row( + "First Run Date", + subtitle: viewModel.appStateService.firstRunDate.formatted(date: .abbreviated, time: .standard) + ) + Separator() + Row("Last Run Date", + subtitle: viewModel.appStateService.lastRunDate.formatted(date: .abbreviated, time: .standard)) + Separator() + Row( + "App last Run Version", + subtitle: viewModel.appStateService.lastRunVersion + ) + } + .sectionContentCompactRowMargins() + .rowContentMargins(.init(horizontal: .medium, vertical: .xxxSmall)) + + SectionView("Review") { + Row( + "App launches for review", + subtitle: "Current: \(viewModel.appStateService.appRunCount), next request after \(viewModel.nextLaunchReviewCount) launches" + ) + Separator() + Row( + "Events to show review request count", + subtitle: "Current: \(viewModel.eventCount), next request after \(viewModel.nextEventReviewCount) events", + action: viewModel.onTapAddEvent + ) + Separator() + Row( + "App Review banner closed date", + subtitle: viewModel.reviewBannerClosedDate.formatted(date: .abbreviated, time: .standard) + ) + Separator() + Row( + "App Review estimate date", + subtitle: viewModel.reviewEstimateDate.formatted(date: .abbreviated, time: .standard) + ) + } + .sectionContentCompactRowMargins() + .rowContentMargins(.init(horizontal: .medium, vertical: .xxxSmall)) + } + } +} + +#Preview { + DebugInfoView() +} diff --git a/Sources/OversizeKit/DebugKit/DebugInfoView/DebugInfoViewModel.swift b/Sources/OversizeKit/DebugKit/DebugInfoView/DebugInfoViewModel.swift new file mode 100644 index 0000000..70b0b8b --- /dev/null +++ b/Sources/OversizeKit/DebugKit/DebugInfoView/DebugInfoViewModel.swift @@ -0,0 +1,57 @@ +// +// Copyright © 2022 Alexander Romanov +// DebugInfoViewModel.swift +// + +import OversizeCore +import OversizeNetwork +import OversizeServices +import OversizeStoreService +import OversizeUI +import SwiftUI +#if canImport(LocalAuthentication) +import LocalAuthentication +#endif +import FactoryKit + +@MainActor +public final class DebugInfoViewModel: ObservableObject { + @Injected(\.appStateService) var appStateService: AppStateService + @Injected(\.settingsService) var settingsService: SettingsServiceProtocol + @Injected(\.appStoreReviewService) var reviewService: AppStoreReviewService + + @Published var eventCount: Int = 0 + @Published var reviewBannerClosedDate: Date = .init() + @Published var reviewEstimateDate: Date = .init() + @Published var launchReviewCount: [Int] = [] + @Published var eventReviewCount: [Int] = [] + + public init() { + Task { + await loadReviewData() + } + } + + private func loadReviewData() async { + eventCount = await reviewService.appStoreReviewReceivedActionsCount + reviewBannerClosedDate = await reviewService.appReviewBannerClosedDate + reviewEstimateDate = await reviewService.appReviewEstimateDate + launchReviewCount = await reviewService.launchReviewCount + eventReviewCount = await reviewService.rewiewAfterEventCount + } + + var nextEventReviewCount: Int { + eventReviewCount.first { $0 > eventCount } ?? eventReviewCount.last ?? 0 + } + + var nextLaunchReviewCount: Int { + let currentCount = appStateService.appRunCount + return launchReviewCount.first { $0 > currentCount } ?? launchReviewCount.last ?? 0 + } + + func onTapAddEvent() { + Task { + await reviewService.actionEvent() + } + } +} diff --git a/Sources/OversizeKit/DebugKit/DebugMenuView/DebugMenuView.swift b/Sources/OversizeKit/DebugKit/DebugMenuView/DebugMenuView.swift new file mode 100644 index 0000000..d875355 --- /dev/null +++ b/Sources/OversizeKit/DebugKit/DebugMenuView/DebugMenuView.swift @@ -0,0 +1,47 @@ +// +// Copyright © 2023 Alexander Romanov +// DebugMenuView.swift +// + +import OversizeCore +import OversizeLocalizable +import OversizeNavigation +import OversizeServices +import OversizeUI +import SwiftUI + +public struct DebugMenuView: View { + @StateObject private var viewModel = DebugMenuViewModel() + + public init() {} + + public var body: some View { + NavigationLayoutView("Debug") { + contentView + } background: { + Color.backgroundSecondary + } + .toolbarTitleDisplayMode(.inline) + } + + var contentView: some View { + LeadingVStack { + SectionView { + Row( + "Rest onboarding", + action: viewModel.onTapRestOnboarding + ) + + Row( + "Rest app run count", + action: viewModel.onTapRestAppRunCount + ) + } + .sectionContentCompactRowMargins() + } + } +} + +#Preview { + DebugMenuView() +} diff --git a/Sources/OversizeKit/DebugKit/DebugMenuView/DebugMenuViewModel.swift b/Sources/OversizeKit/DebugKit/DebugMenuView/DebugMenuViewModel.swift new file mode 100644 index 0000000..7ca598f --- /dev/null +++ b/Sources/OversizeKit/DebugKit/DebugMenuView/DebugMenuViewModel.swift @@ -0,0 +1,30 @@ +// +// Copyright © 2022 Alexander Romanov +// DebugMenuViewModel.swift +// + +import OversizeCore +import OversizeNetwork +import OversizeServices +import OversizeStoreService +import OversizeUI +import SwiftUI +#if canImport(LocalAuthentication) +import LocalAuthentication +#endif +import FactoryKit + +@MainActor +public final class DebugMenuViewModel: ObservableObject { + @Injected(\.appStateService) var appStateService: AppStateService + + public init() {} + + func onTapRestOnboarding() { + appStateService.resetOnboarding() + } + + func onTapRestAppRunCount() { + appStateService.resetAppRunCount() + } +} diff --git a/Sources/OversizeKit/LauncherKit/Launcher.swift b/Sources/OversizeKit/LauncherKit/Launcher.swift index d1bcc57..aa1bdea 100644 --- a/Sources/OversizeKit/LauncherKit/Launcher.swift +++ b/Sources/OversizeKit/LauncherKit/Launcher.swift @@ -15,25 +15,23 @@ public struct Launcher: View { private var onboarding: Onboarding? private let content: Content - @StateObject private var viewModel = LauncherViewModel() + @StateObject private var viewModel: LauncherViewModel - public init(@ViewBuilder content: () -> Content) { + public init( + @ViewBuilder content: () -> Content, + firstRunAction: (() -> Void)? = nil, + appUpdateAction: (() -> Void)? = nil + ) { self.content = content() + _viewModel = StateObject(wrappedValue: LauncherViewModel( + firstRunAction: firstRunAction, + appUpdateAction: appUpdateAction + )) } public var body: some View { contentView - .onAppear { - viewModel.isShowSplashScreen = false -// #if DEBUG -// viewModel.appStateService.restOnbarding() -// viewModel.appStateService.restAppRunCount() -// #endif - viewModel.appStateService.appRun() - } - .task { - await viewModel.checkPremium() - } + .task(viewModel.onAppear) .appLaunchCover(item: $viewModel.activeFullScreenSheet) { fullScreenCover(sheet: $0) .systemServices() @@ -42,37 +40,23 @@ public struct Launcher: View { // .interactiveDismissDisabled(!viewModel.appStateService.isCompletedOnbarding) #endif } - .onChange(of: viewModel.appStateService.isCompletedOnbarding) { _, isCompletedOnbarding in - if isCompletedOnbarding, !viewModel.isPremium { - viewModel.setPayWall() - } else { - viewModel.activeFullScreenSheet = nil - } + .onChange(of: viewModel.appStateService.isCompletedOnboarding) { _, isCompletedOnbarding in + viewModel.onCompeteOnboarding(isCompletedOnbarding) } .onChange(of: scenePhase) { _, value in - switch value { - case .background: - viewModel.authState = .locked - viewModel.pinCodeField = "" - default: - break - } + viewModel.onScenePhaseChange(value) } } @ViewBuilder var contentView: some View { - if viewModel.isShowSplashScreen { - SplashScreen() - } else if viewModel.isShowLockscreen { + if viewModel.isShowLockscreen { lockscreenView } else { content - .onAppear { - Task { @MainActor in - await viewModel.reviewService.launchEvent() - } - viewModel.launcherSheetsChek() + .task { + await viewModel.reviewService.launchEvent() + await viewModel.launcherSheetsCheck() } } } @@ -81,7 +65,7 @@ public struct Launcher: View { private func fullScreenCover(sheet: LauncherViewModel.FullScreenSheet) -> some View { switch sheet { case .onboarding: onboarding - case .payWall: StoreInstuctinsView() + case .payWall: StoreInstructionsView() case .rate: RateAppScreen() case let .specialOffer(event): StoreSpecialOfferView(event: event) } @@ -93,17 +77,17 @@ public struct Launcher: View { state: $viewModel.authState, title: L10n.Security.enterPINCode, errorText: L10n.Security.invalidPIN, - pinCodeEnabled: viewModel.settingsService.pinCodeEnabend, + pinCodeEnabled: viewModel.settingsService.pinCodeEnabled, biometricEnabled: viewModel.settingsService.biometricEnabled, biometricType: viewModel.biometricService.biometricType ) { viewModel.checkPassword() } biometricAction: { - viewModel.appLockValidation() + viewModel.appBiometricUnlock() } .onAppear { if viewModel.settingsService.biometricEnabled, scenePhase != .background { - viewModel.appLockValidation() + viewModel.appBiometricUnlock() } } } @@ -116,8 +100,16 @@ public struct Launcher: View { } public extension Launcher where Onboarding == EmptyView { - init(@ViewBuilder content: () -> Content) { + init( + @ViewBuilder content: () -> Content, + firstRunAction: (() -> Void)? = nil, + appUpdateAction: (() -> Void)? = nil + ) { self.content = content() + _viewModel = StateObject(wrappedValue: LauncherViewModel( + firstRunAction: firstRunAction, + appUpdateAction: appUpdateAction + )) onboarding = nil } } @@ -135,6 +127,19 @@ public extension View { } .onboarding(onboarding: onboarding) } + + func appLaunch( + @ViewBuilder onboarding: @escaping () -> some View, + firstRun: (() -> Void)? = nil, + appUpdate: (() -> Void)? = nil + ) -> some View { + Launcher( + content: { self }, + firstRunAction: firstRun, + appUpdateAction: appUpdate + ) + .onboarding(onboarding: onboarding) + } } private extension View { diff --git a/Sources/OversizeKit/LauncherKit/LauncherViewModel.swift b/Sources/OversizeKit/LauncherKit/LauncherViewModel.swift index 55b1655..bdd4619 100644 --- a/Sources/OversizeKit/LauncherKit/LauncherViewModel.swift +++ b/Sources/OversizeKit/LauncherKit/LauncherViewModel.swift @@ -16,26 +16,30 @@ import FactoryKit @MainActor public final class LauncherViewModel: ObservableObject { - @Injected(\.biometricService) var biometricService + @Injected(\.biometricService) var biometricService: BiometricServiceProtocol @Injected(\.appStateService) var appStateService: AppStateService - @Injected(\.settingsService) var settingsService - @Injected(\.appStoreReviewService) var reviewService: AppStoreReviewServiceProtocol + @Injected(\.settingsService) var settingsService: SettingsServiceProtocol + @Injected(\.appStoreReviewService) var reviewService: AppStoreReviewService @Injected(\.storeKitService) private var storeKitService: StoreKitService @Injected(\.networkService) var networkService @AppStorage("AppState.PremiumState") var isPremium: Bool = false @AppStorage("AppState.SubscriptionsState") var subscriptionsState: RenewalState = .expired @AppStorage("AppState.LastClosedSpecialOfferSheet") var lastClosedSpecialOffer: Int = .init() + @AppStorage("AppState.AppBackgroundDate") var appBackgroundDate: Date = .init() @Published public var pinCodeField: String = "" @Published public var authState: LockscreenViewState = .locked @Published var activeFullScreenSheet: FullScreenSheet? @Published var isShowSplashScreen: Bool = true + @Published var isNeedAuthCheking = false - let expectedFormat = Date.ISO8601FormatStyle() + let firstRunAction: (() -> Void)? + + let appUpdateAction: (() -> Void)? var isShowLockscreen: Bool { if FeatureFlags.secure.lookscreen ?? false { - if settingsService.pinCodeEnabend || settingsService.biometricEnabled, authState != .unlocked { + if settingsService.pinCodeEnabled || settingsService.biometricEnabled, authState != .unlocked { true } else { false @@ -45,7 +49,13 @@ public final class LauncherViewModel: ObservableObject { } } - public init() {} + public init( + firstRunAction: (() -> Void)? = nil, + appUpdateAction: (() -> Void)? = nil + ) { + self.firstRunAction = firstRunAction + self.appUpdateAction = appUpdateAction + } } extension LauncherViewModel { @@ -67,32 +77,12 @@ extension LauncherViewModel { // Lockscreen public extension LauncherViewModel { - func launcherSheetsChek() { + func launcherSheetsCheck() async { checkOnboarding() - checkAppRate() + await checkAppRate() checkSpecialOffer() } - func checkPremium() async { - guard let appStoreID = Info.app.appStoreID else { - return - } - let productIds = await networkService.fetchAppStoreProductIds(appId: appStoreID).successResult ?? [] - - let status = await storeKitService.fetchPremiumAndSubscriptionsStatus(productIds: productIds) - if let premiumStatus = status.0 { - isPremium = premiumStatus - log("\(premiumStatus ? "👑 Premium status" : "🆓 Free status")") - } - - if let subscriptionStatus = status.1 { - if #available(iOS 15.4, macOS 12.3, *) { - log("📝 Subscription: \(subscriptionStatus.localizedDescription)") - } - subscriptionsState = subscriptionStatus - } - } - func checkPassword() { authState = .loading @@ -100,29 +90,34 @@ public extension LauncherViewModel { if self.pinCodeField == self.settingsService.getPINCode() { self.authState = .unlocked self.activeFullScreenSheet = nil + logSecurity("Unlocked by PIN") } else { self.authState = .error self.pinCodeField = "" + logError("PIN unlock failed") } } } - func appLockValidation() { + func appBiometricUnlock() { Task { let reason = "Auth in app" let authenticate = await biometricService.authenticating(reason: reason) if authenticate { authState = .unlocked activeFullScreenSheet = nil + logSecurity("Unlocked by biometric") } else { + logError("Biometric unlock failed") authState = .error } } } func checkOnboarding() { - if !appStateService.isCompletedOnbarding { + if !appStateService.isCompletedOnboarding { activeFullScreenSheet = .onboarding + logNotice("Onboarding shown") } } @@ -131,27 +126,15 @@ public extension LauncherViewModel { delay(time: 0.2) { Task { @MainActor in self.activeFullScreenSheet = .payWall + logNotice("Paywall shown") } } } - func checkAppRate() { - if reviewService.isShowReviewSheet, activeFullScreenSheet == nil { + func checkAppRate() async { + if await reviewService.isShowReviewSheet, activeFullScreenSheet == nil { activeFullScreenSheet = .rate - } - } - - func fetchAndSetSpecialOffer() async { - let result = await networkService.fetchSpecialOffers() - switch result { - case let .success(offers): - if let offer = offers.first(where: { checkDateInSelectedPeriod(startDate: $0.startDate, endDate: $0.endDate) }) { - if offer.id != lastClosedSpecialOffer { - activeFullScreenSheet = .specialOffer(event: offer) - } - } - case .failure: - break + logNotice("App rate shown") } } @@ -170,4 +153,92 @@ public extension LauncherViewModel { } } } + + @Sendable func onAppear() async { + isShowSplashScreen = false + if appStateService.appRunCount == 0 { + firstRunAction?() + } else if appStateService.lastRunVersion != Info.app.version { + appUpdateAction?() + } + + appStateService.appRun() + + await checkPremium() + } + + func onScenePhaseChange(_ scenePhase: ScenePhase) { + switch scenePhase { + case .background: + log("↩️ [STATE] App background") + appBackgroundDate = Date() + pinCodeField = "" + isNeedAuthCheking = true + case .active: + log("❇️ [STATE] App active") + if isNeedAuthCheking, appBackgroundDate.addingTimeInterval(settingsService.appLockTimeout) < Date() { + authState = .locked + isNeedAuthCheking = false + } + default: + break + } + } + + func onCompeteOnboarding(_ isCompletedOnbarding: Bool) { + if isCompletedOnbarding, !isPremium { + setPayWall() + } else { + activeFullScreenSheet = nil + } + } + + func checkPremium() async { + guard let appStoreID = Info.app.appStoreID else { + logError("Not found App Store ID in AppConfig.plist") + return + } + let productIdsResult = await networkService.fetchAppStoreProductIds(appId: appStoreID) + + guard let productIds = productIdsResult.successResult else { + logError("Not loaded product IDs") + return + } + + let status = await storeKitService.fetchPremiumAndSubscriptionsStatus(productIds: productIds) + + guard let premiumStatus = status.0 else { + logWarning("Could not fetch premium status") + return + } + + isPremium = premiumStatus + log("\(premiumStatus ? "👑 [INFO] Premium status" : "🆓 [INFO] Free status")") + + guard let subscriptionStatus = status.1 else { + logWarning("Could not fetch subscription status") + return + } + + if #available(iOS 15.4, macOS 12.3, *) { + logInfo("Subscription: \(subscriptionStatus.localizedDescription)") + } + subscriptionsState = subscriptionStatus + } + + func fetchAndSetSpecialOffer() async { + let result = await networkService.fetchSpecialOffers() + switch result { + case let .success(offers): + logSuccess("Offers loaded") + if let offer = offers.first(where: { checkDateInSelectedPeriod(startDate: $0.startDate, endDate: $0.endDate) }) { + if offer.id != lastClosedSpecialOffer { + activeFullScreenSheet = .specialOffer(event: offer) + logNotice("Offer shown") + } + } + case let .failure(error): + logError("Loading special offers failed", error: error) + } + } } diff --git a/Sources/OversizeKit/LauncherKit/RateAppScreen.swift b/Sources/OversizeKit/LauncherKit/RateAppScreen.swift index d02eb6f..91c4378 100644 --- a/Sources/OversizeKit/LauncherKit/RateAppScreen.swift +++ b/Sources/OversizeKit/LauncherKit/RateAppScreen.swift @@ -41,13 +41,17 @@ struct RateAppScreen: View { .buttonStyle(.primary(infinityWidth: false)) .accent() .simultaneousGesture(TapGesture().onEnded { - reviewService.estimate(goodRating: true) - dismiss() + Task { + await reviewService.estimate(goodRating: true) + dismiss() + } }) Button { - reviewService.estimate(goodRating: false) - dismiss() + Task { + await reviewService.estimate(goodRating: false) + dismiss() + } } label: { IconDeprecated(.thumbsDown, color: .onSurfacePrimary) } @@ -64,8 +68,10 @@ struct RateAppScreen: View { .padding(.xLarge) .overlay(alignment: .topTrailing) { Button { - reviewService.rewiewBunnerClosed() - dismiss() + Task { + await reviewService.reviewBannerClosed() + dismiss() + } } label: { IconDeprecated(.xMini, color: .onSurfacePrimary) } diff --git a/Sources/OversizeKit/SettingsKit/SettingsRouter/ResolveRouter.swift b/Sources/OversizeKit/SettingsKit/SettingsRouter/ResolveRouter.swift index 28fac47..186cd3a 100644 --- a/Sources/OversizeKit/SettingsKit/SettingsRouter/ResolveRouter.swift +++ b/Sources/OversizeKit/SettingsKit/SettingsRouter/ResolveRouter.swift @@ -3,15 +3,13 @@ // ResolveRouter.swift, created on 16.05.2024 // -import Foundation +import NavigatorUI import OversizeComponents -import OversizeLocalizable import OversizeNetwork -import OversizeRouter import SwiftUI -extension SettingsScreen: RoutableView { - public func view() -> some View { +extension SettingsDestinations: NavigationDestination { + public var body: some View { switch self { case .premium: StoreView() @@ -27,10 +25,12 @@ extension SettingsScreen: RoutableView { AboutView() case .feedback: FeedbackView() - case .ourResorses: - OurResorsesView() + .presentationDetents([.height(485)]) + case .ourResources: + OurResourcesView() case .support: SupportView() + .presentationDetents([.height(460)]) case .border: BorderSettingView() case .font: @@ -59,6 +59,19 @@ extension SettingsScreen: RoutableView { #else EmptyView() #endif + case .debugMenu: + DebugMenuView() + case .debugInfo: + DebugInfoView() + } + } + + public var method: NavigationMethod { + switch self { + case .webView, .sendMail, .updatePINCode, .setPINCode, .support, .feedback, .premium, .offer, .premiumFeature, .debugMenu, .debugInfo: + .managedSheet + default: + .push } } } diff --git a/Sources/OversizeKit/SettingsKit/SettingsRouter/Screens.swift b/Sources/OversizeKit/SettingsKit/SettingsRouter/Screens.swift deleted file mode 100644 index 57e5a1a..0000000 --- a/Sources/OversizeKit/SettingsKit/SettingsRouter/Screens.swift +++ /dev/null @@ -1,115 +0,0 @@ -// -// Copyright © 2024 Alexander Romanov -// SettingsScreen.swift, created on 15.04.2024 -// - -import OversizeComponents -import OversizeModels -import OversizeNetwork -import OversizeRouter -import SwiftUI - -public enum SettingsScreen: Routable { - case premium - case premiumFeature(feature: Components.Schemas.Feature) - case soundAndVibration - case appearance - case sync - case about - case feedback - case ourResorses - case support - case border - case font - case radius - case notifications - case setPINCode - case updatePINCode - case security - case offer(event: Components.Schemas.InAppPurchaseOffer) - case webView(url: URL) - case sendMail(to: String, subject: String, content: String) -} - -public extension SettingsScreen { - var id: String { - switch self { - case .premium: - "premium" - case .premiumFeature: - "premiumFeature" - case .soundAndVibration: - "soundAndVibration" - case .appearance: - "appearance" - case .sync: - "sync" - case .about: - "about" - case .feedback: - "feedback" - case .webView: - "webView" - case .ourResorses: - "ourResorses" - case .support: - "support" - case .border: - "border" - case .font: - "font" - case .radius: - "radius" - case .setPINCode: - "setPINCode" - case .updatePINCode: - "updatePINCode" - case .security: - "security" - case .offer: - "offer" - case .sendMail: - "sendMail" - case .notifications: - "notifications" - } - } -} - -// public struct SettingsNavigateAction { -// public typealias Action = (SettingsNavigationType) -> Void -// public let action: Action -// public func callAsFunction(_ navigationType: SettingsNavigationType) { -// action(navigationType) -// } -// } - -// public enum SettingsNavigationType { -// case move(SettingsScreen) -// case backToRoot -// case back(Int = 1) -// case present(_ sheet: SettingsScreen, detents: Set = [.large], indicator: Visibility = .hidden, dismissDisabled: Bool = false) -// case dismiss -// case dismissSheet -// case dismissFullScreenCover -// case dismissDisabled(_ isDismissDisabled: Bool = true) -// case presentHUD(_ text: String, type: HUDMessageType) -// } - -// -// public struct SettingsNavigateEnvironmentKey: EnvironmentKey { -// public static var defaultValue: SettingsNavigateAction = .init(action: { _ in }) -// } -// -// public extension EnvironmentValues { -// var settingsNavigate: SettingsNavigateAction { -// get { self[SettingsNavigateEnvironmentKey.self] } -// set { self[SettingsNavigateEnvironmentKey.self] = newValue } -// } -// } -// -// public extension View { -// func onSettingsNavigate(_ action: @escaping SettingsNavigateAction.Action) -> some View { -// environment(\.settingsNavigate, SettingsNavigateAction(action: action)) -// } -// } diff --git a/Sources/OversizeKit/SettingsKit/SettingsRouter/SettingsDestinations.swift b/Sources/OversizeKit/SettingsKit/SettingsRouter/SettingsDestinations.swift new file mode 100644 index 0000000..c9191e3 --- /dev/null +++ b/Sources/OversizeKit/SettingsKit/SettingsRouter/SettingsDestinations.swift @@ -0,0 +1,32 @@ +// +// Copyright © 2024 Alexander Romanov +// SettingsScreen.swift, created on 15.04.2024 +// + +import Foundation +import OversizeModels +import OversizeNetwork + +public enum SettingsDestinations: Hashable { + case premium + case premiumFeature(feature: Components.Schemas.Feature) + case soundAndVibration + case appearance + case sync + case about + case feedback + case ourResources + case support + case border + case font + case radius + case notifications + case setPINCode + case updatePINCode + case security + case offer(event: Components.Schemas.InAppPurchaseOffer) + case webView(url: URL) + case sendMail(to: String, subject: String, content: String) + case debugMenu + case debugInfo +} diff --git a/Sources/OversizeKit/SettingsKit/SettingsRouter/SettingsRouting.swift b/Sources/OversizeKit/SettingsKit/SettingsRouter/SettingsRouting.swift index 11aca22..654ec6c 100644 --- a/Sources/OversizeKit/SettingsKit/SettingsRouter/SettingsRouting.swift +++ b/Sources/OversizeKit/SettingsKit/SettingsRouter/SettingsRouting.swift @@ -3,18 +3,38 @@ // SettingsRouting.swift, created on 10.05.2024 // +import NavigatorUI import OversizeRouter import SwiftUI -public struct SettingsRoutingView: View { - public init() {} +public struct SettingsNavigationStack: View { + private let appSection: AppSection + private let headSection: HeadSection + + public init( + @ViewBuilder appSection: () -> AppSection, + @ViewBuilder headSection: () -> HeadSection + ) { + self.appSection = appSection() + self.headSection = headSection() + } public var body: some View { - RoutingView { - SettingsView { - EmptyView() - } + ManagedNavigationStack { + SettingsView( + appSection: { appSection }, + headSection: { headSection } + ) + .navigationDestinationAutoReceive(SettingsDestinations.self) } - .systemServices() + } +} + +public extension SettingsNavigationStack where HeadSection == EmptyView { + init(@ViewBuilder appSection: () -> AppSection) { + self.init( + appSection: appSection, + headSection: { EmptyView() } + ) } } diff --git a/Sources/OversizeKit/SettingsKit/SettingsRouter/SettingsRoutingView.swift b/Sources/OversizeKit/SettingsKit/SettingsRouter/SettingsRoutingView.swift deleted file mode 100644 index 5515416..0000000 --- a/Sources/OversizeKit/SettingsKit/SettingsRouter/SettingsRoutingView.swift +++ /dev/null @@ -1,123 +0,0 @@ -// -// Copyright © 2025 Alexander Romanov -// SettingsTabRoutingView.swift, created on 07.03.2025 -// - -import OversizeRouter -import OversizeUI -import SwiftUI - -public struct SettingsTabRoutingView: View { - @State private var router: TabRouter = .init( - selection: .general, - tabs: [ - SettingsTab.general, - SettingsTab.apperance, - // SettingsTab.syncrhonization, - SettingsTab.security, - // SettingsTab.about - ] - ) - - private var height: CGFloat { - switch router.selection { - case .general: - 450 - case .apperance: - 450 - case .syncrhonization: - 90 - case .security: - 220 - case .help: - 300 - case .about: - 600 - } - } - - private let appSettingsViewBuilder: () -> AppSettingsViewType - - public init(@ViewBuilder appSettingsViewBuilder: @escaping () -> AppSettingsViewType) { - self.appSettingsViewBuilder = appSettingsViewBuilder - } - - public var body: some View { - RoutingTabView(router: router) - .frame(width: 550, height: height) - .environment(\.appSettingsViewBuilder, makeViewBuilderBox()) - } - - private func makeViewBuilderBox() -> ViewBuilderBox { - ViewBuilderBox(appSettingsViewBuilder: { AnyView(appSettingsViewBuilder()) }) - } -} - -public class ViewBuilderBox { - public let appSettingsViewBuilder: () -> AnyView - - public init(appSettingsViewBuilder: @escaping () -> AnyView) { - self.appSettingsViewBuilder = appSettingsViewBuilder - } -} - -private struct AppSettingsViewBuilderKey: @preconcurrency EnvironmentKey { - @MainActor static let defaultValue = ViewBuilderBox(appSettingsViewBuilder: { AnyView(EmptyView()) }) -} - -public extension EnvironmentValues { - var appSettingsViewBuilder: ViewBuilderBox { - get { self[AppSettingsViewBuilderKey.self] } - set { self[AppSettingsViewBuilderKey.self] = newValue } - } -} - -extension SettingsTab: TabableView { - public func view() -> some View { - switch self { - case .general: - RoutingView { - SettingsGenericWrapper() - } - .systemServices() - case .security: - RoutingView { - SecuritySettingsView() - } - .systemServices() - case .help: - RoutingView { - SupportView() - } - .systemServices() - case .apperance: - RoutingView { - AppearanceSettingView() - } - .systemServices() - case .syncrhonization: - RoutingView { - iCloudSettingsView() - } - .systemServices() - case .about: - RoutingView { - AboutView() - } - .systemServices() - } - } -} - -// Generic wrapper to handle passing the AppSettingsView to SettingsView -public struct SettingsGenericWrapper: View { - @Environment(\.appSettingsViewBuilder) private var appSettingsViewBuilder - - public init() {} - - public var body: some View { - SettingsView { - appSettingsViewBuilder.appSettingsViewBuilder() - } - } -} diff --git a/Sources/OversizeKit/SettingsKit/SettingsRouter/SettingsTab.swift b/Sources/OversizeKit/SettingsKit/SettingsRouter/SettingsTab.swift deleted file mode 100644 index f04235a..0000000 --- a/Sources/OversizeKit/SettingsKit/SettingsRouter/SettingsTab.swift +++ /dev/null @@ -1,90 +0,0 @@ -// -// Copyright © 2025 Alexander Romanov -// SettingsTab.swift, created on 05.03.2025 -// - -import OversizeResources -import OversizeRouter -import OversizeUI -import SwiftUI - -public enum SettingsTab: Tabable { - case general - case apperance - case syncrhonization - case security - case help - case about -} - -public extension SettingsTab { - var icon: Image { - switch self { - case .general: - .init(systemName: "gearshape") - case .security: - .init(systemName: "checkmark.shield") - case .help: - .init(systemName: "questionmark.circle") - case .about: - .init(systemName: "person.circle") - case .apperance: - .init(systemName: "swatchpalette") - case .syncrhonization: - .init(systemName: "icloud") - } - } - - /* - var icon: Image { - switch self { - case .general: - Image.Base.Setting.mini - case .security: - Image.Base.lock - case .help: - Image.Alert.Help.circle - case .about: - Image.Base.Info.circle - case .apperance: - Image.Design.paintingPalette - case .syncrhonization: - Image.Weather.cloud2 - } - } - */ - - var title: String { - switch self { - case .general: - .init("General") - case .security: - .init("Security") - case .help: - .init("Help") - case .about: - .init("About") - case .apperance: - .init("Appearance") - case .syncrhonization: - .init("iCloud") - } - } - - var id: String { - switch self { - case .general: - "general" - case .security: - "security" - case .help: - "help" - case .about: - "about" - case .apperance: - "apperance" - case .syncrhonization: - "icloud" - } - } -} diff --git a/Sources/OversizeKit/SettingsKit/Views/About/About/AboutView.swift b/Sources/OversizeKit/SettingsKit/Views/About/About/AboutView.swift index 8dc0de3..461c754 100644 --- a/Sources/OversizeKit/SettingsKit/Views/About/About/AboutView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/About/About/AboutView.swift @@ -4,9 +4,11 @@ // import CachedAsyncImage +import NavigatorUI import OversizeComponents import OversizeCore import OversizeLocalizable +import OversizeNavigation import OversizeResources import OversizeRouter import OversizeServices @@ -19,7 +21,7 @@ import MessageUI #endif public struct AboutView: View { - @Environment(Router.self) var router + @Environment(\.navigator) var navigator @Environment(\.screenSize) var screenSize @Environment(\.iconStyle) var iconStyle: IconStyle @@ -66,14 +68,15 @@ public struct AboutView: View { public var body: some View { #if os(iOS) - Page(L10n.Settings.about) { + NavigationLayoutView(L10n.Settings.about) { list .surfaceContentRowMargins() .task { await viewModel.fetchApps() } + } background: { + Color.backgroundSecondary } - .backgroundSecondary() #else list @@ -221,7 +224,7 @@ public struct AboutView: View { #if os(iOS) if MFMailComposeViewController.canSendMail(), let mail = Info.links?.company.email, - let appVersion = Info.app.verstion, + let appVersion = Info.app.version, let appName = Info.app.name, let device = Info.app.device, let appBuild = Info.app.build, @@ -278,19 +281,19 @@ public struct AboutView: View { SectionView { VStack(spacing: .zero) { Row("Our open resources") { - router.move(.ourResorses) + navigator.navigate(to: SettingsDestinations.ourResources) } .rowArrow() if let privacyUrl = Info.url.appPrivacyPolicyUrl { Row(L10n.Store.privacyPolicy) { - router.present(.webView(url: privacyUrl)) + navigator.navigate(to: SettingsDestinations.webView(url: privacyUrl)) } } if let termsOfUde = Info.url.appTermsOfUseUrl { Row(L10n.Store.termsOfUse) { - router.present(.webView(url: termsOfUde)) + navigator.navigate(to: SettingsDestinations.webView(url: termsOfUde)) } } } @@ -475,7 +478,7 @@ public struct AboutView: View { if let authorLink = Info.links?.company.url { Link(destination: authorLink) { if let developerName = Info.developer.name, - let appVersion = Info.app.verstion, + let appVersion = Info.app.version, let appName = Info.app.name, let appBuild = Info.app.build { diff --git a/Sources/OversizeKit/SettingsKit/Views/About/FeedbackView.swift b/Sources/OversizeKit/SettingsKit/Views/About/FeedbackView.swift index e08a87d..0aa9e8f 100644 --- a/Sources/OversizeKit/SettingsKit/Views/About/FeedbackView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/About/FeedbackView.swift @@ -8,6 +8,7 @@ import MessageUI #endif import OversizeComponents import OversizeLocalizable +import OversizeNavigation import OversizeResources import OversizeRouter import OversizeServices @@ -15,11 +16,10 @@ import OversizeUI import SwiftUI public struct FeedbackView: View { - @Environment(Router.self) var router public init() {} public var body: some View { - Page("Feedback") { + NavigationLayoutView("Feedback") { VStack(spacing: .large) { SectionView { FeedbackViewRows() @@ -29,17 +29,10 @@ public struct FeedbackView: View { hero .padding(.bottom, .medium) } + } background: { + Color.backgroundSecondary } - .backgroundSecondary() - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button { - router.dismiss() - } label: { - Image.Base.close.icon() - } - } - } + .toolbarTitleDisplayMode(.inline) } private var hero: some View { @@ -50,7 +43,7 @@ public struct FeedbackView: View { struct FeedbackViewRows: View { @Environment(\.iconStyle) var iconStyle: IconStyle - @Environment(Router.self) var router + @Environment(\.navigator) var navigator var body: some View { LeadingVStack { @@ -66,7 +59,7 @@ struct FeedbackViewRows: View { #if os(iOS) if MFMailComposeViewController.canSendMail(), let mail = Info.links?.company.email, - let appVersion = Info.app.verstion, + let appVersion = Info.app.version, let appName = Info.app.name, let device = Info.app.device, let appBuild = Info.app.build, @@ -76,7 +69,11 @@ struct FeedbackViewRows: View { let subject = "Feedback" Row(L10n.Settings.feedbakAuthor) { - router.present(.sendMail(to: mail, subject: subject, content: contentPreText)) + navigator.navigate(to: SettingsDestinations.sendMail( + to: mail, + subject: subject, + content: contentPreText + )) } leading: { mailIcon.icon() } @@ -94,7 +91,7 @@ struct FeedbackViewRows: View { #elseif os(macOS) if let mail = Info.links?.company.email, - let appVersion = Info.app.verstion, + let appVersion = Info.app.version, let appName = Info.app.name, let appBuild = Info.app.build, let systemVersion = Info.app.system diff --git a/Sources/OversizeKit/SettingsKit/Views/About/OurResorsesView.swift b/Sources/OversizeKit/SettingsKit/Views/About/OurResourcesView.swift similarity index 89% rename from Sources/OversizeKit/SettingsKit/Views/About/OurResorsesView.swift rename to Sources/OversizeKit/SettingsKit/Views/About/OurResourcesView.swift index 991ca77..ae218cc 100644 --- a/Sources/OversizeKit/SettingsKit/Views/About/OurResorsesView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/About/OurResourcesView.swift @@ -1,6 +1,6 @@ // // Copyright © 2022 Alexander Romanov -// OurResorsesView.swift +// OurResourcesView.swift // #if canImport(MessageUI) @@ -8,22 +8,23 @@ import MessageUI #endif import OversizeComponents import OversizeLocalizable +import OversizeNavigation import OversizeResources import OversizeServices import OversizeUI import SwiftUI -public struct OurResorsesView: View { +public struct OurResourcesView: View { @Environment(\.iconStyle) var iconStyle: IconStyle public init() {} public var body: some View { - Page("Our open resources") { + NavigationLayoutView("Our open resources") { links .surfaceContentRowMargins() + } background: { + Color.backgroundSecondary } - - .backgroundSecondary() } private var links: some View { diff --git a/Sources/OversizeKit/SettingsKit/Views/About/SupportView.swift b/Sources/OversizeKit/SettingsKit/Views/About/SupportView.swift index 0481515..3fca004 100644 --- a/Sources/OversizeKit/SettingsKit/Views/About/SupportView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/About/SupportView.swift @@ -8,6 +8,7 @@ import MessageUI #endif import OversizeComponents import OversizeLocalizable +import OversizeNavigation import OversizeResources import OversizeRouter import OversizeServices @@ -15,31 +16,22 @@ import OversizeUI import SwiftUI public struct SupportView: View { - @Environment(Router.self) var router + @Environment(\.navigator) var navigator @Environment(\.iconStyle) var iconStyle: IconStyle public init() {} public var body: some View { - Page(L10n.Settings.supportSection) { + NavigationLayoutView(L10n.Settings.supportSection) { VStack(spacing: .large) { help hero .padding(.bottom, .medium) } + } background: { + Color.backgroundSecondary } - .backgroundSecondary() - #if !os(macOS) - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button { - router.dismissSheet() - } label: { - Image.Base.close.icon() - } - } - } - #endif + .toolbarTitleDisplayMode(.inline) } private var hero: some View { @@ -53,7 +45,7 @@ public struct SupportView: View { #if os(iOS) if MFMailComposeViewController.canSendMail(), let mail = Info.links?.company.email, - let appVersion = Info.app.verstion, + let appVersion = Info.app.version, let appName = Info.app.name, let device = Info.app.device, let appBuild = Info.app.build, @@ -63,7 +55,14 @@ public struct SupportView: View { let subject = "Support" Row("Contact Us") { - router.present(.sendMail(to: mail, subject: subject, content: contentPreText)) + navigator.navigate( + to: SettingsDestinations.sendMail( + to: mail, + subject: subject, + content: contentPreText + ) + ) + } leading: { mailIcon.icon() } diff --git a/Sources/OversizeKit/SettingsKit/Views/Apperance/AppearanceSettingView.swift b/Sources/OversizeKit/SettingsKit/Views/Appearance/AppearanceSettingView.swift similarity index 92% rename from Sources/OversizeKit/SettingsKit/Views/Apperance/AppearanceSettingView.swift rename to Sources/OversizeKit/SettingsKit/Views/Appearance/AppearanceSettingView.swift index 5a75c8e..c12371f 100644 --- a/Sources/OversizeKit/SettingsKit/Views/Apperance/AppearanceSettingView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/Appearance/AppearanceSettingView.swift @@ -5,13 +5,14 @@ import OversizeCore import OversizeLocalizable +import OversizeNavigation import OversizeRouter import OversizeServices import OversizeUI import SwiftUI public struct AppearanceSettingView: View { - @Environment(Router.self) var router + @Environment(\.navigator) var navigator @Environment(\.theme) private var theme: ThemeSettings @Environment(\.iconStyle) var iconStyle: IconStyle @Environment(\.isPremium) var isPremium: Bool @@ -27,16 +28,17 @@ public struct AppearanceSettingView: View { public init() {} public var body: some View { - Page(L10n.Settings.apperance) { + NavigationLayoutView(L10n.Settings.apperance) { settings .surfaceContentRowMargins() + } background: { + Color.backgroundSecondary } - .backgroundSecondary() } private var settings: some View { LazyVStack(alignment: .leading, spacing: 0) { - apperance + appearance .padding(.top, .xxxSmall) #if os(iOS) @@ -68,7 +70,7 @@ public struct AppearanceSettingView: View { .preferredColorScheme(theme.appearance.colorScheme) } - private var apperance: some View { + private var appearance: some View { SectionView { HStack { ForEach(Appearance.allCases, id: \.self) { appearance in @@ -133,7 +135,9 @@ public struct AppearanceSettingView: View { ) .onTapGesture { if index != 0, isPremium == false { - router.present(.premium) + navigator.navigate( + to: SettingsDestinations.premium) + } else { let defaultIconIndex = iconSettings.iconNames .firstIndex(of: UIApplication.shared.alternateIconName) ?? 0 @@ -162,7 +166,7 @@ public struct AppearanceSettingView: View { SectionView("Advanced settings") { VStack(spacing: .zero) { Row("Fonts") { - router.move(.font) + navigator.navigate(to: SettingsDestinations.font) } leading: { textIcon.icon() } @@ -172,7 +176,8 @@ public struct AppearanceSettingView: View { Switch(isOn: theme.$borderApp) { Row("Borders") { - router.move(.border) + navigator.navigate(to: SettingsDestinations.border) + } leading: { borderIcon.icon() } @@ -187,7 +192,8 @@ public struct AppearanceSettingView: View { } Row("Radius") { - router.move(.radius) + navigator.navigate(to: SettingsDestinations.radius) + } leading: { radiusIcon.icon() } diff --git a/Sources/OversizeKit/SettingsKit/Views/Apperance/BorderSettingView.swift b/Sources/OversizeKit/SettingsKit/Views/Appearance/BorderSettingView.swift similarity index 93% rename from Sources/OversizeKit/SettingsKit/Views/Apperance/BorderSettingView.swift rename to Sources/OversizeKit/SettingsKit/Views/Appearance/BorderSettingView.swift index 3a16be0..e948e38 100644 --- a/Sources/OversizeKit/SettingsKit/Views/Apperance/BorderSettingView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/Appearance/BorderSettingView.swift @@ -3,6 +3,7 @@ // BorderSettingView.swift // +import OversizeNavigation import OversizeUI import SwiftUI @@ -12,18 +13,12 @@ public struct BorderSettingView: View { public init() {} public var body: some View { - PageView("Borders in app") { + NavigationLayoutView("Borders in app") { settings .surfaceContentRowMargins() + } background: { + Color.backgroundSecondary } - .leadingBar { - // if !isPortrait, verticalSizeClass == .regular { - // EmptyView() - // } else { - BarButton(.back) - // } - } - .backgroundSecondary() } private var settings: some View { diff --git a/Sources/OversizeKit/SettingsKit/Views/Apperance/FontSettingView.swift b/Sources/OversizeKit/SettingsKit/Views/Appearance/FontSettingView.swift similarity index 99% rename from Sources/OversizeKit/SettingsKit/Views/Apperance/FontSettingView.swift rename to Sources/OversizeKit/SettingsKit/Views/Appearance/FontSettingView.swift index 8112526..ba77c3c 100644 --- a/Sources/OversizeKit/SettingsKit/Views/Apperance/FontSettingView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/Appearance/FontSettingView.swift @@ -3,6 +3,7 @@ // FontSettingView.swift // +import OversizeNavigation import OversizeUI import SwiftUI diff --git a/Sources/OversizeKit/SettingsKit/Views/Apperance/RadiusSettingView.swift b/Sources/OversizeKit/SettingsKit/Views/Appearance/RadiusSettingView.swift similarity index 92% rename from Sources/OversizeKit/SettingsKit/Views/Apperance/RadiusSettingView.swift rename to Sources/OversizeKit/SettingsKit/Views/Appearance/RadiusSettingView.swift index e7129e7..4fd31c1 100644 --- a/Sources/OversizeKit/SettingsKit/Views/Apperance/RadiusSettingView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/Appearance/RadiusSettingView.swift @@ -3,6 +3,7 @@ // RadiusSettingView.swift // +import OversizeNavigation import OversizeUI import SwiftUI @@ -12,14 +13,12 @@ public struct RadiusSettingView: View { public init() {} public var body: some View { - PageView("Radius") { + NavigationLayoutView("Radius") { settings .surfaceContentRowMargins() + } background: { + Color.backgroundSecondary } - .leadingBar { - BarButton(.back) - } - .backgroundSecondary() } private var settings: some View { diff --git a/Sources/OversizeKit/SettingsKit/Views/Notifications/NotificationsSettingsView.swift b/Sources/OversizeKit/SettingsKit/Views/Notifications/NotificationsSettingsView.swift index 66cc4c8..bf25bb1 100644 --- a/Sources/OversizeKit/SettingsKit/Views/Notifications/NotificationsSettingsView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/Notifications/NotificationsSettingsView.swift @@ -4,6 +4,7 @@ // import OversizeLocalizable +import OversizeNavigation import OversizeServices import OversizeUI import SwiftUI @@ -16,11 +17,12 @@ public struct NotificationsSettingsView: View { public init() {} public var body: some View { - Page(L10n.Settings.notifications) { + NavigationLayoutView(L10n.Settings.notifications) { soundsAndVibrations .surfaceContentRowMargins() + } background: { + Color.backgroundSecondary } - .backgroundSecondary() } } diff --git a/Sources/OversizeKit/SettingsKit/Views/Security/PINCode/SetPINCodeView.swift b/Sources/OversizeKit/SettingsKit/Views/Security/PINCode/SetPINCodeView.swift index a2710de..4219173 100644 --- a/Sources/OversizeKit/SettingsKit/Views/Security/PINCode/SetPINCodeView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/Security/PINCode/SetPINCodeView.swift @@ -4,13 +4,14 @@ // import OversizeLocalizable +import OversizeNavigation import OversizeRouter import OversizeUI import SwiftUI public struct SetPINCodeView: View { - @Environment(Router.self) var router - @Environment(HUDRouter.self) private var hudRouter: HUDRouter + @Environment(\.navigator) var navigator + // @Environment(HUDRouter.self) private var hudRouter: HUDRouter @ObservedObject var viewModel: SetPINCodeViewModel @Environment(\.dismiss) var dismiss @@ -23,7 +24,7 @@ public struct SetPINCodeView: View { .toolbar { ToolbarItem(placement: .cancellationAction) { Button { - router.dismiss() + navigator.dismiss() } label: { Image.Base.close.icon() } @@ -42,7 +43,7 @@ public struct SetPINCodeView: View { title: L10n.Security.oldPINCode, errorText: viewModel.errorText ) { - viewModel.chekOldPINCode() + viewModel.checkOldPINCode() } biometricAction: {} case .newPINField: @@ -71,9 +72,11 @@ public struct SetPINCodeView: View { dismiss() switch viewModel.action { case .set: - hudRouter.present(L10n.Security.createPINCode) + break + // hudRouter.present(L10n.Security.createPINCode) case .update: - hudRouter.present(L10n.Security.pinChanged) + break + // hudRouter.present(L10n.Security.pinChanged) } case false: diff --git a/Sources/OversizeKit/SettingsKit/Views/Security/PINCode/SetPINCodeViewModel.swift b/Sources/OversizeKit/SettingsKit/Views/Security/PINCode/SetPINCodeViewModel.swift index 3ef7f19..ce5b24a 100644 --- a/Sources/OversizeKit/SettingsKit/Views/Security/PINCode/SetPINCodeViewModel.swift +++ b/Sources/OversizeKit/SettingsKit/Views/Security/PINCode/SetPINCodeViewModel.swift @@ -52,7 +52,7 @@ public final class SetPINCodeViewModel: ObservableObject { } } - public func chekOldPINCode() { + public func checkOldPINCode() { if oldCodeField != curentPinCode { authState = .error errorText = L10n.Security.invalidCurrentPINCode @@ -74,7 +74,7 @@ public final class SetPINCodeViewModel: ObservableObject { let result = await settingsStore.updatePINCode(oldPIN: curentPinCode, newPIN: newPinCodeField) switch result { case true: - settingsStore.pinCodeEnabend = true + settingsStore.pinCodeEnabled = true TapticEngine.success.vibrate() return true case false: diff --git a/Sources/OversizeKit/SettingsKit/Views/Security/SecuritySettingsView.swift b/Sources/OversizeKit/SettingsKit/Views/Security/SecuritySettingsView.swift index b2137e8..57ae67d 100644 --- a/Sources/OversizeKit/SettingsKit/Views/Security/SecuritySettingsView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/Security/SecuritySettingsView.swift @@ -5,6 +5,7 @@ import FactoryKit import OversizeLocalizable +import OversizeNavigation import OversizeRouter import OversizeServices import OversizeUI @@ -14,17 +15,19 @@ import SwiftUI public struct SecuritySettingsView: View { @Injected(\.biometricService) var biometricService - @Environment(Router.self) var router + @Environment(\.navigator) var navigator @StateObject var settingsService = SettingsService() + let min: [Double] = [60, 120, 300, 600] public init() {} public var body: some View { - Page(L10n.Security.title) { + NavigationLayoutView(L10n.Security.title) { iOSSettings .surfaceContentRowMargins() + } background: { + Color.backgroundSecondary } - .backgroundSecondary() } } @@ -33,7 +36,7 @@ extension SecuritySettingsView { VStack(alignment: .center, spacing: 0) { faceID - // additionally + additionally } } } @@ -67,13 +70,12 @@ extension SecuritySettingsView { if FeatureFlags.secure.lookscreen.valueOrFalse { Switch(isOn: Binding(get: { - settingsService.pinCodeEnabend + settingsService.pinCodeEnabled }, set: { - if settingsService.isSetedPinCode() { - settingsService.pinCodeEnabend = $0 + if settingsService.isSetPinCode() { + settingsService.pinCodeEnabled = $0 } else { - router.present(.setPINCode) - + navigator.navigate(to: SettingsDestinations.setPINCode) } }) ) { @@ -82,9 +84,9 @@ extension SecuritySettingsView { } } - if settingsService.isSetedPinCode() { + if settingsService.isSetPinCode() { Row(L10n.Security.changePINCode) { - router.present(.updatePINCode) + navigator.navigate(to: SettingsDestinations.updatePINCode) } .rowArrow() } @@ -102,11 +104,11 @@ extension SecuritySettingsView { private var additionally: some View { SectionView(L10n.Settings.additionally) { VStack(spacing: .zero) { -// if FeatureFlags.secure.lookscreen.valueOrFalse { +// if FeatureFlags.secure.lockscreen.valueOrFalse { // Row(L10n.Security.inactiveAskPassword, trallingType: .toggle(isOn: $settingsStore.askPasswordWhenInactiveEnabend)) // } // -// if FeatureFlags.secure.lookscreen.valueOrFalse { +// if FeatureFlags.secure.lockscreen.valueOrFalse { // Row(L10n.Security.minimizeAskPassword, trallingType: .toggle(isOn: $settingsStore.askPasswordAfterMinimizeEnabend)) // } @@ -120,7 +122,7 @@ extension SecuritySettingsView { // .onPremiumTap() // } -// if FeatureFlags.secure.lookscreen.valueOrFalse { +// if FeatureFlags.secure.lockscreen.valueOrFalse { // Row(L10n.Security.alertPINCode, trallingType: .toggle(isOn: $settingsStore.alertPINCodeEnabled)) // } // @@ -128,19 +130,43 @@ extension SecuritySettingsView { // Row(L10n.Security.photoBreaker, trallingType: .toggle(isOn: $settingsStore.photoBreakerEnabend)) // } // -// if FeatureFlags.secure.lookscreen.valueOrFalse { +// if FeatureFlags.secure.lockscreen.valueOrFalse { // Row(L10n.Security.facedownLock, trallingType: .toggle(isOn: $settingsStore.lookScreenDownEnabend)) // } // if FeatureFlags.secure.blurMinimize.valueOrFalse { - Switch(isOn: $settingsService.blurMinimizeEnabend) { + Switch(isOn: $settingsService.blurMinimizeEnabled) { Row(L10n.Security.blurMinimize) .premium() } .onPremiumTap() } -// if FeatureFlags.secure.lookscreen.valueOrFalse { + if FeatureFlags.secure.lookscreen.valueOrFalse { + Switch(isOn: $settingsService.fastEnter) { + Row("Fast enter") + } + } + if settingsService.fastEnter { + Row("Time to enter", trailing: { + Picker("", selection: $settingsService.appLockTimeout) { + ForEach(0 ..< min.count, id: \.self) { // Non-constant range: argument must be an integer literal + let min = Int(self.min[$0] / 60) + + Text("\(min) \(OversizeLocalizable.L10n.Time.mins)") + .tag(self.min[$0]) + } + } + #if !os(macOS) + .pickerStyle(.navigationLink) + #endif + .labelsHidden() + .clipped() + }) + .rowArrow() + } + +// if FeatureFlags.secure.lockscreen.valueOrFalse { // Row(L10n.Security.authHistory, trallingType: .toggle(isOn: $settingsService.authHistoryEnabend)) // .premium() // .onPremiumTap() diff --git a/Sources/OversizeKit/SettingsKit/Views/SettingsView.swift b/Sources/OversizeKit/SettingsKit/Views/SettingsView.swift index af282b5..8b4445c 100644 --- a/Sources/OversizeKit/SettingsKit/Views/SettingsView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/SettingsView.swift @@ -4,6 +4,7 @@ // import OversizeLocalizable +import OversizeNavigation import OversizeResources import OversizeRouter import OversizeServices @@ -12,7 +13,7 @@ import SwiftUI // swiftlint:disable line_length public struct SettingsView: View { - @Environment(Router.self) var router + @Environment(\.navigator) var navigator @Environment(\.iconStyle) var iconStyle: IconStyle @Environment(\.theme) var theme: ThemeSettings @StateObject var settingsService = SettingsService() @@ -29,13 +30,16 @@ public struct SettingsView: View { } public var body: some View { - Page(L10n.Settings.title) { + NavigationLayoutView(L10n.Settings.title) { #if os(iOS) iOSSettings #else macSettings #endif - }.backgroundSecondary() + } background: { + Color.backgroundSecondary + } + .toolbarTitleDisplayMode(.inline) } } @@ -44,16 +48,19 @@ public struct SettingsView: View { extension SettingsView { private var iOSSettings: some View { VStack(alignment: .center, spacing: 0) { - if let stoteKit = FeatureFlags.app.storeKit { - if stoteKit { + if let storeKit = FeatureFlags.app.storeKit { + if storeKit { SectionView { - PrmiumBannerRow() + PremiumBannerRow() } .surfaceContentMargins(.zero) } } Group { app + #if DEBUG + debug + #endif help about } @@ -74,18 +81,18 @@ extension SettingsView { private var app: some View { SectionView("General") { VStack(spacing: .zero) { - if FeatureFlags.app.apperance.valueOrFalse { + if FeatureFlags.app.appearance.valueOrFalse { Row(L10n.Settings.apperance) { - router.move(.appearance) + navigator.navigate(to: SettingsDestinations.appearance) } leading: { - apperanceSettingsIcon.icon() + appearanceSettingsIcon.icon() } .rowArrow() } if FeatureFlags.app.сloudKit.valueOrFalse || FeatureFlags.app.healthKit.valueOrFalse { Row(L10n.Title.synchronization) { - router.move(.sync) + navigator.navigate(to: SettingsDestinations.sync) } leading: { cloudKitIcon.icon() } @@ -101,7 +108,8 @@ extension SettingsView { || FeatureFlags.secure.photoBreaker.valueOrFalse { Row(L10n.Security.title) { - router.move(.security) + navigator.navigate(to: SettingsDestinations.security) + } leading: { securityIcon.icon() } @@ -110,7 +118,8 @@ extension SettingsView { if FeatureFlags.app.sounds.valueOrFalse || FeatureFlags.app.vibration.valueOrFalse { Row(soundsAndVibrationTitle) { - router.move(.soundAndVibration) + navigator.navigate(to: SettingsDestinations.soundAndVibration) + } leading: { FeatureFlags.app.sounds.valueOrFalse ? soundIcon.icon() : vibrationIcon.icon() } @@ -119,7 +128,8 @@ extension SettingsView { if FeatureFlags.app.notifications.valueOrFalse { Row(L10n.Settings.notifications) { - router.move(.notifications) + navigator.navigate(to: SettingsDestinations.notifications) + } leading: { notificationsIcon.icon() } @@ -136,7 +146,7 @@ extension SettingsView { appSection } - var apperanceSettingsIcon: Image { + var appearanceSettingsIcon: Image { switch iconStyle { case .line: Image.Design.paintingPalette @@ -208,7 +218,8 @@ extension SettingsView { VStack(alignment: .leading) { Row("Get help") { #if os(iOS) - router.present(.support, detents: [.medium]) + navigator.navigate(to: SettingsDestinations.support) + #endif } leading: { helpIcon.icon() @@ -218,7 +229,8 @@ extension SettingsView { Row("Send feedback") { #if os(iOS) - router.present(.feedback, detents: [.medium]) + navigator.navigate(to: SettingsDestinations.feedback) + #endif } leading: { chatIcon.icon() @@ -273,6 +285,32 @@ extension SettingsView { } } + #if DEBUG + var debugIcon: Image { + switch iconStyle { + case .line: + Image.Base.game + case .fill: + Image.Base.Game.fill + case .twoTone: + Image.Base.Game.twoTone + } + } + #endif + + #if DEBUG + var debugInfoIcon: Image { + switch iconStyle { + case .line: + Image.Base.document + case .fill: + Image.Base.Document.fill + case .twoTone: + Image.Base.Document.twoTone + } + } + #endif + var oversizeIcon: Image { switch iconStyle { case .line: @@ -295,11 +333,34 @@ extension SettingsView { } } + #if DEBUG + private var debug: some View { + SectionView { + VStack(spacing: .zero) { + Row("Debug") { + navigator.navigate(to: SettingsDestinations.debugMenu) + } leading: { + debugIcon.icon() + } + .rowArrow() + + Row("Information") { + navigator.navigate(to: SettingsDestinations.debugInfo) + } leading: { + debugInfoIcon.icon() + } + .rowArrow() + } + .buttonStyle(.row) + } + } + #endif + private var about: some View { SectionView { VStack(spacing: .zero) { Row(L10n.Settings.about) { - router.move(.about) + navigator.navigate(to: SettingsDestinations.about) } leading: { infoIcon.icon() } @@ -326,10 +387,10 @@ extension SettingsView { @available(macOS 13.0, *) private var macSettings: some View { VStack(alignment: .center, spacing: 0) { - if let stoteKit = FeatureFlags.app.storeKit { - if stoteKit { + if let storeKit = FeatureFlags.app.storeKit { + if storeKit { SectionView { - PrmiumBannerRow() + PremiumBannerRow() } .surfaceContentMargins(.zero) } @@ -337,6 +398,10 @@ extension SettingsView { macGeneral + #if DEBUG + debug + #endif + SectionView("Feedback") { FeedbackViewRows() } diff --git a/Sources/OversizeKit/SettingsKit/Views/SoundAndVibration/SoundsAndVibrationsSettingsView.swift b/Sources/OversizeKit/SettingsKit/Views/SoundAndVibration/SoundsAndVibrationsSettingsView.swift index fa1549c..c3bc5c1 100644 --- a/Sources/OversizeKit/SettingsKit/Views/SoundAndVibration/SoundsAndVibrationsSettingsView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/SoundAndVibration/SoundsAndVibrationsSettingsView.swift @@ -5,6 +5,7 @@ import OversizeCore import OversizeLocalizable +import OversizeNavigation import OversizeServices import OversizeUI import SwiftUI @@ -16,11 +17,12 @@ public struct SoundsAndVibrationsSettingsView: View { public init() {} public var body: some View { - Page(title) { + NavigationLayoutView(title) { iOSSettings .surfaceContentRowMargins() + } background: { + Color.backgroundSecondary } - .backgroundSecondary() } private var title: String { diff --git a/Sources/OversizeKit/SettingsKit/Views/iCloud/iCloudSettingsView.swift b/Sources/OversizeKit/SettingsKit/Views/iCloud/iCloudSettingsView.swift index b8042c3..ffc63b3 100644 --- a/Sources/OversizeKit/SettingsKit/Views/iCloud/iCloudSettingsView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/iCloud/iCloudSettingsView.swift @@ -4,23 +4,24 @@ // import OversizeLocalizable +import OversizeNavigation import OversizeServices import OversizeUI import SwiftUI // swiftlint:disable line_length type_name - public struct iCloudSettingsView: View { // Synchronization @StateObject var settingsService = SettingsService() public init() {} public var body: some View { - Page(L10n.Title.synchronization) { + NavigationLayoutView(L10n.Title.synchronization) { iOSSettings .surfaceContentRowMargins() + } background: { + Color.backgroundSecondary } - .backgroundSecondary() } } diff --git a/Sources/OversizeKit/StoreKit/StoreScreen/AppStoreProduct/AppStoreProductViewController.swift b/Sources/OversizeKit/StoreKit/StoreScreen/AppStoreProduct/AppStoreProductViewController.swift index 5b63ee2..0c1d88f 100644 --- a/Sources/OversizeKit/StoreKit/StoreScreen/AppStoreProduct/AppStoreProductViewController.swift +++ b/Sources/OversizeKit/StoreKit/StoreScreen/AppStoreProduct/AppStoreProductViewController.swift @@ -42,18 +42,17 @@ public class AppStoreProductViewController: UIViewController { let parameters = [SKStoreProductParameterITunesItemIdentifier: appId] storeProductViewController.loadProduct(withParameters: parameters) { status, error in - if status { - self.present(storeProductViewController, animated: true, completion: nil) - } else { - if let error { - print("Error: \(error.localizedDescription)") + DispatchQueue.main.async { + if status { + self.present(storeProductViewController, animated: true, completion: nil) + } else { + if let error { + print("Error: \(error.localizedDescription)") + } } + self.isPresentStoreProduct.wrappedValue = false } } - - DispatchQueue.main.async { - self.isPresentStoreProduct.wrappedValue = false - } } } diff --git a/Sources/OversizeKit/StoreKit/StoreScreen/StoreInstuctinsView.swift b/Sources/OversizeKit/StoreKit/StoreScreen/StoreInstructionsView.swift similarity index 90% rename from Sources/OversizeKit/StoreKit/StoreScreen/StoreInstuctinsView.swift rename to Sources/OversizeKit/StoreKit/StoreScreen/StoreInstructionsView.swift index 84c8016..37d2274 100644 --- a/Sources/OversizeKit/StoreKit/StoreScreen/StoreInstuctinsView.swift +++ b/Sources/OversizeKit/StoreKit/StoreScreen/StoreInstructionsView.swift @@ -1,6 +1,6 @@ // // Copyright © 2023 Alexander Romanov -// StoreInstuctinsView.swift +// StoreInstructionsView.swift // import OversizeComponents @@ -11,7 +11,7 @@ import OversizeStoreService import OversizeUI import SwiftUI -public struct StoreInstuctinsView: View { +public struct StoreInstructionsView: View { @StateObject var viewModel: StoreViewModel @Environment(\.screenSize) var screenSize @Environment(\.isPremium) var isPremium @@ -40,9 +40,9 @@ public struct StoreInstuctinsView: View { .paddingContent(.horizontal) } .backgroundLinerGradient(LinearGradient(colors: [.backgroundPrimary, .backgroundSecondary], startPoint: .top, endPoint: .center)) - .titleLabel { - PremiumLabel(image: Resource.Store.zap, text: Info.store.subscriptionsName, size: .medium) - } +// .titleLabel { +// PremiumLabel(image: Resource.Store.zap, text: Info.store.subscriptionsName, size: .medium) +// } .trailingBar { BarButton(.close) } @@ -135,7 +135,7 @@ public struct StoreInstuctinsView: View { Text("Begin your path towards feeling better with a ") .foregroundColor(.onSurfaceSecondary) - + Text("\(viewModel.saleProcent)% discount") + + Text("\(viewModel.salePercent)% discount") .foregroundColor(.accent) } .body(.semibold) @@ -171,7 +171,10 @@ public struct StoreInstuctinsView: View { .id(10) } - SubscriptionPrivacyView(products: data) + SubscriptionPrivacyView( + subscriptionsName: viewModel.productsState.result?.banner.badge ?? "", + products: data + ) } .padding(.bottom, 220) .onAppear { @@ -239,7 +242,7 @@ public struct StoreInstuctinsView: View { .background { Circle() .fill(Color.surfacePrimary) - .shadowElevaton(.z2) + .shadowElevation(.z2) } TextBox( @@ -267,7 +270,7 @@ public struct StoreInstuctinsView: View { .background { Circle() .fill(Color.surfacePrimary) - .shadowElevaton(.z2) + .shadowElevation(.z2) } TextBox( @@ -285,21 +288,6 @@ public struct StoreInstuctinsView: View { @ViewBuilder func productsLust(data: StoreKitProducts) -> some View { VStack(spacing: .small) { -// VStack { -// if let currentSubscription = viewModel.currentSubscription { -// VStack { -// Text("My Subscription") -// -// StoreProductView(product: currentSubscription, products: data) {} -// -// if let status = viewModel.status { -// StatusInfoView(product: currentSubscription, status: status, products: data) -// } -// } -// .listStyle(GroupedListStyle()) -// } -// } - ForEach(viewModel.availableSubscriptions) { product in if !product.isOffer { StoreProductView(product: product, products: data, isSelected: .constant(false)) { @@ -320,8 +308,8 @@ public struct StoreInstuctinsView: View { } } -struct StoreViewInstuctins_Previews: PreviewProvider { +struct StoreViewInstructions_Previews: PreviewProvider { static var previews: some View { - StoreInstuctinsView() + StoreInstructionsView() } } diff --git a/Sources/OversizeKit/StoreKit/StoreScreen/StoreSpecialOfferView.swift b/Sources/OversizeKit/StoreKit/StoreScreen/StoreSpecialOfferView.swift index fcda6a8..1aa2e9e 100644 --- a/Sources/OversizeKit/StoreKit/StoreScreen/StoreSpecialOfferView.swift +++ b/Sources/OversizeKit/StoreKit/StoreScreen/StoreSpecialOfferView.swift @@ -172,9 +172,9 @@ public struct StoreSpecialOfferView: View { .paddingContent(.horizontal) } .backgroundLinerGradient(LinearGradient(colors: [.backgroundPrimary, .backgroundSecondary], startPoint: .top, endPoint: .center)) - .titleLabel { - PremiumLabel(image: Resource.Store.zap, text: Info.store.subscriptionsName, size: .medium) - } +// .titleLabel { +// PremiumLabel(image: Resource.Store.zap, text: Info.store.subscriptionsName, size: .medium) +// } .trailingBar { BarButton(.closeAction { lastClosedSpecialOffer = event.id @@ -215,7 +215,8 @@ public struct StoreSpecialOfferView: View { ScrollViewReader { value in VStack(spacing: .medium) { VStack(spacing: .zero) { - PremiumLabel(image: Resource.Store.zap, text: Info.store.subscriptionsName, size: platform == .macOS ? .small : .medium) + // PremiumLabel(image: Resource.Store.zap, text: Info.store.subscriptionsName, size: platform == .macOS ? .small : .medium) + Text("") #if os(macOS) .padding(.vertical, .medium) #else @@ -283,9 +284,12 @@ public struct StoreSpecialOfferView: View { .opacity(0 + (offset * 0.01)) .id(10) - SubscriptionPrivacyView(products: data) - .padding(.horizontal, .medium) - .padding(.bottom, .large) + SubscriptionPrivacyView( + subscriptionsName: viewModel.productsState.result?.banner.badge ?? "", + products: data + ) + .padding(.horizontal, .medium) + .padding(.bottom, .large) } .padding(.bottom, 180) .task { @@ -387,7 +391,7 @@ public struct StoreSpecialOfferView: View { text .replacingOccurrences(of: "", with: salePercent.toString) .replacingOccurrences(of: "", with: trialDaysPeriodText) - .replacingOccurrences(of: "", with: Info.store.subscriptionsName) + // .replacingOccurrences(of: "", with: Info.store.subscriptionsName) } @ViewBuilder diff --git a/Sources/OversizeKit/StoreKit/StoreScreen/StoreView.swift b/Sources/OversizeKit/StoreKit/StoreScreen/StoreView.swift index 5b8fe1c..b0555b7 100644 --- a/Sources/OversizeKit/StoreKit/StoreScreen/StoreView.swift +++ b/Sources/OversizeKit/StoreKit/StoreScreen/StoreView.swift @@ -4,7 +4,9 @@ // import OversizeComponents +import OversizeCore import OversizeLocalizable +import OversizeNavigation import OversizeResources import OversizeServices import OversizeStoreService @@ -26,42 +28,39 @@ public struct StoreView: View { } public var body: some View { - Page { + NavigationLayoutView { Group { switch viewModel.state { case .idle, .loading: contentPlaceholder() case let .result(data): content(data: data) - .if(platform == .macOS) { view in - view.padding(.top, 24) - } case let .error(error): ErrorView(error) } } .paddingContent(.horizontal) + } background: { + LinearGradient( + colors: [ + .backgroundPrimary, + .backgroundSecondary, + ], + startPoint: .top, + endPoint: .center + ) } - #if os(macOS) - .backgroundSecondary() - #endif -// .backgroundLinerGradient(LinearGradient(colors: [.backgroundPrimary, .backgroundSecondary], startPoint: .top, endPoint: .center)) -// .titleLabel { -// PremiumLabel(image: Resource.Store.zap, text: Info.store.subscriptionsName, size: .medium) -// } -// .leadingBar { -// if !isPortrait, verticalSizeClass == .regular, isClosable { -// EmptyView() -// } else { -// BarButton(.back) -// } -// } -// .trailingBar { -// if isClosable { -// BarButton(.close) -// } -// } - .bottomToolbar(style: .none) { + .toolbarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .principal) { + PremiumLabel( + image: Resource.Store.zap, + text: Info.store.subscriptionsName, + size: .medium + ) + } + } + .safeAreaInset(edge: .bottom) { if !viewModel.isPremium { StorePaymentButtonBar() .environmentObject(viewModel) @@ -81,13 +80,13 @@ public struct StoreView: View { if viewModel.isPremium { "You are all set!" } else { - "Upgrade to \(Info.store.subscriptionsName)" + "Upgrade to \(viewModel.productsState.result?.banner.badge ?? "")" } } var subtitleText: String { if viewModel.isPremium { - "Thank you for use to \(Info.store.subscriptionsName).\nHere's what is now unlocked." + "Thank you for use to \(viewModel.productsState.result?.banner.badge ?? "").\nHere's what is now unlocked." } else { "Remove ads and unlock all features" } @@ -122,47 +121,55 @@ public struct StoreView: View { @ViewBuilder private func content(data: StoreKitProducts) -> some View { - VStack(spacing: .medium) { - VStack(spacing: .xxSmall) { - Text(titleText) - .title() - .foregroundColor(.onSurfacePrimary) + LazyVStack(spacing: .medium) { + titleView - Text(subtitleText) - .headline() - .foregroundColor(.onSurfaceSecondary) - } - .multilineTextAlignment(.center) + #if DEBUG + if let currentSubscription = viewModel.currentSubscription { + LeadingVStack(spacing: .small) { + Text("My Subscription") + .headline() + .onSurfacePrimaryForeground() - if !viewModel.isPremium { - HStack(spacing: .xSmall) { - ForEach(viewModel.availableSubscriptions /* data.autoRenewable */ ) { product in - if !product.isOffer { - StoreProductView(product: product, products: data, isSelected: .constant(viewModel.selectedProduct == product)) { - viewModel.selectedProduct = product - } - .storeProductStyle(.collumn) - } - } - ForEach(data.nonConsumable) { product in - StoreProductView(product: product, products: data, isSelected: .constant(viewModel.selectedProduct == product)) { - viewModel.selectedProduct = product - } - .storeProductStyle(.collumn) + StoreProductView(product: currentSubscription, products: data) {} + + if let status = viewModel.status { + Text("Status: \(status.state.localizedDescription)") + .caption() + .onSurfacePrimaryForeground() } } + } else { + Surface { + Text("No subscription") + .onSurfacePrimaryForeground() + .body() + .frame( + maxWidth: .infinity, + alignment: .center + ) + } + } + #endif + + if !viewModel.isPremium { + productsCollum(data: data) } StoreFeaturesView() .environmentObject(viewModel) - SubscriptionPrivacyView(products: data) + SubscriptionPrivacyView( + subscriptionsName: viewModel.productsState.result?.banner.badge ?? "", + products: data + ) if !viewModel.isPremium { - productsLust(data: data) - .padding(.bottom, 170) + productsList(data: data) } } + .padding(.top, 24) + .padding(.bottom, 12) .onAppear { Task { // When this view appears, get the latest subscription status. @@ -183,24 +190,42 @@ public struct StoreView: View { } } + private var titleView: some View { + VStack(spacing: .xxSmall) { + Text(titleText) + .title() + .foregroundColor(.onSurfacePrimary) + + Text(subtitleText) + .headline() + .foregroundColor(.onSurfaceSecondary) + } + .multilineTextAlignment(.center) + } + + @ViewBuilder + private func productsCollum(data: StoreKitProducts) -> some View { + HStack(spacing: .xSmall) { + ForEach(viewModel.availableSubscriptions /* data.autoRenewable */ ) { product in + if !product.isOffer { + StoreProductView(product: product, products: data, isSelected: .constant(viewModel.selectedProduct == product)) { + viewModel.selectedProduct = product + } + .storeProductStyle(.column) + } + } + ForEach(data.nonConsumable) { product in + StoreProductView(product: product, products: data, isSelected: .constant(viewModel.selectedProduct == product)) { + viewModel.selectedProduct = product + } + .storeProductStyle(.column) + } + } + } + @ViewBuilder - private func productsLust(data: StoreKitProducts) -> some View { + private func productsList(data: StoreKitProducts) -> some View { VStack(spacing: .small) { -// VStack { -// if let currentSubscription = viewModel.currentSubscription { -// VStack { -// Text("My Subscription") -// -// StoreProductView(product: currentSubscription, products: data) {} -// -// if let status = viewModel.status { -// StatusInfoView(product: currentSubscription, status: status, products: data) -// } -// } -// .listStyle(GroupedListStyle()) -// } -// } - ForEach(viewModel.availableSubscriptions /* data.autoRenewable */ ) { product in if !product.isOffer { StoreProductView(product: product, products: data, isSelected: .constant(viewModel.selectedProduct == product)) { @@ -211,9 +236,6 @@ public struct StoreView: View { ForEach(data.nonConsumable) { product in StoreProductView(product: product, products: data, isSelected: .constant(viewModel.selectedProduct == product)) { viewModel.selectedProduct = product -// Task { -// await viewModel.buy(product: product) -// } } } } @@ -245,3 +267,4 @@ public struct StoreView: View { } } #endif + \ No newline at end of file diff --git a/Sources/OversizeKit/StoreKit/StoreScreen/ViewModel/StoreViewModel.swift b/Sources/OversizeKit/StoreKit/StoreScreen/ViewModel/StoreViewModel.swift index 72a89a4..77ea3dd 100644 --- a/Sources/OversizeKit/StoreKit/StoreScreen/ViewModel/StoreViewModel.swift +++ b/Sources/OversizeKit/StoreKit/StoreScreen/ViewModel/StoreViewModel.swift @@ -61,7 +61,7 @@ public class StoreViewModel: ObservableObject { // MARK: - Descriptions extension StoreViewModel { - var subsribtionStatusText: String { + var subscriptionStatusText: String { guard case let .result(products) = state else { return "" } if !products.purchasedNonConsumable.isEmpty { return "Lifetime" @@ -103,7 +103,7 @@ extension StoreViewModel { } } - var subsribtionStatusColor: Color { + var subscriptionStatusColor: Color { guard case let .result(products) = state else { return .gray } if !products.purchasedNonConsumable.isEmpty { return .green } guard let subscriptionStatus = products.subscriptionGroupStatus else { return .red } @@ -135,12 +135,12 @@ extension StoreViewModel { } } - var saleProcent: String { + var salePercent: String { guard let yearSubscriptionProduct else { return "" } if let monthSubscriptionProduct { let yearPriceMonthly = monthSubscriptionProduct.price * 12 - let procent = (yearPriceMonthly - yearSubscriptionProduct.price) / yearPriceMonthly - return (procent * 100).rounded(0).toString + let percent = (yearPriceMonthly - yearSubscriptionProduct.price) / yearPriceMonthly + return (percent * 100).rounded(0).toString } else { return "" } @@ -249,7 +249,7 @@ extension StoreViewModel { await transaction.finish() } catch { // StoreKit has a transaction that fails verification. Don't deliver content to the user. - log("Transaction failed verification") + logError("Transaction failed verification", error: error) } } } @@ -299,12 +299,6 @@ extension StoreViewModel { let highestTier = storeKitService.tier(for: currentProduct.id) let newTier = storeKitService.tier(for: renewalInfo.currentProductID) - log("romanov.cc.ScaleDown.monthly") - log(storeKitService.tier(for: "romanov.cc.ScaleDown.monthly")) - - log("romanov.cc.ScaleDown.yearly") - log(storeKitService.tier(for: "romanov.cc.ScaleDown.yearly")) - if newTier > highestTier { highestStatus = status highestProduct = newSubscription @@ -315,7 +309,7 @@ extension StoreViewModel { status = highestStatus currentSubscription = highestProduct } catch { - log("Could not update subscription status \(error)") + logError("Could not update subscription status", error: error) } } @@ -399,15 +393,15 @@ extension StoreViewModel { currentSubscriptionStatus = status } state = .result(finalProducts) - log("✅ StoeKit fetched") + logSuccess("StoreKit products fetched") if finalProducts.autoRenewable.isEmpty { - log("❌ NO autoRenewable products") + logError("No autoRenewable products") } else { - log("📦 \(finalProducts.autoRenewable.count) autoRenewable products") + logInfo("\(finalProducts.autoRenewable.count) autoRenewable products") } case let .failure(error): state = .error(error) - log("❌ Product not fetched (\(error.title))") + logError("StoreKit Products not fetched", error: error) } case let .failure(error): @@ -424,106 +418,105 @@ extension Date { } } -/* - // MARK: - StoreKit status - extension StoreViewModel { - var statusDescription: String { - - guard case .verified(let renewalInfo) = status?.renewalInfo, - case .verified(let transaction) = status?.transaction else { - return "The App Store could not verify your subscription status." - } - - guard let status = status else { return "" } - - guard let product = currentSubscription else { return "" } - var description = "" - - switch status.state { - case .subscribed: - description = subscribedDescription(product: product) - case .expired: - if let expirationDate = transaction.expirationDate, - let expirationReason = renewalInfo.expirationReason { - description = expirationDescription(expirationReason, expirationDate: expirationDate, product: product) - } - case .revoked: - if let revokedDate = transaction.revocationDate { - description = "The App Store refunded your subscription to \(product.displayName) on \(revokedDate.formattedDate())." - } - case .inGracePeriod: - description = gracePeriodDescription(renewalInfo, product: product) - case .inBillingRetryPeriod: - description = billingRetryDescription(product: product) - default: - break - } - - if let expirationDate = transaction.expirationDate { - description += renewalDescription(renewalInfo, expirationDate, product: product) - } - return description - } - - fileprivate func subscribedDescription(product: Product) -> String { - return "You are currently subscribed to \(product.displayName)." - } - - //Build a string description of the `expirationReason` to display to the user. - fileprivate func expirationDescription(_ expirationReason: RenewalInfo.ExpirationReason, expirationDate: Date, product: Product) -> String { - var description = "" - - switch expirationReason { - case .autoRenewDisabled: - if expirationDate > Date() { - description += "Your subscription to \(product.displayName) will expire on \(expirationDate.formattedDate())." - } else { - description += "Your subscription to \(product.displayName) expired on \(expirationDate.formattedDate())." - } - case .billingError: - description = "Your subscription to \(product.displayName) was not renewed due to a billing error." - case .didNotConsentToPriceIncrease: - description = "Your subscription to \(product.displayName) was not renewed due to a price increase that you disapproved." - case .productUnavailable: - description = "Your subscription to \(product.displayName) was not renewed because the product is no longer available." - default: - description = "Your subscription to \(product.displayName) was not renewed." - } - - return description - } - - fileprivate func renewalDescription(_ renewalInfo: RenewalInfo, _ expirationDate: Date, product: Product) -> String { - guard case let .result(products) = state else { return "" } - - var description = "" - - if let newProductID = renewalInfo.autoRenewPreference { - if let newProduct = products.autoRenewable.first(where: { $0.id == newProductID }) { - description += "\nYour subscription to \(newProduct.displayName)" - description += " will begin when your current subscription expires on \(expirationDate.formattedDate())." - } - } else if renewalInfo.willAutoRenew { - description += "\nNext billing date: \(expirationDate.formattedDate())." - } - - return description - } - - fileprivate func gracePeriodDescription(_ renewalInfo: RenewalInfo, product: Product) -> String { - var description = "The App Store could not confirm your billing information for \(product.displayName)." - if let untilDate = renewalInfo.gracePeriodExpirationDate { - description += " Please verify your billing information to continue service after \(untilDate.formattedDate())" - } - - return description - } - - fileprivate func billingRetryDescription(product: Product) -> String { - var description = "The App Store could not confirm your billing information for \(product.displayName)." - description += " Please verify your billing information to resume service." - return description - } - - } - */ +// MARK: - StoreKit status + +extension StoreViewModel { + var statusDescription: String { + guard case let .verified(renewalInfo) = status?.renewalInfo, + case let .verified(transaction) = status?.transaction + else { + return "The App Store could not verify your subscription status." + } + + guard let status else { return "" } + + guard let product = currentSubscription else { return "" } + var description = "" + + switch status.state { + case .subscribed: + description = subscribedDescription(product: product) + case .expired: + if let expirationDate = transaction.expirationDate, + let expirationReason = renewalInfo.expirationReason + { + description = expirationDescription(expirationReason, expirationDate: expirationDate, product: product) + } + case .revoked: + if let revokedDate = transaction.revocationDate { + description = "The App Store refunded your subscription to \(product.displayName) on \(revokedDate.formattedDate())." + } + case .inGracePeriod: + description = gracePeriodDescription(renewalInfo, product: product) + case .inBillingRetryPeriod: + description = billingRetryDescription(product: product) + default: + break + } + + if let expirationDate = transaction.expirationDate { + description += renewalDescription(renewalInfo, expirationDate, product: product) + } + return description + } + + private func subscribedDescription(product: Product) -> String { + "You are currently subscribed to \(product.displayName)." + } + + // Build a string description of the `expirationReason` to display to the user. + private func expirationDescription(_ expirationReason: RenewalInfo.ExpirationReason, expirationDate: Date, product: Product) -> String { + var description = "" + + switch expirationReason { + case .autoRenewDisabled: + if expirationDate > Date() { + description += "Your subscription to \(product.displayName) will expire on \(expirationDate.formattedDate())." + } else { + description += "Your subscription to \(product.displayName) expired on \(expirationDate.formattedDate())." + } + case .billingError: + description = "Your subscription to \(product.displayName) was not renewed due to a billing error." + case .didNotConsentToPriceIncrease: + description = "Your subscription to \(product.displayName) was not renewed due to a price increase that you disapproved." + case .productUnavailable: + description = "Your subscription to \(product.displayName) was not renewed because the product is no longer available." + default: + description = "Your subscription to \(product.displayName) was not renewed." + } + + return description + } + + private func renewalDescription(_ renewalInfo: RenewalInfo, _ expirationDate: Date, product _: Product) -> String { + guard case let .result(products) = state else { return "" } + + var description = "" + + if let newProductID = renewalInfo.autoRenewPreference { + if let newProduct = products.autoRenewable.first(where: { $0.id == newProductID }) { + description += "\nYour subscription to \(newProduct.displayName)" + description += " will begin when your current subscription expires on \(expirationDate.formattedDate())." + } + } else if renewalInfo.willAutoRenew { + description += "\nNext billing date: \(expirationDate.formattedDate())." + } + + return description + } + + private func gracePeriodDescription(_ renewalInfo: RenewalInfo, product: Product) -> String { + var description = "The App Store could not confirm your billing information for \(product.displayName)." + if let untilDate = renewalInfo.gracePeriodExpirationDate { + description += " Please verify your billing information to continue service after \(untilDate.formattedDate())" + } + + return description + } + + private func billingRetryDescription(product: Product) -> String { + var description = "The App Store could not confirm your billing information for \(product.displayName)." + description += " Please verify your billing information to resume service." + return description + } +} diff --git a/Sources/OversizeKit/StoreKit/ViewModifier/OnPremiumTap.swift b/Sources/OversizeKit/StoreKit/ViewModifier/OnPremiumTap.swift index 98ca7fb..57a0297 100644 --- a/Sources/OversizeKit/StoreKit/ViewModifier/OnPremiumTap.swift +++ b/Sources/OversizeKit/StoreKit/ViewModifier/OnPremiumTap.swift @@ -6,12 +6,12 @@ import SwiftUI public struct OnPremiumTap: ViewModifier { - @State var isShowPremium = false @Environment(\.isPremium) var isPremium @Environment(\.colorScheme) var colorScheme #if os(macOS) @Environment(\.openWindow) var openWindow #endif + @Environment(\.navigator) var navigator public func body(content: Content) -> some View { if isPremium { @@ -25,14 +25,10 @@ public struct OnPremiumTap: ViewModifier { #if os(macOS) openWindow(id: "Window.StoreView") #else - isShowPremium.toggle() + navigator.navigate(to: SettingsDestinations.premium) #endif } ) - .sheet(isPresented: $isShowPremium) { - StoreView() - .systemServices() - } } } } diff --git a/Sources/OversizeKit/StoreKit/Views/BuyButtonStyle.swift b/Sources/OversizeKit/StoreKit/Views/BuyButtonStyle.swift index 1ca74d8..fd3ef83 100644 --- a/Sources/OversizeKit/StoreKit/Views/BuyButtonStyle.swift +++ b/Sources/OversizeKit/StoreKit/Views/BuyButtonStyle.swift @@ -56,7 +56,7 @@ public struct PaymentButtonStyle: ButtonStyle { .background(background) .overlay(loadingView(for: configuration.role)) .scaleEffect(configuration.isPressed ? 0.98 : 1) - .shadowElevaton(elevation) + .shadowElevation(elevation) } @ViewBuilder diff --git a/Sources/OversizeKit/StoreKit/Views/PrmiumBannerRow.swift b/Sources/OversizeKit/StoreKit/Views/PremiumBannerRow.swift similarity index 86% rename from Sources/OversizeKit/StoreKit/Views/PrmiumBannerRow.swift rename to Sources/OversizeKit/StoreKit/Views/PremiumBannerRow.swift index 39d8c22..3301ab7 100644 --- a/Sources/OversizeKit/StoreKit/Views/PrmiumBannerRow.swift +++ b/Sources/OversizeKit/StoreKit/Views/PremiumBannerRow.swift @@ -1,8 +1,9 @@ // // Copyright © 2023 Alexander Romanov -// PrmiumBannerRow.swift +// PremiumBannerRow.swift // +import NavigatorUI import OversizeLocalizable import OversizeResources import OversizeServices @@ -11,7 +12,7 @@ import OversizeUI import SwiftUI // swiftlint:disable all -public struct PrmiumBannerRow: View { +public struct PremiumBannerRow: View { @Environment(\.colorScheme) var colorScheme @StateObject private var viewModel: StoreViewModel #if os(macOS) @@ -22,27 +23,21 @@ public struct PrmiumBannerRow: View { @State var showModal = false + @Environment(\.navigator) private var navigator + public init() { _viewModel = StateObject(wrappedValue: StoreViewModel()) } public var body: some View { VStack { - #if os(iOS) - NavigationLink { - StoreView() - .closable(false) - } label: { - if viewModel.isPremium || viewModel.isPremiumActivated { - subscriptionRow - } else { - banner - } - } - .buttonStyle(.row) - #elseif os(macOS) Button { + #if os(macOS) openWindow(id: "Window.StoreView") + #else + navigator.push(SettingsDestinations.premium) + #endif + } label: { if viewModel.isPremium || viewModel.isPremiumActivated { subscriptionRow @@ -51,7 +46,6 @@ public struct PrmiumBannerRow: View { } } .buttonStyle(.row) - #endif } .task { await viewModel.fetchData() @@ -82,19 +76,19 @@ public struct PrmiumBannerRow: View { )) ) - Text(Info.store.subscriptionsName) + Text(viewModel.productsState.result?.banner.badge ?? "") .headline(.semibold) .foregroundColor(.onSurfacePrimary) Spacer() HStack(spacing: .small) { - Text(viewModel.subsribtionStatusText) + Text(viewModel.subscriptionStatusText) .headline(.medium) .foregroundColor(.onSurfaceSecondary) Circle() - .foregroundColor(viewModel.subsribtionStatusColor) + .foregroundColor(viewModel.subscriptionStatusColor) .frame(width: 8, height: 8) } @@ -105,7 +99,7 @@ public struct PrmiumBannerRow: View { } } -public extension PrmiumBannerRow { +public extension PremiumBannerRow { var banner: some View { HStack { Spacer() @@ -120,7 +114,7 @@ public extension PrmiumBannerRow { .colorMultiply(Color(hex: "B75375")) #endif - Text(viewModel.productsState.result?.banner.badge ?? "Pro") + Text(viewModel.productsState.result?.banner.badge ?? "") .font(.system(size: platform == .macOS ? 16 : 20, weight: platform == .macOS ? .bold : .heavy)) .foregroundColor(Color(hex: "B75375")) .redacted(reason: viewModel.productsState.isLoading ? .placeholder : .init()) @@ -160,8 +154,8 @@ public extension PrmiumBannerRow { } } -struct PrmiumBannerRow_Previews: PreviewProvider { +struct PremiumBannerRow_Previews: PreviewProvider { static var previews: some View { - PrmiumBannerRow() + PremiumBannerRow() } } diff --git a/Sources/OversizeKit/StoreKit/Views/StoreProductView.swift b/Sources/OversizeKit/StoreKit/Views/StoreProductView.swift index b8c54d1..d1f1e30 100644 --- a/Sources/OversizeKit/StoreKit/Views/StoreProductView.swift +++ b/Sources/OversizeKit/StoreKit/Views/StoreProductView.swift @@ -12,7 +12,7 @@ import SwiftUI public struct StoreProductView: View { public enum StoreProductViewType { - case row, collumn + case row, column } @Injected(\.storeKitService) private var store: StoreKitService @@ -48,11 +48,11 @@ public struct StoreProductView: View { } } - var saleProcent: String { + var salePercent: String { if let monthSubscriptionProduct { let yearPriceMonthly = monthSubscriptionProduct.price * 12 - let procent = (yearPriceMonthly - product.price) / yearPriceMonthly - return (procent * 100).rounded(0).toString + let percent = (yearPriceMonthly - product.price) / yearPriceMonthly + return (percent * 100).rounded(0).toString } else { return "" } @@ -76,7 +76,7 @@ public struct StoreProductView: View { if product.type == .autoRenewable, let offer = product.subscription?.introductoryOffer { topLabelRow(offer: offer) } - case .collumn: + case .column: topLabelCollumn } } @@ -151,7 +151,7 @@ public struct StoreProductView: View { } .padding(.vertical, platform == .macOS ? .xxSmall : .small) .padding(.horizontal, platform == .macOS ? 12 : 18) - case .collumn: + case .column: VStack(spacing: .zero) { Text(product.displayMonthsCount) .title2() @@ -215,7 +215,7 @@ public struct StoreProductView: View { .foregroundColor(.onSurfacePrimary) if isHaveSale, !isPurchased { - Text("Save " + saleProcent + "%") + Text("Save " + salePercent + "%") .caption2(.bold) .foregroundColor(.onPrimary) .padding(.horizontal, .xxSmall) @@ -255,7 +255,7 @@ public struct StoreProductView: View { #if os(iOS) || os(macOS) if isHaveSale, !isPurchased { - Text("Save " + saleProcent + "%") + Text("Save " + salePercent + "%") .caption2(.bold) .foregroundColor(.onPrimary) .padding(.vertical, .xxxSmall) @@ -310,7 +310,7 @@ public struct StoreProductView: View { RoundedRectangle(cornerRadius: platform == .macOS ? 5 : 10, style: .continuous) .fill(Color.surfacePrimary) .overlay { - if type == .collumn, !isSelected { + if type == .column, !isSelected { RoundedRectangle(cornerRadius: platform == .macOS ? 6 : 12, style: .continuous) .strokeBorder(Color.backgroundTertiary, lineWidth: platform == .macOS ? 1 : 2) .padding(-2) @@ -344,7 +344,7 @@ public struct StoreProductView: View { switch type { case .row: .backgroundTertiary - case .collumn: + case .column: .surfaceSecondary } } @@ -384,7 +384,7 @@ public struct StoreProductView: View { } } - public func storeProductStyle(_ type: StoreProductViewType = .collumn) -> StoreProductView { + public func storeProductStyle(_ type: StoreProductViewType = .column) -> StoreProductView { var control = self control.type = type return control diff --git a/Sources/OversizeKit/StoreKit/Views/SubscriptionPrivacyView.swift b/Sources/OversizeKit/StoreKit/Views/SubscriptionPrivacyView.swift index 67bb3a5..b018370 100644 --- a/Sources/OversizeKit/StoreKit/Views/SubscriptionPrivacyView.swift +++ b/Sources/OversizeKit/StoreKit/Views/SubscriptionPrivacyView.swift @@ -11,6 +11,7 @@ import StoreKit import SwiftUI struct SubscriptionPrivacyView: View { + let subscriptionsName: String let products: StoreKitProducts @Environment(\.platform) private var platform @@ -20,11 +21,11 @@ struct SubscriptionPrivacyView: View { var body: some View { Surface { VStack(spacing: .xxSmall) { - Text("About \(Info.store.subscriptionsName) subscription") + Text("About \(subscriptionsName) subscription") .subheadline(.bold) .foregroundColor(Color.onSurfaceTertiary) - Text("\(Info.store.subscriptionsName) subscription is required to get access to all functions. Regardless whether the subscription has free trial period or not it automatically renews with the price and duration given above unless it is canceled at least 24 hours before the end of the current period. Payment will be charged to your Apple ID account at the confirmation of purchase. Your account will be charged for renewal within 24 hours prior to the end of the current period. You can manage and cancel your subscriptions by going to your account settings on the App Store after purchase. Any unused portion of a free trial period, if offered, will be forfeited when the user purchases a subscription to that publication, where applicable.") + Text("\(subscriptionsName) subscription is required to get access to all functions. Regardless whether the subscription has free trial period or not it automatically renews with the price and duration given above unless it is canceled at least 24 hours before the end of the current period. Payment will be charged to your Apple ID account at the confirmation of purchase. Your account will be charged for renewal within 24 hours prior to the end of the current period. You can manage and cancel your subscriptions by going to your account settings on the App Store after purchase. Any unused portion of a free trial period, if offered, will be forfeited when the user purchases a subscription to that publication, where applicable.") .caption() .foregroundColor(Color.onSurfaceSecondary) diff --git a/Sources/OversizeKit/SystemKit/SystemServices.swift b/Sources/OversizeKit/SystemKit/SystemServices.swift index 5313c12..fc42c12 100644 --- a/Sources/OversizeKit/SystemKit/SystemServices.swift +++ b/Sources/OversizeKit/SystemKit/SystemServices.swift @@ -12,7 +12,7 @@ import SwiftUI public struct SystemServicesModifier: ViewModifier { @Injected(\.appStateService) private var appState: AppStateService @Injected(\.settingsService) private var settingsService: SettingsServiceProtocol - @Injected(\.appStoreReviewService) private var appStoreReviewService: AppStoreReviewServiceProtocol + @Injected(\.appStoreReviewService) private var appStoreReviewService: AppStoreReviewService @Environment(\.scenePhase) private var scenePhase: ScenePhase @Environment(\.theme) private var theme: ThemeSettings @@ -42,7 +42,7 @@ public struct SystemServicesModifier: ViewModifier { .theme(ThemeSettings()) .screenSize(screnSize) #if os(iOS) - .accentColor(theme.accentColor) + .tint(theme.accentColor) #endif .onAppear(perform: { onAppear(geometry: geometry) }) .onChange(of: scenePhase) { _, phase in @@ -54,19 +54,19 @@ public struct SystemServicesModifier: ViewModifier { private func onChangeScenePhase(_ phase: ScenePhase) { switch phase { case .active: - if settingsService.blurMinimizeEnabend { + if settingsService.blurMinimizeEnabled { withAnimation { blurRadius = 0 } } case .background: - if settingsService.blurMinimizeEnabend { + if settingsService.blurMinimizeEnabled { withAnimation { blurRadius = 10 } } case .inactive: - if settingsService.blurMinimizeEnabend { + if settingsService.blurMinimizeEnabled { withAnimation { blurRadius = 10 } diff --git a/Sources/OversizeLocationKit/MapCoordinateView/MapCoordinateView.swift b/Sources/OversizeLocationKit/MapCoordinateView/MapCoordinateView.swift index cd525b3..07b8366 100644 --- a/Sources/OversizeLocationKit/MapCoordinateView/MapCoordinateView.swift +++ b/Sources/OversizeLocationKit/MapCoordinateView/MapCoordinateView.swift @@ -96,7 +96,7 @@ public struct MapCoordinateView: View { .background { Capsule() .fillSurfacePrimary() - .shadowElevaton(.z1) + .shadowElevation(.z1) } Spacer() } @@ -112,7 +112,7 @@ public struct MapCoordinateView: View { .background { Capsule() .fillSurfacePrimary() - .shadowElevaton(.z1) + .shadowElevation(.z1) } }) .padding(.trailing, 16) diff --git a/Sources/OversizeNoticeKit/NoticeListView.swift b/Sources/OversizeNoticeKit/NoticeListView.swift index ea46577..85e3598 100644 --- a/Sources/OversizeNoticeKit/NoticeListView.swift +++ b/Sources/OversizeNoticeKit/NoticeListView.swift @@ -45,24 +45,30 @@ public struct NoticeListView: View { .buttonStyle(.primary(infinityWidth: true)) .accent() .simultaneousGesture(TapGesture().onEnded { - viewModel.reviewService.estimate(goodRating: true) - withAnimation { - isBannerClosed = true + Task { + await viewModel.reviewService.estimate(goodRating: true) + withAnimation { + isBannerClosed = true + } } }) Button("Bad") { - viewModel.reviewService.estimate(goodRating: false) - withAnimation { - isBannerClosed = true + Task { + await viewModel.reviewService.estimate(goodRating: false) + withAnimation { + isBannerClosed = true + } } } .buttonStyle(.tertiary(infinityWidth: true)) } closeAction: { - viewModel.reviewService.rewiewBunnerClosed() - withAnimation { - isBannerClosed = true + Task { + await viewModel.reviewService.reviewBannerClosed() + withAnimation { + isBannerClosed = true + } } } .animation(.default, value: isBannerClosed) diff --git a/Sources/OversizeNoticeKit/NoticeListViewModel.swift b/Sources/OversizeNoticeKit/NoticeListViewModel.swift index aa9b184..e7f48a6 100644 --- a/Sources/OversizeNoticeKit/NoticeListViewModel.swift +++ b/Sources/OversizeNoticeKit/NoticeListViewModel.swift @@ -25,10 +25,6 @@ public final class NoticeListViewModel: ObservableObject { @Injected(\.networkService) var networkService @Injected(\.storeKitService) var storeKitService: StoreKitService - var isShowReviewBanner: Bool { - reviewService.isShowReviewBanner - } - @AppStorage("AppState.LastClosedSpecialOfferBanner") var lastClosedSpecialOffer: Int = .init() private let expectedFormat = Date.ISO8601FormatStyle() @@ -71,6 +67,7 @@ public final class NoticeListViewModel: ObservableObject { let result = await networkService.fetchSpecialOffers() switch result { case let .success(offers): + let isShowReviewBanner = await reviewService.isShowReviewBanner if let offer = offers.first(where: { checkDateInSelectedPeriod(startDate: $0.startDate, endDate: $0.endDate) }) { if offer.id != lastClosedSpecialOffer { withAnimation { @@ -107,6 +104,6 @@ public final class NoticeListViewModel: ObservableObject { text .replacingOccurrences(of: "", with: salePercent.toString) .replacingOccurrences(of: "", with: trialDaysPeriodText) - .replacingOccurrences(of: "", with: Info.store.subscriptionsName) + // .replacingOccurrences(of: "", with: Info.store.subscriptionsName) } }