diff --git a/.github/workflows/ci-pull-request.yml b/.github/workflows/ci-pull-request.yml
index a1b5635..9230a1a 100644
--- a/.github/workflows/ci-pull-request.yml
+++ b/.github/workflows/ci-pull-request.yml
@@ -4,8 +4,8 @@ on:
branches:
- 'main'
workflow_dispatch:
+
jobs:
-
build-swiftpm:
name: Build SwiftPM
uses: oversizedev/GithubWorkflows/.github/workflows/build-swiftpm.yml@main
@@ -19,10 +19,14 @@ jobs:
build-example:
name: Build Example
needs: build-swiftpm
- uses: oversizedev/GithubWorkflows/.github/workflows/build-ios-app.yml@main
+ uses: oversizedev/GithubWorkflows/.github/workflows/build-app.yml@main
+ strategy:
+ matrix:
+ destination: ['platform=iOS Simulator,name=iPhone 15 Pro,OS=17.2', 'platform=iOS Simulator,name=iPad Pro (12.9-inch) (6th generation),OS=17.2']
with:
- folder: AppExample
- app: Example
+ path: AppExample/Example
+ scheme: Example
+ destination: ${{ matrix.destination }}
secrets: inherit
# tests:
diff --git a/.github/workflows/ci-push.yml b/.github/workflows/ci-push.yml
index 58c3fd0..9803441 100644
--- a/.github/workflows/ci-push.yml
+++ b/.github/workflows/ci-push.yml
@@ -20,10 +20,14 @@ jobs:
build-example:
name: Build Example
needs: build-swiftpm
- uses: oversizedev/GithubWorkflows/.github/workflows/build-ios-app.yml@main
+ uses: oversizedev/GithubWorkflows/.github/workflows/build-app.yml@main
+ strategy:
+ matrix:
+ destination: ['platform=iOS Simulator,name=iPhone 15 Pro,OS=17.2', 'platform=iOS Simulator,name=iPad Pro (12.9-inch) (6th generation),OS=17.2']
with:
- folder: AppExample
- app: Example
+ path: AppExample/Example
+ scheme: Example
+ destination: ${{ matrix.destination }}
secrets: inherit
# tests:
diff --git a/.gitignore b/.gitignore
index b0a3eb8..5c5c727 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,6 @@
Package.resolved
.swiftpm
xcuserdata/
-DerivedData/
\ No newline at end of file
+/.swiftpm
+DerivedData/
+/AppExample/Example.xcodeproj/project.xcworkspace/xcuserdata/admin.xcuserdatad/UserInterfaceState.xcuserstate
diff --git a/.swiftpm/xcode/package.xcworkspace/xcuserdata/admin.xcuserdatad/UserInterfaceState.xcuserstate b/.swiftpm/xcode/package.xcworkspace/xcuserdata/admin.xcuserdatad/UserInterfaceState.xcuserstate
new file mode 100644
index 0000000..51ac161
Binary files /dev/null and b/.swiftpm/xcode/package.xcworkspace/xcuserdata/admin.xcuserdatad/UserInterfaceState.xcuserstate differ
diff --git a/.swiftpm/xcode/xcuserdata/admin.xcuserdatad/xcschemes/xcschememanagement.plist b/.swiftpm/xcode/xcuserdata/admin.xcuserdatad/xcschemes/xcschememanagement.plist
new file mode 100644
index 0000000..120e2c4
--- /dev/null
+++ b/.swiftpm/xcode/xcuserdata/admin.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -0,0 +1,77 @@
+
+
+
+
+ SchemeUserState
+
+ OversizeAdsKit.xcscheme_^#shared#^_
+
+ orderHint
+ 0
+
+ OversizeKit-Package.xcscheme_^#shared#^_
+
+ orderHint
+ 5
+
+ OversizeKitTests.xcscheme_^#shared#^_
+
+ orderHint
+ 13
+
+
+ SuppressBuildableAutocreation
+
+ OversizeAdsKit
+
+ primary
+
+
+ OversizeCalendarKit
+
+ primary
+
+
+ OversizeContactsKit
+
+ primary
+
+
+ OversizeKit
+
+ primary
+
+
+ OversizeKitTests
+
+ primary
+
+
+ OversizeLocationKit
+
+ primary
+
+
+ OversizeNoticeKit
+
+ primary
+
+
+ OversizeNotificationKit
+
+ primary
+
+
+ OversizeOnboardingKit
+
+ primary
+
+
+ OversizePhotoKit
+
+ primary
+
+
+
+
+
diff --git a/AppExample/Example.xcodeproj/project.pbxproj b/AppExample/Example.xcodeproj/project.pbxproj
index b4928e8..a8d543f 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 */; };
- 840CD69C2AC0E43000C6AAD0 /* OversizeAdsKit in Frameworks */ = {isa = PBXBuildFile; productRef = 840CD69B2AC0E43000C6AAD0 /* OversizeAdsKit */; };
840CD69E2AC0E43000C6AAD0 /* OversizeCalendarKit in Frameworks */ = {isa = PBXBuildFile; productRef = 840CD69D2AC0E43000C6AAD0 /* OversizeCalendarKit */; };
840CD6A02AC0E43000C6AAD0 /* OversizeContactsKit in Frameworks */ = {isa = PBXBuildFile; productRef = 840CD69F2AC0E43000C6AAD0 /* OversizeContactsKit */; };
840CD6A22AC0E43000C6AAD0 /* OversizeKit in Frameworks */ = {isa = PBXBuildFile; productRef = 840CD6A12AC0E43000C6AAD0 /* OversizeKit */; };
@@ -33,6 +32,7 @@
840CD6AC2AC0E43000C6AAD0 /* OversizePhotoKit in Frameworks */ = {isa = PBXBuildFile; productRef = 840CD6AB2AC0E43000C6AAD0 /* OversizePhotoKit */; };
840CD6AF2AC0E44E00C6AAD0 /* Factory in Frameworks */ = {isa = PBXBuildFile; productRef = 840CD6AE2AC0E44E00C6AAD0 /* Factory */; };
840CD6B12AC0E6E200C6AAD0 /* Products.storekit in Resources */ = {isa = PBXBuildFile; fileRef = 840CD6B02AC0E6E200C6AAD0 /* Products.storekit */; };
+ 845A59332BA4FD2B00988D52 /* OversizeModels in Frameworks */ = {isa = PBXBuildFile; productRef = 845A59322BA4FD2B00988D52 /* OversizeModels */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@@ -69,10 +69,10 @@
840CD6AF2AC0E44E00C6AAD0 /* Factory in Frameworks */,
840CD69E2AC0E43000C6AAD0 /* OversizeCalendarKit in Frameworks */,
840CD6A82AC0E43000C6AAD0 /* OversizeNotificationKit in Frameworks */,
- 840CD69C2AC0E43000C6AAD0 /* OversizeAdsKit in Frameworks */,
840CD6A22AC0E43000C6AAD0 /* OversizeKit in Frameworks */,
840CD6A42AC0E43000C6AAD0 /* OversizeLocationKit in Frameworks */,
840CD6A62AC0E43000C6AAD0 /* OversizeNoticeKit in Frameworks */,
+ 845A59332BA4FD2B00988D52 /* OversizeModels in Frameworks */,
840CD6A02AC0E43000C6AAD0 /* OversizeContactsKit in Frameworks */,
840CD6AA2AC0E43000C6AAD0 /* OversizeOnboardingKit in Frameworks */,
);
@@ -206,7 +206,6 @@
);
name = Example;
packageProductDependencies = (
- 840CD69B2AC0E43000C6AAD0 /* OversizeAdsKit */,
840CD69D2AC0E43000C6AAD0 /* OversizeCalendarKit */,
840CD69F2AC0E43000C6AAD0 /* OversizeContactsKit */,
840CD6A12AC0E43000C6AAD0 /* OversizeKit */,
@@ -216,6 +215,7 @@
840CD6A92AC0E43000C6AAD0 /* OversizeOnboardingKit */,
840CD6AB2AC0E43000C6AAD0 /* OversizePhotoKit */,
840CD6AE2AC0E44E00C6AAD0 /* Factory */,
+ 845A59322BA4FD2B00988D52 /* OversizeModels */,
);
productName = Example;
productReference = 840CD6632AC0E39D00C6AAD0 /* Example.app */;
@@ -248,6 +248,7 @@
packageReferences = (
840CD69A2AC0E43000C6AAD0 /* XCLocalSwiftPackageReference ".." */,
840CD6AD2AC0E44E00C6AAD0 /* XCRemoteSwiftPackageReference "Factory" */,
+ 845A59312BA4FD2B00988D52 /* XCRemoteSwiftPackageReference "OversizeModels" */,
);
productRefGroup = 840CD6642AC0E39D00C6AAD0 /* Products */;
projectDirPath = "";
@@ -515,13 +516,17 @@
minimumVersion = 2.2.0;
};
};
+ 845A59312BA4FD2B00988D52 /* XCRemoteSwiftPackageReference "OversizeModels" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/oversizedev/OversizeModels.git";
+ requirement = {
+ kind = upToNextMajorVersion;
+ minimumVersion = 0.1.0;
+ };
+ };
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
- 840CD69B2AC0E43000C6AAD0 /* OversizeAdsKit */ = {
- isa = XCSwiftPackageProductDependency;
- productName = OversizeAdsKit;
- };
840CD69D2AC0E43000C6AAD0 /* OversizeCalendarKit */ = {
isa = XCSwiftPackageProductDependency;
productName = OversizeCalendarKit;
@@ -559,6 +564,11 @@
package = 840CD6AD2AC0E44E00C6AAD0 /* XCRemoteSwiftPackageReference "Factory" */;
productName = Factory;
};
+ 845A59322BA4FD2B00988D52 /* OversizeModels */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 845A59312BA4FD2B00988D52 /* XCRemoteSwiftPackageReference "OversizeModels" */;
+ productName = OversizeModels;
+ };
/* 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 c5dd800..1cd4fb5 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/Resources/Products.storekit b/AppExample/Example/Resources/Products.storekit
index f9dfc04..98367b6 100644
--- a/AppExample/Example/Resources/Products.storekit
+++ b/AppExample/Example/Resources/Products.storekit
@@ -22,6 +22,8 @@
],
"settings" : {
"_failTransactionsEnabled" : false,
+ "_locale" : "en_US",
+ "_storefront" : "USA",
"_storeKitErrors" : [
{
"current" : null,
diff --git a/AppExample/Example/Router/Alerts.swift b/AppExample/Example/Router/Alerts.swift
index b216f7c..153c230 100644
--- a/AppExample/Example/Router/Alerts.swift
+++ b/AppExample/Example/Router/Alerts.swift
@@ -6,6 +6,7 @@
import OversizeLocalizable
import OversizeServices
import SwiftUI
+import OversizeModels
enum RootAlert: Identifiable {
case dismiss(_ action: () -> Void)
diff --git a/AppExample/Example/Screens/AppSettings/AppSettingsView.swift b/AppExample/Example/Screens/AppSettings/AppSettingsView.swift
index 625e930..9d6b36e 100644
--- a/AppExample/Example/Screens/AppSettings/AppSettingsView.swift
+++ b/AppExample/Example/Screens/AppSettings/AppSettingsView.swift
@@ -16,7 +16,6 @@ struct AppSettingsView: View {
Image(systemName: "")
}
.rowArrow()
-
.multilineTextAlignment(.leading)
}
.buttonStyle(.row)
diff --git a/Package.swift b/Package.swift
index be0790f..c5b5396 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,6 +1,7 @@
-// swift-tools-version: 5.7
+// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
+import Foundation
import PackageDescription
let productionDependencies: [PackageDescription.Package.Dependency] = [
@@ -14,6 +15,7 @@ let productionDependencies: [PackageDescription.Package.Dependency] = [
.package(url: "https://github.com/oversizedev/OversizeModels.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/GetStream/effects-library.git", .upToNextMajor(from: "1.0.0")),
]
let developmentDependencies: [PackageDescription.Package.Dependency] = [
@@ -27,8 +29,11 @@ let developmentDependencies: [PackageDescription.Package.Dependency] = [
.package(name: "OversizeModels", path: "../OversizeModels"),
.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/GetStream/effects-library.git", .upToNextMajor(from: "1.0.0")),
]
+let isProductionDependencies = ProcessInfo.processInfo.environment["RELEASE_DEPENDENCIES"] == "TRUE"
+
let package = Package(
name: "OversizeKit",
platforms: [
@@ -39,7 +44,6 @@ let package = Package(
],
products: [
.library(name: "OversizeKit", targets: ["OversizeKit"]),
- .library(name: "OversizeAdsKit", targets: ["OversizeAdsKit"]),
.library(name: "OversizeOnboardingKit", targets: ["OversizeOnboardingKit"]),
.library(name: "OversizeNoticeKit", targets: ["OversizeNoticeKit"]),
.library(name: "OversizeCalendarKit", targets: ["OversizeCalendarKit"]),
@@ -65,19 +69,7 @@ let package = Package(
.product(name: "OversizeNetwork", package: "OversizeNetwork"),
.product(name: "Factory", package: "Factory"),
.product(name: "CachedAsyncImage", package: "swiftui-cached-async-image"),
- ]
- ),
- .target(
- name: "OversizeAdsKit",
- dependencies: [
- "OversizeKit",
- .product(name: "Factory", package: "Factory"),
- .product(name: "OversizeUI", package: "OversizeUI"),
- .product(name: "OversizeServices", package: "OversizeServices"),
- .product(name: "CachedAsyncImage", package: "swiftui-cached-async-image"),
- .product(name: "OversizeCore", package: "OversizeCore"),
- .product(name: "OversizeNetwork", package: "OversizeNetwork"),
- .product(name: "OversizeModels", package: "OversizeModels"),
+ .product(name: "EffectsLibrary", package: "effects-library"),
]
),
.target(
diff --git a/Sources/OversizeCalendarKit/Pickers/AlertPicker.swift b/Sources/OversizeCalendarKit/Pickers/AlertPicker.swift
index bdd4492..68fbfb8 100644
--- a/Sources/OversizeCalendarKit/Pickers/AlertPicker.swift
+++ b/Sources/OversizeCalendarKit/Pickers/AlertPicker.swift
@@ -33,7 +33,7 @@ public struct AlarmPicker: View {
}
}
}
- .surfaceContentRowInsets()
+ .surfaceContentRowMargins()
}
.backgroundSecondary()
.leadingBar {
diff --git a/Sources/OversizeAdsKit/AdView.swift b/Sources/OversizeKit/AdsKit/AdView.swift
similarity index 64%
rename from Sources/OversizeAdsKit/AdView.swift
rename to Sources/OversizeKit/AdsKit/AdView.swift
index 8417c99..aeb1c44 100644
--- a/Sources/OversizeAdsKit/AdView.swift
+++ b/Sources/OversizeKit/AdsKit/AdView.swift
@@ -5,7 +5,6 @@
import CachedAsyncImage
import OversizeCore
-import OversizeKit
import OversizeModels
import OversizeNetwork
import OversizeServices
@@ -50,33 +49,35 @@ public struct AdView: View {
}
}
- func premiumBanner(appAd: Components.Schemas.AppShort) -> some View {
+ func premiumBanner(appAd: Components.Schemas.Ad) -> some View {
HStack(spacing: .zero) {
- CachedAsyncImage(url: URL(string: "\(Info.links?.company.cdnString ?? "")/assets/apps/\(appAd.address)/icon.png"), urlCache: .imageCache, content: {
- $0
- .resizable()
- .frame(width: 64, height: 64)
- .mask(RoundedRectangle(cornerRadius: .large,
- style: .continuous))
- .overlay(
- RoundedRectangle(cornerRadius: 16,
- style: .continuous)
- .stroke(lineWidth: 1)
- .opacity(0.15)
- )
- .onTapGesture {
- isShowProduct.toggle()
- }
+ if let iconUrl = appAd.iconURL, let url = URL(string: iconUrl) {
+ CachedAsyncImage(url: url, urlCache: .imageCache, content: {
+ $0
+ .resizable()
+ .frame(width: 64, height: 64)
+ .mask(RoundedRectangle(cornerRadius: .large,
+ style: .continuous))
+ .overlay(
+ RoundedRectangle(cornerRadius: 16,
+ style: .continuous)
+ .stroke(lineWidth: 1)
+ .opacity(0.15)
+ )
+ .onTapGesture {
+ isShowProduct.toggle()
+ }
- }, placeholder: {
- RoundedRectangle(cornerRadius: .large, style: .continuous)
- .fillSurfaceSecondary()
- .frame(width: 64, height: 64)
- })
+ }, placeholder: {
+ RoundedRectangle(cornerRadius: .large, style: .continuous)
+ .fillSurfaceSecondary()
+ .frame(width: 64, height: 64)
+ })
+ }
VStack(alignment: .leading, spacing: .xxxSmall) {
HStack {
- Text(appAd.name)
+ Text(appAd.title)
.subheadline(.bold)
.onSurfaceHighEmphasisForegroundColor()
@@ -86,7 +87,7 @@ public struct AdView: View {
}
}
- Text(appAd.title)
+ Text(appAd.description)
.subheadline()
.onSurfaceMediumEmphasisForegroundColor()
}
diff --git a/Sources/OversizeAdsKit/AdViewModel.swift b/Sources/OversizeKit/AdsKit/AdViewModel.swift
similarity index 89%
rename from Sources/OversizeAdsKit/AdViewModel.swift
rename to Sources/OversizeKit/AdsKit/AdViewModel.swift
index a17cbf0..98f2869 100644
--- a/Sources/OversizeAdsKit/AdViewModel.swift
+++ b/Sources/OversizeKit/AdsKit/AdViewModel.swift
@@ -18,7 +18,7 @@ public class AdViewModel: ObservableObject {
public init() {}
public func fetchAd() async {
- let result = await networkService.fetchApps()
+ let result = await networkService.fetchAds()
switch result {
case let .success(ads):
guard let ad = ads.filter({ $0.appStoreId != Info.app.appStoreID }).randomElement() else {
@@ -36,7 +36,7 @@ extension AdViewModel {
enum State {
case initial
case loading
- case result(Components.Schemas.AppShort)
+ case result(Components.Schemas.Ad)
case error(AppError)
}
}
diff --git a/Sources/OversizeKit/LauncherKit/LauncherViewModel.swift b/Sources/OversizeKit/LauncherKit/LauncherViewModel.swift
index d32203a..a8e4e0c 100644
--- a/Sources/OversizeKit/LauncherKit/LauncherViewModel.swift
+++ b/Sources/OversizeKit/LauncherKit/LauncherViewModel.swift
@@ -4,6 +4,7 @@
//
import OversizeCore
+import OversizeNetwork
import OversizeServices
import OversizeStoreService
import OversizeUI
@@ -20,15 +21,18 @@ public final class LauncherViewModel: ObservableObject {
@Injected(\.settingsService) var settingsService
@Injected(\.appStoreReviewService) var reviewService: AppStoreReviewServiceProtocol
@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: StoreSpecialOfferEventType = .oldUser
+ @AppStorage("AppState.LastClosedSpecialOfferSheet") var lastClosedSpecialOffer: String = ""
@Published public var pinCodeField: String = ""
@Published public var authState: LockscreenViewState = .locked
@Published var activeFullScreenSheet: FullScreenSheet?
@Published var isShowSplashScreen: Bool = true
+ let expectedFormat = Date.ISO8601FormatStyle()
+
var isShowLockscreen: Bool {
if FeatureFlags.secure.lookscreen ?? false {
if settingsService.pinCodeEnabend || settingsService.biometricEnabled, authState != .unlocked {
@@ -49,7 +53,7 @@ extension LauncherViewModel {
case onboarding
case payWall
case rate
- case specialOffer(event: StoreSpecialOfferEventType)
+ case specialOffer(event: Components.Schemas.SpecialOffer)
public var id: Int {
switch self {
case .onboarding: return 0
@@ -130,13 +134,33 @@ public extension LauncherViewModel {
}
}
- func checkSpecialOffer() {
- if !isPremium {
- for event in StoreSpecialOfferEventType.allCases where event.isNow {
- if activeFullScreenSheet == nil, lastClosedSpecialOffer != event {
- activeFullScreenSheet = .specialOffer(event: event)
+ 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
+ }
+ }
+
+ func checkDateInSelectedPeriod(startDate: Date, endDate: Date) -> Bool {
+ if startDate < endDate {
+ return (startDate ... endDate).contains(Date())
+ } else {
+ return false
+ }
+ }
+
+ func checkSpecialOffer() {
+ if !isPremium, activeFullScreenSheet == nil {
+ Task {
+ await fetchAndSetSpecialOffer()
+ }
}
}
}
diff --git a/Sources/OversizeKit/LockscreenKit/LockscreenView.swift b/Sources/OversizeKit/LockscreenKit/LockscreenView.swift
index b5d1968..0833b73 100644
--- a/Sources/OversizeKit/LockscreenKit/LockscreenView.swift
+++ b/Sources/OversizeKit/LockscreenKit/LockscreenView.swift
@@ -255,6 +255,11 @@ public struct LockscreenView: View {
.font(.system(size: 26))
.foregroundColor(Color.onBackgroundHighEmphasis)
.frame(width: 24, height: 24, alignment: .center)
+ case .opticID:
+ Image(systemName: "opticid")
+ .font(.system(size: 26))
+ .foregroundColor(Color.onBackgroundHighEmphasis)
+ .frame(width: 24, height: 24, alignment: .center)
}
}
diff --git a/Sources/OversizeKit/SettingsKit/Views/About/About/AboutView.swift b/Sources/OversizeKit/SettingsKit/Views/About/About/AboutView.swift
index d506acc..eaf732b 100644
--- a/Sources/OversizeKit/SettingsKit/Views/About/About/AboutView.swift
+++ b/Sources/OversizeKit/SettingsKit/Views/About/About/AboutView.swift
@@ -15,6 +15,7 @@ import SwiftUI
// swiftlint:disable all
#if os(iOS)
import MessageUI
+
public struct AboutView: View {
@Environment(\.verticalSizeClass) private var verticalSizeClass
@Environment(\.isPortrait) var isPortrait
@@ -208,7 +209,6 @@ import SwiftUI
.foregroundColor(.onBackgroundHighEmphasis)
.padding(.horizontal, isLargeScreen ? 72 : 52)
.padding(.bottom, .large)
-
.multilineTextAlignment(.center)
soclal
@@ -219,7 +219,7 @@ import SwiftUI
if let reviewUrl = Info.url.appStoreReview, let id = Info.app.appStoreID, !id.isEmpty, let appName = Info.app.name {
Link(destination: reviewUrl) {
Row("Rate \(appName) on App Store") {
- rateSettingsIcon
+ rateSettingsIcon.icon()
}
}
.buttonStyle(.row)
@@ -239,7 +239,7 @@ import SwiftUI
Row(L10n.About.suggestIdea) {
isShowMail.toggle()
} leading: {
- ideaSettingsIcon
+ ideaSettingsIcon.icon()
}
.buttonStyle(.row)
@@ -253,7 +253,7 @@ import SwiftUI
Row(L10n.Settings.shareApplication) {
isSharePresented.toggle()
} leading: {
- shareSettingsIcon
+ shareSettingsIcon.icon()
}
.sheet(isPresented: $isSharePresented) {
ActivityViewController(activityItems: [shareUrl])
@@ -491,7 +491,7 @@ import SwiftUI
let appName = Info.app.name,
let appBuild = Info.app.build
{
- Text("© 2022 \(developerName). \(appName) \(appVersion) (\(appBuild))")
+ Text("© 2023 \(developerName). \(appName) \(appVersion) (\(appBuild))")
.footnote()
.foregroundColor(.onBackgroundDisabled)
} else {
diff --git a/Sources/OversizeKit/SettingsKit/Views/About/About/AboutViewModel.swift b/Sources/OversizeKit/SettingsKit/Views/About/About/AboutViewModel.swift
index 00b9693..0379448 100644
--- a/Sources/OversizeKit/SettingsKit/Views/About/About/AboutViewModel.swift
+++ b/Sources/OversizeKit/SettingsKit/Views/About/About/AboutViewModel.swift
@@ -22,7 +22,6 @@ public class AboutViewModel: ObservableObject {
async let resultInfo = networkService.fetchInfo()
if case let .success(apps) = await resultApps, case let .success(info) = await resultInfo {
state = .result(apps.filter { $0.appStoreId != Info.app.appStoreID }, info)
-
} else {
state = .error(.network(type: .noResponse))
}
diff --git a/Sources/OversizeKit/SettingsKit/Views/About/FeedbackView.swift b/Sources/OversizeKit/SettingsKit/Views/About/FeedbackView.swift
index cf97d05..976faa1 100644
--- a/Sources/OversizeKit/SettingsKit/Views/About/FeedbackView.swift
+++ b/Sources/OversizeKit/SettingsKit/Views/About/FeedbackView.swift
@@ -43,7 +43,7 @@ public struct FeedbackView: View {
if let reviewUrl = Info.url.appStoreReview, let id = Info.app.appStoreID, !id.isEmpty {
Link(destination: reviewUrl) {
Row(L10n.Settings.feedbakAppStore) {
- heartIcon
+ heartIcon.icon()
}
}
.buttonStyle(.row)
@@ -65,7 +65,7 @@ public struct FeedbackView: View {
Row(L10n.Settings.feedbakAuthor) {
isShowMail.toggle()
} leading: {
- mailIcon
+ mailIcon.icon()
}
.buttonStyle(.row)
@@ -77,7 +77,7 @@ public struct FeedbackView: View {
if let sendMailUrl = Info.url.developerSendMail {
Link(destination: sendMailUrl) {
Row(L10n.Settings.feedbakAuthor) {
- mailIcon
+ mailIcon.icon()
}
}
.buttonStyle(.row)
@@ -89,7 +89,7 @@ public struct FeedbackView: View {
if let telegramChatUrl = Info.url.appTelegramChat, let id = Info.app.telegramChatID, !id.isEmpty {
Link(destination: telegramChatUrl) {
Row(L10n.Settings.telegramChat) {
- chatIcon
+ chatIcon.icon()
}
}
.buttonStyle(.row)
diff --git a/Sources/OversizeKit/SettingsKit/Views/About/OurResorsesView.swift b/Sources/OversizeKit/SettingsKit/Views/About/OurResorsesView.swift
index cac6b70..745cb19 100644
--- a/Sources/OversizeKit/SettingsKit/Views/About/OurResorsesView.swift
+++ b/Sources/OversizeKit/SettingsKit/Views/About/OurResorsesView.swift
@@ -35,7 +35,7 @@ public struct OurResorsesView: View {
if let gitHubUrl = URL(string: "https://github.com/oversizedev") {
Link(destination: gitHubUrl) {
Row("GitHub Open Source") {
- githubIcon
+ githubIcon.icon()
}
}
.buttonStyle(.row)
@@ -44,7 +44,7 @@ public struct OurResorsesView: View {
if let figmaUrl = URL(string: "https://www.figma.com/@oversizedesign") {
Link(destination: figmaUrl) {
Row("Figma Community") {
- figmaIcon
+ figmaIcon.icon()
}
}
.buttonStyle(.row)
diff --git a/Sources/OversizeKit/SettingsKit/Views/About/SupportView.swift b/Sources/OversizeKit/SettingsKit/Views/About/SupportView.swift
index af969c6..cc4c5c4 100644
--- a/Sources/OversizeKit/SettingsKit/Views/About/SupportView.swift
+++ b/Sources/OversizeKit/SettingsKit/Views/About/SupportView.swift
@@ -56,7 +56,7 @@ public struct SupportView: View {
Row("Contact Us") {
isShowMail.toggle()
} leading: {
- mailIcon
+ mailIcon.icon()
}
.buttonStyle(.row)
@@ -68,7 +68,7 @@ public struct SupportView: View {
if let sendMailUrl = Info.url.developerSendMail {
Link(destination: sendMailUrl) {
Row("Contact Us") {
- mailIcon
+ mailIcon.icon()
}
}
.buttonStyle(.row)
@@ -80,7 +80,7 @@ public struct SupportView: View {
if let telegramChatUrl = Info.url.appTelegramChat, let id = Info.app.telegramChatID, !id.isEmpty {
Link(destination: telegramChatUrl) {
Row(L10n.Settings.telegramChat) {
- chatIcon
+ chatIcon.icon()
}
}
.buttonStyle(.row)
diff --git a/Sources/OversizeKit/SettingsKit/Views/Apperance/AppearanceSettingView.swift b/Sources/OversizeKit/SettingsKit/Views/Apperance/AppearanceSettingView.swift
index 701b88d..2423055 100644
--- a/Sources/OversizeKit/SettingsKit/Views/Apperance/AppearanceSettingView.swift
+++ b/Sources/OversizeKit/SettingsKit/Views/Apperance/AppearanceSettingView.swift
@@ -109,7 +109,6 @@ import SwiftUI
// swiftlint:disable multiple_closures_with_trailing_closure superfluous_disable_command
.navigationTitle("Appearance")
-
.preferredColorScheme(theme.appearance.colorScheme)
}
@@ -225,7 +224,7 @@ import SwiftUI
Row("Fonts") {
pageDestenation = .font
} leading: {
- textIcon
+ textIcon.icon()
}
.rowArrow()
.premium()
@@ -235,7 +234,7 @@ import SwiftUI
Row("Borders") {
pageDestenation = .border
} leading: {
- borderIcon
+ borderIcon.icon()
}
.premium()
}
@@ -256,7 +255,7 @@ import SwiftUI
Row("Radius") {
pageDestenation = .radius
} leading: {
- radiusIcon
+ radiusIcon.icon()
}
.rowArrow()
.premium()
diff --git a/Sources/OversizeKit/SettingsKit/Views/Apperance/BorderSettongView.swift b/Sources/OversizeKit/SettingsKit/Views/Apperance/BorderSettongView.swift
index 9c7db57..5092940 100644
--- a/Sources/OversizeKit/SettingsKit/Views/Apperance/BorderSettongView.swift
+++ b/Sources/OversizeKit/SettingsKit/Views/Apperance/BorderSettongView.swift
@@ -14,7 +14,7 @@ public struct BorderSettingView: View {
public var body: some View {
PageView("Borders in app") {
settings
- .surfaceContentRowInsets()
+ .surfaceContentRowMargins()
}
.leadingBar {
// if !isPortrait, verticalSizeClass == .regular {
@@ -63,7 +63,7 @@ public struct BorderSettingView: View {
}
}
.surfaceStyle(.secondary)
- .surfaceContentInsets(.small)
+ .surfaceContentMargins(.small)
.padding(.horizontal, Space.medium)
.padding(.bottom, Space.xxSmall)
diff --git a/Sources/OversizeKit/SettingsKit/Views/Apperance/FontSettingView.swift b/Sources/OversizeKit/SettingsKit/Views/Apperance/FontSettingView.swift
index b09fbf7..ed4fba7 100644
--- a/Sources/OversizeKit/SettingsKit/Views/Apperance/FontSettingView.swift
+++ b/Sources/OversizeKit/SettingsKit/Views/Apperance/FontSettingView.swift
@@ -32,7 +32,6 @@ public struct FontSettingView: View {
}
.padding(.horizontal)
.padding(.bottom)
-
.navigationBar("Fonts", style: .fixed($offset)) {
BarButton(.back)
} trailingBar: {} bottomBar: {}
diff --git a/Sources/OversizeKit/SettingsKit/Views/Apperance/RadiusSettingView.swift b/Sources/OversizeKit/SettingsKit/Views/Apperance/RadiusSettingView.swift
index 2b47884..c42d83a 100644
--- a/Sources/OversizeKit/SettingsKit/Views/Apperance/RadiusSettingView.swift
+++ b/Sources/OversizeKit/SettingsKit/Views/Apperance/RadiusSettingView.swift
@@ -14,7 +14,7 @@ struct RadiusSettingView: View {
public var body: some View {
PageView("Radius") {
settings
- .surfaceContentRowInsets()
+ .surfaceContentRowMargins()
}
.leadingBar {
BarButton(.back)
diff --git a/Sources/OversizeKit/SettingsKit/Views/Notifications/NotificationsSettingsView.swift b/Sources/OversizeKit/SettingsKit/Views/Notifications/NotificationsSettingsView.swift
index c72f575..6129783 100644
--- a/Sources/OversizeKit/SettingsKit/Views/Notifications/NotificationsSettingsView.swift
+++ b/Sources/OversizeKit/SettingsKit/Views/Notifications/NotificationsSettingsView.swift
@@ -20,7 +20,7 @@ import SwiftUI
public var body: some View {
PageView(L10n.Settings.notifications) {
soundsAndVibrations
- .surfaceContentRowInsets()
+ .surfaceContentRowMargins()
}
.leadingBar {
BarButton(.back)
diff --git a/Sources/OversizeKit/SettingsKit/Views/Security/SecuritySettingsView.swift b/Sources/OversizeKit/SettingsKit/Views/Security/SecuritySettingsView.swift
index d70f40e..bc89bab 100644
--- a/Sources/OversizeKit/SettingsKit/Views/Security/SecuritySettingsView.swift
+++ b/Sources/OversizeKit/SettingsKit/Views/Security/SecuritySettingsView.swift
@@ -60,7 +60,6 @@ import SwiftUI
Row(biometricService.biometricType.rawValue) {
Image(systemName: biometricImageName)
.foregroundColor(Color.onBackgroundHighEmphasis)
-
.font(.system(size: 20, weight: .semibold))
.frame(width: 24, height: 24, alignment: .center)
}
@@ -80,7 +79,7 @@ import SwiftUI
})
) {
Row(L10n.Security.pinCode) {
- Image.Security.lock
+ Image.Security.lock.icon()
}
}.sheet(item: $isSetPINCodeSheet) { sheet in
SetPINCodeView(action: sheet)
@@ -158,12 +157,12 @@ import SwiftUI
switch biometricService.biometricType {
case .none:
return ""
-
case .touchID:
return "touchid"
-
case .faceID:
return "faceid"
+ case .opticID:
+ return "opticid"
}
}
}
diff --git a/Sources/OversizeKit/SettingsKit/Views/SettingsView.swift b/Sources/OversizeKit/SettingsKit/Views/SettingsView.swift
index dbd5f10..0a8d144 100644
--- a/Sources/OversizeKit/SettingsKit/Views/SettingsView.swift
+++ b/Sources/OversizeKit/SettingsKit/Views/SettingsView.swift
@@ -271,7 +271,6 @@ import SwiftUI
helpIcon.icon()
}
.rowArrow(isShowArrow)
-
.buttonStyle(.row)
.sheet(isPresented: $isShowSupport) {
SupportView()
@@ -287,7 +286,6 @@ import SwiftUI
chatIcon.icon()
}
.rowArrow(isShowArrow)
-
.buttonStyle(.row)
.sheet(isPresented: $isShowFeedback) {
FeedbackView()
diff --git a/Sources/OversizeKit/SettingsKit/Views/SoundAndVibration/SoundsAndVibrationsSettingsView.swift b/Sources/OversizeKit/SettingsKit/Views/SoundAndVibration/SoundsAndVibrationsSettingsView.swift
index 031d1fb..ed0f81c 100644
--- a/Sources/OversizeKit/SettingsKit/Views/SoundAndVibration/SoundsAndVibrationsSettingsView.swift
+++ b/Sources/OversizeKit/SettingsKit/Views/SoundAndVibration/SoundsAndVibrationsSettingsView.swift
@@ -70,7 +70,7 @@ import SwiftUI
if FeatureFlags.app.vibration.valueOrFalse {
Switch(isOn: $settingsService.vibrationEnabled) {
Row(L10n.Settings.vibration) {
- vibrationIcon
+ vibrationIcon.icon()
}
}
}
diff --git a/Sources/OversizeKit/SettingsKit/Views/iCloud/iCloudSettingsView.swift b/Sources/OversizeKit/SettingsKit/Views/iCloud/iCloudSettingsView.swift
index d83cc16..59eab6b 100644
--- a/Sources/OversizeKit/SettingsKit/Views/iCloud/iCloudSettingsView.swift
+++ b/Sources/OversizeKit/SettingsKit/Views/iCloud/iCloudSettingsView.swift
@@ -20,7 +20,7 @@ import SwiftUI
public var body: some View {
PageView(L10n.Title.synchronization) {
iOSSettings
- .surfaceContentRowInsets()
+ .surfaceContentRowMargins()
}
.leadingBar {
BarButton(.back)
@@ -44,7 +44,7 @@ import SwiftUI
if FeatureFlags.app.сloudKit.valueOrFalse {
Switch(isOn: $settingsService.cloudKitEnabled) {
Row(L10n.Settings.iCloudSync) {
- Image.Weather.Cloud.square
+ Image.Weather.Cloud.square.icon()
}
.premium()
}
@@ -57,6 +57,7 @@ import SwiftUI
subtitle: settingsService.cloudKitCVVEnabled ? L10n.Security.iCloudSyncCVVDescriptionCloudKit : L10n.Security.iCloudSyncCVVDescriptionLocal)
{
Image.Security.cloudLock
+ .icon()
.frame(width: 24, height: 24)
}
.premium()
@@ -67,7 +68,7 @@ import SwiftUI
if FeatureFlags.app.healthKit.valueOrFalse {
Switch(isOn: $settingsService.healthKitEnabled) {
Row("HealthKit synchronization", subtitle: "After switching on, data from the Health app will be downloaded") {
- Image.Romantic.heart
+ Image.Romantic.heart.icon()
}
}
}
diff --git a/Sources/OversizeKit/StoreKit/StoreScreen/StoreSpecialOfferView.swift b/Sources/OversizeKit/StoreKit/StoreScreen/StoreSpecialOfferView.swift
index a1d0b70..292b1bc 100644
--- a/Sources/OversizeKit/StoreKit/StoreScreen/StoreSpecialOfferView.swift
+++ b/Sources/OversizeKit/StoreKit/StoreScreen/StoreSpecialOfferView.swift
@@ -3,8 +3,12 @@
// StoreSpecialOfferView.swift
//
+import CachedAsyncImage
+import EffectsLibrary
import OversizeComponents
+import OversizeCore
import OversizeLocalizable
+import OversizeNetwork
import OversizeResources
import OversizeServices
import OversizeStoreService
@@ -16,22 +20,47 @@ public struct StoreSpecialOfferView: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.isPremium) private var isPremium
@StateObject private var viewModel: StoreViewModel
- @AppStorage("AppState.LastClosedSpecialOfferSheet") private var lastClosedSpecialOffer: StoreSpecialOfferEventType = .oldUser
+ @AppStorage("AppState.LastClosedSpecialOfferSheet") private var lastClosedSpecialOffer: String = "0"
@State private var isShowAllPlans = false
@State private var offset: CGFloat = 0
- private let event: StoreSpecialOfferEventType
+ private let event: Components.Schemas.SpecialOffer
@State var trialDaysPeriodText: String = ""
+ @State var salePercent: Decimal = 0
- public init(event: StoreSpecialOfferEventType = .newUser) {
+ public init(event: Components.Schemas.SpecialOffer) {
self.event = event
_viewModel = StateObject(wrappedValue: StoreViewModel(specialOfferMode: true))
}
public var body: some View {
#if os(iOS)
- PageView { offset = $0 } content: {
+ Group {
+ if #available(iOS 16.0, *) {
+ newPage
+ } else {
+ oldPage
+ }
+ }
+
+ .onChange(of: isPremium) { status in
+ if status {
+ dismiss()
+ }
+ }
+ .task {
+ await viewModel.fetchData()
+ }
+ #else
+ EmptyView()
+ #endif
+ }
+
+ @available(iOS 16.0, *)
+ var newPage: some View {
+ NavigationStack {
+ Page(badgeText, onScroll: handleOffset) {
Group {
switch viewModel.state {
case .initial:
@@ -48,47 +77,109 @@ public struct StoreSpecialOfferView: View {
ProgressView()
case let .result(data):
content(data: data)
+ .background {
+ effectsView
+ }
case let .error(error):
ErrorView(error)
}
}
- .paddingContent(.horizontal)
}
.backgroundLinerGradient(LinearGradient(colors: [.backgroundPrimary, .backgroundSecondary], startPoint: .top, endPoint: .center))
- .titleLabel {
- PremiumLabel(image: Resource.Store.zap, text: Info.store.subscriptionsName, size: .medium)
- }
- .trailingBar {
- BarButton(.closeAction {
- lastClosedSpecialOffer = event
- dismiss()
- })
- }
- .bottomToolbar(style: .none) {
- VStack(spacing: .zero) {
- StorePaymentButtonBar()
+ .bottomToolbar(style: .gradient) {
+ VStack(spacing: .small) {
+ productsLust
+ .padding(.horizontal, .medium)
+
+ StorePaymentButtonBar(showDescription: false)
.environmentObject(viewModel)
- .padding(.horizontal, 8)
+ .padding(.horizontal, .small)
}
}
- .onChange(of: isPremium) { status in
- if status {
- dismiss()
+ .toolbar {
+ ToolbarItem(placement: .topBarLeading) {
+ Button {
+ lastClosedSpecialOffer = event.id
+ dismiss()
+ } label: {
+ Image.Base.close.icon()
+ }
}
}
- .task {
- await viewModel.fetchData()
- }
- #else
+ }
+ }
+
+ @ViewBuilder
+ var effectsView: some View {
+ switch event.effect {
+ case .snow:
+ SnowView(config: .init(
+ intensity: .low,
+ lifetime: .long,
+ initialVelocity: .medium,
+ fadeOut: .slow,
+ spreadRadius: .high
+ ))
+ .offset(y: -150)
+ default:
EmptyView()
- #endif
+ }
+ }
+
+ var oldPage: some View {
+ PageView { offset = $0 } content: {
+ Group {
+ switch viewModel.state {
+ case .initial:
+ VStack {
+ Spacer()
+ HStack {
+ Spacer()
+ ProgressView()
+ Spacer()
+ }
+ Spacer()
+ }
+ case .loading:
+ ProgressView()
+ case let .result(data):
+ content(data: data)
+ case let .error(error):
+ ErrorView(error)
+ }
+ }
+ .paddingContent(.horizontal)
+ }
+ .backgroundLinerGradient(LinearGradient(colors: [.backgroundPrimary, .backgroundSecondary], startPoint: .top, endPoint: .center))
+ .titleLabel {
+ PremiumLabel(image: Resource.Store.zap, text: Info.store.subscriptionsName, size: .medium)
+ }
+ .trailingBar {
+ BarButton(.closeAction {
+ lastClosedSpecialOffer = event.id
+ dismiss()
+ })
+ }
+ .bottomToolbar(style: .none) {
+ VStack(spacing: .zero) {
+ productsLust
+ StorePaymentButtonBar()
+ .environmentObject(viewModel)
+ .padding(.horizontal, 8)
+ }
+ }
+ }
+
+ func handleOffset(_ scrollOffset: CGPoint, visibleHeaderRatio _: CGFloat) {
+ offset = -scrollOffset.y
+ // visibleRatio = visibleHeaderRatio
}
var imageSize: CGFloat {
if screenSize.height > 830 {
- return 144
- } else if screenSize.height > 800 {
- return 98
+ return 200
+ } else if screenSize.height > 700 {
+ return 160
} else {
return 64
}
@@ -97,31 +188,29 @@ public struct StoreSpecialOfferView: View {
@ViewBuilder
private func content(data: StoreKitProducts) -> some View {
ScrollViewReader { value in
-
VStack(spacing: .medium) {
VStack(spacing: .zero) {
- if screenSize.height > 810 {
+ PremiumLabel(image: Resource.Store.zap, text: Info.store.subscriptionsName, size: .medium)
+ .offset(y: -32)
+
+ if screenSize.height > 850 {
Spacer()
}
- AsyncIllustrationView(event.specialOfferImageURL)
- .frame(width: imageSize, height: imageSize)
- .padding(.bottom, screenSize.height > 810 ? 38 : 8)
-
- VStack(spacing: .xSmall) {
- Text(event.specialOfferSubtitle.uppercased())
- .footnote(.semibold)
- .onBackgroundMediumEmphasisForegroundColor()
-
- Text(event.isNeedTrialDescription ? event.specialOfferTitle + " " + trialDaysPeriodText : event.specialOfferTitle)
- .title(.bold)
- .foregroundColor(.onSurfaceHighEmphasis)
-
- Text(event.specialOfferDescription)
- .foregroundColor(.onSurfaceMediumEmphasis)
- .headline(.semibold)
+ if let imageURLString = event.imageURL, let imageURL = URL(string: imageURLString) {
+ CachedAsyncImage(url: imageURL, urlCache: .imageCache) { image in
+ image
+ .resizable()
+ .frame(width: imageSize, height: imageSize)
+ } placeholder: {
+ Circle()
+ .fill(Color.surfaceTertiary)
+ .frame(width: imageSize, height: imageSize)
+ }
+ .padding(.bottom, .small)
+ .zIndex(999_999_999)
}
- .multilineTextAlignment(.center)
+ titleTexts
Button {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
@@ -137,7 +226,6 @@ public struct StoreSpecialOfferView: View {
.padding(.bottom, screenSize.height > 810 ? .small : .zero)
Spacer()
- productsLust(data: data)
}
.frame(height: screenSize.safeAreaHeight - 235)
.overlay {
@@ -145,7 +233,7 @@ public struct StoreSpecialOfferView: View {
.stroke(style: StrokeStyle(lineWidth: 5, lineCap: .round))
.foregroundColor(.onSurfaceHighEmphasis.opacity(0.3))
.frame(width: 30)
- .offset(y: screenSize.safeAreaHeight - 370)
+ .offset(y: screenSize.safeAreaHeight - 280)
.opacity(1 - (offset * 0.01))
}
@@ -154,6 +242,7 @@ public struct StoreSpecialOfferView: View {
.title()
.onBackgroundHighEmphasisForegroundColor()
.multilineTextAlignment(.center)
+ .fixedSize()
.padding(.top, .large)
StoreFeaturesLargeView()
@@ -164,46 +253,137 @@ public struct StoreSpecialOfferView: View {
.id(10)
SubscriptionPrivacyView(products: data)
+ .padding(.horizontal, .medium)
+ .padding(.bottom, .large)
}
.padding(.bottom, 180)
-
- .onAppear {
- Task {
- // When this view appears, get the latest subscription status.
- await viewModel.updateSubscriptionStatus(products: data)
- }
+ .task {
+ await viewModel.updateSubscriptionStatus(products: data)
}
.onChange(of: data.purchasedAutoRenewable) { _ in
Task {
- // When `purchasedSubscriptions` changes, get the latest subscription status.
await viewModel.updateSubscriptionStatus(products: data)
}
}
}
}
+ var titleTexts: some View {
+ VStack(spacing: .zero) {
+ Text(badgeText.uppercased())
+ .footnote(.semibold)
+ .onBackgroundMediumEmphasisForegroundColor()
+ .padding(.bottom, .xxxSmall)
+
+ Text(headline)
+ .title(.bold)
+ .foregroundColor(.onSurfaceHighEmphasis)
+ .frame(maxWidth: .infinity, alignment: .center)
+
+ Text(event.title)
+ .largeTitle(.heavy)
+ .foregroundColor(titleColor)
+
+ Text(description)
+ .foregroundColor(.onSurfaceMediumEmphasis)
+ .headline(.regular)
+ .padding(.top, .xSmall)
+ }
+ .multilineTextAlignment(.center)
+ .background {
+ RoundedRectangle(cornerRadius: 28, style: .continuous)
+ .fill(LinearGradient(
+ stops: [
+ .init(color: Color.surfaceSecondary, location: 0),
+ .init(color: Color.surfaceSecondary.opacity(0), location: 0.7),
+ ],
+ startPoint: .top,
+ endPoint: .bottom
+ ))
+ .overlay(
+ RoundedRectangle(cornerRadius: 28, style: .continuous)
+ .strokeBorder(
+ LinearGradient(
+ stops: [
+ .init(color: Color.surfaceTertiary, location: 0),
+ .init(color: Color.surfaceSecondary.opacity(0), location: 0.7),
+ ],
+ startPoint: .top,
+ endPoint: .bottom
+ ),
+ lineWidth: 2
+ )
+ )
+ .padding(.top, -54)
+ .padding(.bottom, -100)
+ }
+ .padding(.horizontal, .small)
+ }
+
+ var badgeText: String {
+ if let badge = event.badge {
+ return textPrepere(badge)
+ } else {
+ return ""
+ }
+ }
+
+ var headline: String {
+ if let headline = event.headline {
+ return textPrepere(headline)
+ } else {
+ return ""
+ }
+ }
+
+ var titleColor: Color {
+ if let accentColor = event.accentColor {
+ return Color(hex: accentColor)
+ } else {
+ return Color.onBackgroundHighEmphasis
+ }
+ }
+
+ var description: String {
+ if let description = event.description {
+ return textPrepere(description)
+ } else {
+ return ""
+ }
+ }
+
+ func textPrepere(_ text: String) -> String {
+ text
+ .replacingOccurrences(of: "", with: salePercent.toString)
+ .replacingOccurrences(of: "", with: trialDaysPeriodText)
+ .replacingOccurrences(of: "", with: Info.store.subscriptionsName)
+ }
+
@ViewBuilder
- func productsLust(data: StoreKitProducts) -> some View {
- VStack(spacing: .small) {
- ForEach(viewModel.availableSubscriptions) { product in
- if product.isOffer {
- StoreProductView(product: product, products: data, isSelected: .constant(false)) {
- Task {
- await viewModel.buy(product: product)
+ var productsLust: some View {
+ if case let .result(data) = viewModel.state {
+ VStack(spacing: .small) {
+ ForEach(viewModel.availableSubscriptions) { product in
+ if product.isOffer {
+ StoreProductView(product: product, products: data, isSelected: .constant(false)) {
+ Task {
+ await viewModel.buy(product: product)
+ }
}
- }
- .onAppear {
- if product.type == .autoRenewable, let offer = product.subscription?.introductoryOffer {
- trialDaysPeriodText = viewModel.storeKitService.daysLabel(offer.period.value, unit: offer.period.unit)
+ .onAppear {
+ if product.type == .autoRenewable, let offer = product.subscription?.introductoryOffer {
+ trialDaysPeriodText = viewModel.storeKitService.daysLabel(offer.period.value, unit: offer.period.unit)
+ salePercent = viewModel.storeKitService.salePercent(product: product, products: data)
+ }
}
}
}
- }
- ForEach(data.nonConsumable) { product in
- if product.isOffer {
- StoreProductView(product: product, products: data, isSelected: .constant(false)) {
- Task {
- await viewModel.buy(product: product)
+ ForEach(data.nonConsumable) { product in
+ if product.isOffer {
+ StoreProductView(product: product, products: data, isSelected: .constant(false)) {
+ Task {
+ await viewModel.buy(product: product)
+ }
}
}
}
@@ -212,8 +392,8 @@ public struct StoreSpecialOfferView: View {
}
}
-struct StoreSpecialOfferView_Previews: PreviewProvider {
- static var previews: some View {
- StoreSpecialOfferView()
- }
-}
+// struct StoreSpecialOfferView_Previews: PreviewProvider {
+// static var previews: some View {
+// StoreSpecialOfferView()
+// }
+// }
diff --git a/Sources/OversizeKit/StoreKit/StoreScreen/ViewModel/StoreViewModel.swift b/Sources/OversizeKit/StoreKit/StoreScreen/ViewModel/StoreViewModel.swift
index 00dddcb..708b461 100644
--- a/Sources/OversizeKit/StoreKit/StoreScreen/ViewModel/StoreViewModel.swift
+++ b/Sources/OversizeKit/StoreKit/StoreScreen/ViewModel/StoreViewModel.swift
@@ -14,7 +14,7 @@ import StoreKit
import SwiftUI
@MainActor
-class StoreViewModel: ObservableObject {
+public class StoreViewModel: ObservableObject {
enum State {
case initial
case loading
diff --git a/Sources/OversizeKit/StoreKit/ViewModifier/PremiumBlockOverlay.swift b/Sources/OversizeKit/StoreKit/ViewModifier/PremiumBlockOverlay.swift
index fec8d7a..63e1aeb 100644
--- a/Sources/OversizeKit/StoreKit/ViewModifier/PremiumBlockOverlay.swift
+++ b/Sources/OversizeKit/StoreKit/ViewModifier/PremiumBlockOverlay.swift
@@ -10,23 +10,24 @@ import SwiftUI
public struct PremiumBlockOverlay: ViewModifier {
@Environment(\.colorScheme) var colorScheme
@State var isShowPremium = false
- @Environment(\.isPremium) var premiumStatus
+ @Environment(\.isPremium) var isPremium
+ @Binding var isShow: Bool
let title: String
let subtitle: String?
private let closeAction: (() -> Void)?
+
- public init(title: String, subtitle: String?, closeAction: (() -> Void)? = nil) {
+ public init(isShow: Binding = .constant(true), title: String, subtitle: String?, closeAction: (() -> Void)? = nil) {
+ self._isShow = isShow
self.title = title
self.subtitle = subtitle
self.closeAction = closeAction
}
public func body(content: Content) -> some View {
- if premiumStatus {
- content
- } else {
+ if !isPremium && isShow {
ZStack {
content
@@ -42,14 +43,14 @@ public struct PremiumBlockOverlay: ViewModifier {
PremiumLabel(size: .medium)
.padding(.bottom, .medium)
- VStack(spacing: .medium) {
+ VStack(spacing: .small) {
Text(title)
.title()
.foregroundColor(.onSurfaceHighEmphasis)
if let subtitle {
Text(subtitle)
- .headline()
+ .headline(.medium)
.foregroundColor(.onSurfaceMediumEmphasis)
}
}
@@ -81,12 +82,24 @@ public struct PremiumBlockOverlay: ViewModifier {
StoreView()
.colorScheme(colorScheme)
}
+ } else {
+ content
}
}
}
public extension View {
+
+ func premiumContent(_ title: String, subtitle: String?, closeAction: (() -> Void)? = nil) -> some View {
+ modifier(PremiumBlockOverlay(title: title, subtitle: subtitle, closeAction: closeAction))
+ }
+
+ @available(*, deprecated, renamed: "premiumContent", message: "Renamed")
func premiumContent(title: String, subtitle: String?, closeAction: (() -> Void)? = nil) -> some View {
modifier(PremiumBlockOverlay(title: title, subtitle: subtitle, closeAction: closeAction))
}
+
+ func premiumContent(isShow: Binding = .constant(true), title: String, subtitle: String?, closeAction: (() -> Void)? = nil) -> some View {
+ modifier(PremiumBlockOverlay(isShow: isShow, title: title, subtitle: subtitle, closeAction: closeAction))
+ }
}
diff --git a/Sources/OversizeKit/StoreKit/Views/BuyButtonStyle.swift b/Sources/OversizeKit/StoreKit/Views/BuyButtonStyle.swift
index 531bee8..3922ccd 100644
--- a/Sources/OversizeKit/StoreKit/Views/BuyButtonStyle.swift
+++ b/Sources/OversizeKit/StoreKit/Views/BuyButtonStyle.swift
@@ -88,7 +88,7 @@ public struct PaymentButtonStyle: ButtonStyle {
return .small
case .regular:
return .small
- case .large:
+ case .large, .extraLarge:
return .medium
@unknown default:
return .zero
@@ -103,7 +103,7 @@ public struct PaymentButtonStyle: ButtonStyle {
return .xxSmall
case .regular:
return .small
- case .large:
+ case .large, .extraLarge:
return .medium
@unknown default:
return .zero
diff --git a/Sources/OversizeKit/StoreKit/Views/StorePaymentButtonBar.swift b/Sources/OversizeKit/StoreKit/Views/StorePaymentButtonBar.swift
index 4436eae..0399d3b 100644
--- a/Sources/OversizeKit/StoreKit/Views/StorePaymentButtonBar.swift
+++ b/Sources/OversizeKit/StoreKit/Views/StorePaymentButtonBar.swift
@@ -10,18 +10,22 @@ struct StorePaymentButtonBar: View {
let action: (() -> Void)?
let trialNotification: Bool
+ let showDescription: Bool
- init(trialNotification: Bool = false, action: (() -> Void)? = nil) {
+ init(trialNotification: Bool = false, showDescription: Bool = true, action: (() -> Void)? = nil) {
self.trialNotification = trialNotification
self.action = action
+ self.showDescription = showDescription
}
var body: some View {
VStack(spacing: .zero) {
- Text(viewModel.selectedProductButtonDescription)
- .subheadline(.semibold)
- .foregroundColor(.onSurfaceMediumEmphasis)
- .padding(.vertical, 20)
+ if showDescription {
+ Text(viewModel.selectedProductButtonDescription)
+ .subheadline(.semibold)
+ .foregroundColor(.onSurfaceMediumEmphasis)
+ .padding(.vertical, 20)
+ }
Button {
if let selectedProduct = viewModel.selectedProduct {
@@ -50,10 +54,12 @@ struct StorePaymentButtonBar: View {
}
.padding(.bottom, .xxSmall)
.background {
- backgroundView
+ if showDescription {
+ backgroundView
+ }
}
- .padding(.bottom, .small)
- .padding(.horizontal, .small)
+ .padding(.bottom, showDescription ? .small : .zero)
+ .padding(.horizontal, showDescription ? .small : .zero)
}
var backgroundView: some View {
diff --git a/Sources/OversizeKit/StoreKit/Views/StoreProductView.swift b/Sources/OversizeKit/StoreKit/Views/StoreProductView.swift
index 73ac63e..2fa0d08 100644
--- a/Sources/OversizeKit/StoreKit/Views/StoreProductView.swift
+++ b/Sources/OversizeKit/StoreKit/Views/StoreProductView.swift
@@ -47,7 +47,6 @@ public struct StoreProductView: View {
}
}
- // Percentage of decrease = |239.88 - 59.99|/239.88 = 179.89/239.88 = 0.74991662497916 = 74.991662497916%
var saleProcent: String {
if let monthSubscriptionProduct {
let yearPriceMonthly = monthSubscriptionProduct.price * 12
diff --git a/Sources/OversizeNoticeKit/NoticeListView.swift b/Sources/OversizeNoticeKit/NoticeListView.swift
index 6c26da3..5916718 100644
--- a/Sources/OversizeNoticeKit/NoticeListView.swift
+++ b/Sources/OversizeNoticeKit/NoticeListView.swift
@@ -3,97 +3,97 @@
// NoticeListView.swift
//
-import Factory
import OversizeKit
+import OversizeNetwork
import OversizeServices
-import OversizeStoreService
import OversizeUI
import StoreKit
import SwiftUI
public struct NoticeListView: View {
- @Injected(\.appStoreReviewService) var reviewService
@Environment(\.isPremium) var isPremium: Bool
+ @StateObject private var viewModel = NoticeListViewModel()
@State private var isBannerClosed = false
- @State private var showRecommended = false
-
- private var specialOffer: StoreSpecialOfferEventType? {
- var specialOffer: StoreSpecialOfferEventType?
- for event in StoreSpecialOfferEventType.allCases where event.isNow {
- if lastClosedSpecialOffer != event {
- specialOffer = event
- }
- }
- return specialOffer
- }
-
@State private var isShowOfferSheet: Bool = false
- @AppStorage("AppState.LastClosedSpecialOfferBanner") var lastClosedSpecialOffer: StoreSpecialOfferEventType = .oldUser
-
- private var isShowRate: Bool {
- !isBannerClosed && reviewService.isShowReviewBanner
- }
-
- private var isShowNoticeView: Bool {
- isShowRate && (specialOffer != nil && isPremium == false)
- }
public init() {}
public var body: some View {
- if isShowNoticeView {
+ switch viewModel.state {
+ case let .result(offer: offer, isShowRate: isShowRate) where (offer != nil || isShowRate) && !isBannerClosed && !isPremium:
VStack(spacing: .small) {
- if isShowRate, let reviewUrl = Info.url.appStoreReview {
- NoticeView("How do you like the application?") {
- Link(destination: reviewUrl) {
- Text("Good")
- }
- .buttonStyle(.primary(infinityWidth: true))
- .accent()
- .simultaneousGesture(TapGesture().onEnded {
- reviewService.estimate(goodRating: true)
- isBannerClosed = true
- })
+ if isShowRate {
+ rateNoticeView
+ }
+ if let offer {
+ offerView(offer: offer)
+ }
+ }
+ case .initial, .loading, .error, .result, .empty:
+ EmptyView()
+ }
+ }
- Button("Bad") {
- reviewService.estimate(goodRating: false)
- isBannerClosed = true
- }
- .buttonStyle(.tertiary(infinityWidth: true))
+ @ViewBuilder
+ private var rateNoticeView: some View {
+ if let reviewUrl = Info.url.appStoreReview {
+ NoticeView("How do you like the \(Info.app.name ?? "app"))?") {
+ Link(destination: reviewUrl) {
+ Text("Good")
+ }
+ .buttonStyle(.primary(infinityWidth: true))
+ .accent()
+ .simultaneousGesture(TapGesture().onEnded {
+ viewModel.reviewService.estimate(goodRating: true)
+ withAnimation {
+ isBannerClosed = true
+ }
+ })
- } closeAction: {
- reviewService.rewiewBunnerClosed()
+ Button("Bad") {
+ viewModel.reviewService.estimate(goodRating: false)
+ withAnimation {
isBannerClosed = true
}
- .animation(.default, value: isBannerClosed)
}
+ .buttonStyle(.tertiary(infinityWidth: true))
- if let event = specialOffer {
- let url = URL(string: "https://cdn.oversize.design/assets/illustrations/\(event.specialOfferImageURL)")
+ } closeAction: {
+ viewModel.reviewService.rewiewBunnerClosed()
+ withAnimation {
+ isBannerClosed = true
+ }
+ }
+ .animation(.default, value: isBannerClosed)
+ }
+ }
- NoticeView(event.specialOfferBannerTitle,
- subtitle: event.specialOfferDescription,
- imageURL: url)
- {
- Button {
- isShowOfferSheet.toggle()
- } label: {
- Text("Get Free Trial")
- }
- .accent()
+ @ViewBuilder
+ private func offerView(offer: Components.Schemas.SpecialOffer) -> some View {
+ if let imageUrl = offer.imageURL, let url = URL(string: imageUrl) {
+ NoticeView(
+ viewModel.textPrepere(offer.title),
+ subtitle: viewModel.textPrepere(offer.description ?? ""),
+ imageURL: url
+ ) {
+ Button {
+ isShowOfferSheet.toggle()
+ } label: {
+ Text("Accept Offer")
+ }
+ .accent()
- } closeAction: {
- lastClosedSpecialOffer = event
- }
- .sheet(isPresented: $isShowOfferSheet) {
- StoreSpecialOfferView(event: event)
- .systemServices()
- }
+ } closeAction: {
+ viewModel.lastClosedSpecialOffer = offer.id
+ withAnimation {
+ isBannerClosed = true
}
}
- } else {
- EmptyView()
+ .sheet(isPresented: $isShowOfferSheet) {
+ StoreSpecialOfferView(event: offer)
+ .systemServices()
+ }
}
}
}
diff --git a/Sources/OversizeNoticeKit/NoticeListViewModel.swift b/Sources/OversizeNoticeKit/NoticeListViewModel.swift
new file mode 100644
index 0000000..0862c48
--- /dev/null
+++ b/Sources/OversizeNoticeKit/NoticeListViewModel.swift
@@ -0,0 +1,107 @@
+//
+// Copyright © 2023 Alexander Romanov
+// NoticeListViewModel.swift, created on 25.12.2023
+//
+
+import Factory
+import OversizeModels
+import OversizeNetwork
+import OversizeServices
+import OversizeStoreService
+import StoreKit
+import SwiftUI
+
+@MainActor
+public final class NoticeListViewModel: ObservableObject {
+ enum State {
+ case initial
+ case loading
+ case result(offer: Components.Schemas.SpecialOffer?, isShowRate: Bool)
+ case empty
+ case error(AppError)
+ }
+
+ @Injected(\.appStoreReviewService) var reviewService
+ @Injected(\.networkService) var networkService
+ @Injected(\.storeKitService) var storeKitService: StoreKitService
+
+ var isShowReviewBanner: Bool {
+ reviewService.isShowReviewBanner
+ }
+
+ @AppStorage("AppState.LastClosedSpecialOfferBanner") var lastClosedSpecialOffer: String = ""
+
+ private let expectedFormat = Date.ISO8601FormatStyle()
+
+ @Published var state = State.initial
+ @Published public var trialDaysPeriodText: String = ""
+ @Published public var salePercent: Decimal = 0
+
+ public init() {
+ Task {
+ await fetchData()
+ }
+ }
+
+ public func fetchData() async {
+ state = .loading
+ await fetchStoreKitProudcts()
+ await fetchAndSetSpecialOffer()
+ }
+
+ public func fetchStoreKitProudcts() async {
+ let result = await storeKitService.requestProducts()
+ switch result {
+ case let .success(products):
+ if let product = products.autoRenewable.first(where: { $0.isOffer }), let offer = product.subscription?.introductoryOffer {
+ trialDaysPeriodText = storeKitService.daysLabel(offer.period.value, unit: offer.period.unit)
+ salePercent = storeKitService.salePercent(product: product, products: products)
+ }
+ case .failure:
+ break
+ }
+ }
+
+ public 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 {
+ withAnimation {
+ state = .result(
+ offer: offer,
+ isShowRate: isShowReviewBanner
+ )
+ }
+ } else if isShowReviewBanner {
+ withAnimation {
+ state = .result(
+ offer: nil,
+ isShowRate: isShowReviewBanner
+ )
+ }
+ } else {
+ state = .empty
+ }
+ }
+ case .failure:
+ break
+ }
+ }
+
+ private func checkDateInSelectedPeriod(startDate: Date, endDate: Date) -> Bool {
+ if startDate < endDate {
+ return (startDate ... endDate).contains(Date())
+ } else {
+ return false
+ }
+ }
+
+ func textPrepere(_ text: String) -> String {
+ text
+ .replacingOccurrences(of: "", with: salePercent.toString)
+ .replacingOccurrences(of: "", with: trialDaysPeriodText)
+ .replacingOccurrences(of: "", with: Info.store.subscriptionsName)
+ }
+}