Skip to content

Commit f1c3628

Browse files
authored
Merge pull request #10 from banghuazhao/feat/add-finish-in-app-purchase
feat: implementation of finish in-app purchase
2 parents 0d9313a + 8764266 commit f1c3628

File tree

4 files changed

+231
-52
lines changed

4 files changed

+231
-52
lines changed

LongevityMaster/App/LongevityMasterApp.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,19 @@ import GoogleMobileAds
1111
struct LongevityMasterApp: App {
1212
@AppStorage("darkModeEnabled") private var darkModeEnabled: Bool = false
1313
@Dependency(\.achievementService) private var achievementService
14+
@Dependency(\.purchaseManager) private var purchaseManager
1415
@StateObject private var openAd = OpenAd()
1516
@Environment(\.scenePhase) private var scenePhase
1617
@State private var didShowOpenAd = false
1718

1819
init() {
20+
// let tabBarAppearance = UITabBarAppearance()
21+
// tabBarAppearance.configureWithOpaqueBackground()
22+
// tabBarAppearance.backgroundColor = UIColor.systemBackground
23+
// UITabBar.appearance().standardAppearance = tabBarAppearance
24+
// if #available(iOS 15.0, *) {
25+
// UITabBar.appearance().scrollEdgeAppearance = tabBarAppearance
26+
// }
1927
MobileAds.shared.start(completionHandler: nil)
2028
prepareDependencies {
2129
$0.defaultDatabase = try! appDatabase()
@@ -43,12 +51,17 @@ struct LongevityMasterApp: App {
4351
.onChange(of: scenePhase) { _, newPhase in
4452
print("scenePhase: \(newPhase)")
4553
if newPhase == .active {
46-
openAd.tryToPresentAd()
54+
if !purchaseManager.isPremiumUserPurchased {
55+
openAd.tryToPresentAd()
56+
}
4757
openAd.appHasEnterBackgroundBefore = false
4858
} else if newPhase == .background {
4959
openAd.appHasEnterBackgroundBefore = true
5060
}
5161
}
62+
.task {
63+
await purchaseManager.checkPurchased()
64+
}
5265
}
5366
}
5467

LongevityMaster/Components/Me/MeFeature.swift

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,31 @@ struct MeView: View {
6464
statView(title: "Achievements", value: "\(allAchievements.filter { $0.isUnlocked }.count)/\(allAchievements.count)")
6565
}
6666
.padding(.top, AppSpacing.small)
67-
if !purchaseManager.isRemoveAdsPurchased {
67+
if !purchaseManager.isPremiumUserPurchased {
6868
Button(action: {
6969
showPurchaseSheet = true
7070
}) {
7171
Text("Upgrade to Premium")
7272
.appButtonStyle(theme: themeManager.current)
7373
}
74-
.sheet(isPresented: $showPurchaseSheet) {
75-
PurchaseSheet()
74+
} else {
75+
HStack(spacing: 8) {
76+
Image(systemName: "crown.fill")
77+
.foregroundColor(.yellow)
78+
.font(.title3)
79+
Text("Welcome, Premium user!")
80+
.font(.headline)
81+
.foregroundColor(themeManager.current.primaryColor)
7682
}
83+
.padding(.vertical, 8)
84+
.padding(.horizontal, 16)
85+
.background(themeManager.current.card)
86+
.overlay(
87+
RoundedRectangle(cornerRadius: AppCornerRadius.button)
88+
.stroke(themeManager.current.primaryColor, lineWidth: 1.5)
89+
)
90+
.cornerRadius(AppCornerRadius.button)
91+
.shadow(color: AppShadow.card.color, radius: 4, x: 0, y: 2)
7792
}
7893
}
7994
.padding()
@@ -106,8 +121,13 @@ struct MeView: View {
106121

107122
}
108123
}
109-
BannerView()
110-
.frame(height: 60)
124+
if !purchaseManager.isPremiumUserPurchased {
125+
BannerView()
126+
.frame(height: 60)
127+
}
128+
}
129+
.sheet(isPresented: $showPurchaseSheet) {
130+
PurchaseSheet()
111131
}
112132
.background(themeManager.current.background)
113133
.scrollDismissesKeyboard(.immediately)

LongevityMaster/Components/Me/PurchaseSheet.swift

Lines changed: 142 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import Dependencies
44
struct PurchaseSheet: View {
55
@Dependency(\.purchaseManager) var purchaseManager
66
@Environment(\.dismiss) private var dismiss
7+
@State private var isPurchasing = false
8+
@State private var showSuccess = false
9+
@State private var showSuccessModal = false
710

811
var body: some View {
912
ZStack(alignment: .topLeading) {
@@ -49,7 +52,7 @@ struct PurchaseSheet: View {
4952
.multilineTextAlignment(.center)
5053
.padding(.horizontal)
5154
.padding(.bottom, 12)
52-
Text("Unlock the full power of Longevity Master with these exclusive benefits:")
55+
Text("Unlock the full power of Longevity Master with exclusive benefit:")
5356
.font(.body)
5457
.multilineTextAlignment(.center)
5558
.foregroundColor(.secondary)
@@ -61,36 +64,54 @@ struct PurchaseSheet: View {
6164
Text("No Ads: ")
6265
.fontWeight(.semibold) + Text("Enjoy a clean, ad-free habit tracking experience.")
6366
}
64-
HStack(alignment: .top) {
65-
Text("").font(.title3).fontWeight(.bold)
66-
Text("Unlimited Habits: ")
67-
.fontWeight(.semibold) + Text("Create and track as many healthy habits as you want—no limits.")
68-
}
67+
//HStack(alignment: .top) {
68+
// Text("• ").font(.title3).fontWeight(.bold)
69+
// Text("Unlimited Habits: ")
70+
// .fontWeight(.semibold) + Text("Create and track as many healthy habits as you want—no limits.")
71+
//}
6972
}
7073
.font(.body)
7174
.padding(.horizontal)
7275
.padding(.bottom, 24)
7376
// Purchase button
74-
if let product = purchaseManager.removeAdsProduct {
75-
Button(action: {
76-
Task {
77-
await purchaseManager.purchaseRemoveAds()
78-
if purchaseManager.isRemoveAdsPurchased {
79-
dismiss()
77+
if let product = purchaseManager.premiumUserProduct {
78+
if purchaseManager.isPremiumUserPurchased {
79+
Text("You are now Premium user!")
80+
.font(.title2)
81+
.fontWeight(.bold)
82+
.multilineTextAlignment(.center)
83+
.padding(.horizontal)
84+
.padding(.bottom, 12)
85+
} else {
86+
Button(action: {
87+
Task {
88+
isPurchasing = true
89+
if await purchaseManager.purchasePremiumUser() {
90+
showSuccess = true
91+
showSuccessModal = true
92+
}
93+
isPurchasing = false
8094
}
81-
}
82-
}) {
83-
Text("\(product.displayPrice) - Upgrade to Premium")
84-
.font(.headline)
85-
.frame(maxWidth: .infinity)
86-
.padding()
87-
.background(Color.blue)
95+
}) {
96+
HStack {
97+
if isPurchasing {
98+
ProgressView()
99+
.progressViewStyle(CircularProgressViewStyle(tint: .white))
100+
.scaleEffect(0.8)
101+
}
102+
Text(showSuccess ? "Purchase Successful!" : "\(product.displayPrice) - Upgrade to Premium")
103+
.font(.subheadline)
104+
}
105+
.padding(.vertical, 12)
106+
.padding(.horizontal, 20)
107+
.background(showSuccess ? Color.green : Color.blue)
88108
.foregroundColor(.white)
89-
.cornerRadius(22)
109+
.cornerRadius(16)
110+
}
111+
.padding(.horizontal)
112+
.padding(.bottom, 18)
113+
.disabled(isPurchasing)
90114
}
91-
.padding(.horizontal)
92-
.padding(.bottom, 18)
93-
.disabled(purchaseManager.isRemoveAdsPurchased)
94115
} else {
95116
ProgressView()
96117
}
@@ -118,12 +139,110 @@ struct PurchaseSheet: View {
118139
Spacer()
119140
}
120141
}
142+
.sheet(isPresented: $showSuccessModal, onDismiss: { showSuccess = false }) {
143+
PremiumSuccessView(onContinue: {
144+
showSuccessModal = false
145+
showSuccess = false
146+
dismiss()
147+
})
148+
}
121149
.task {
122150
await purchaseManager.loadProducts()
123151
}
124152
}
125153
}
126154

155+
struct ConfettiDot: Identifiable {
156+
let id = UUID()
157+
let x: CGFloat
158+
let y: CGFloat
159+
let color: Color
160+
let size: CGFloat
161+
}
162+
163+
struct PremiumSuccessView: View {
164+
var onContinue: () -> Void
165+
@State private var animate = false
166+
private let confetti: [ConfettiDot] = (0..<20).map { _ in
167+
ConfettiDot(
168+
x: CGFloat.random(in: 40...340),
169+
y: CGFloat.random(in: 40...600),
170+
color: [Color.yellow, Color.orange, Color.green, Color.blue, Color.pink, Color.purple].randomElement()!,
171+
size: CGFloat.random(in: 8...16)
172+
)
173+
}
174+
175+
var body: some View {
176+
ZStack {
177+
Color.white.opacity(0.4).ignoresSafeArea()
178+
179+
// Confetti
180+
ForEach(confetti) { dot in
181+
Circle()
182+
.fill(dot.color)
183+
.frame(width: dot.size, height: dot.size)
184+
.position(x: dot.x, y: animate ? dot.y : dot.y - 80)
185+
.opacity(0.7)
186+
.animation(.easeOut(duration: 1.2), value: animate)
187+
}
188+
189+
VStack(spacing: 28) {
190+
ZStack {
191+
Circle()
192+
.fill(LinearGradient(gradient: Gradient(colors: [Color.yellow.opacity(0.5), Color.orange.opacity(0.2)]), startPoint: .top, endPoint: .bottom))
193+
.frame(width: 120, height: 120)
194+
.blur(radius: 8)
195+
Image(systemName: "crown.fill")
196+
.resizable()
197+
.scaledToFit()
198+
.frame(width: 80, height: 80)
199+
.foregroundColor(.yellow)
200+
.shadow(color: .yellow, radius: 12)
201+
}
202+
Text("Congratulations!")
203+
.font(.system(size: 28, weight: .bold))
204+
.foregroundColor(.primary)
205+
Text("💎 Thanks for being a Premium member!")
206+
.font(.title3)
207+
.foregroundColor(.secondary)
208+
Divider()
209+
VStack(alignment: .leading, spacing: 10) {
210+
HStack {
211+
Image(systemName: "checkmark.seal.fill").foregroundColor(.green)
212+
Text("Ad-free experience")
213+
}
214+
HStack {
215+
Image(systemName: "star.fill").foregroundColor(.yellow)
216+
Text("Thank you for supporting Longevity Master!")
217+
}
218+
}
219+
.font(.body)
220+
.frame(maxWidth: .infinity, alignment: .leading)
221+
Button(action: onContinue) {
222+
Text("Continue")
223+
.font(.headline)
224+
.padding(.vertical, 12)
225+
.padding(.horizontal, 40)
226+
.background(
227+
LinearGradient(gradient: Gradient(colors: [Color.blue, Color.purple]), startPoint: .leading, endPoint: .trailing)
228+
)
229+
.foregroundColor(.white)
230+
.cornerRadius(16)
231+
.shadow(radius: 4)
232+
}
233+
}
234+
.padding(36)
235+
.background(
236+
RoundedRectangle(cornerRadius: 28)
237+
.fill(LinearGradient(gradient: Gradient(colors: [Color.white, Color.yellow.opacity(0.1)]), startPoint: .top, endPoint: .bottom))
238+
)
239+
.shadow(radius: 24)
240+
.padding(.horizontal, 24)
241+
.onAppear { animate = true }
242+
}
243+
}
244+
}
245+
127246
#Preview {
128247
PurchaseSheet()
129248
}

0 commit comments

Comments
 (0)