From a94404d93d8122bdc770c64d9026e31851815d55 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 3 Feb 2026 14:51:54 -0600 Subject: [PATCH] Signed-off-by: Matt Bruce --- .gitignore | 1 + .../UserInterfaceState.xcuserstate | Bin 21690 -> 38146 bytes .../xcshareddata/xcschemes/SelfieCam.xcscheme | 2 +- SelfieCam/App/RootView.swift | 10 +- SelfieCam/Configuration/Debug.xcconfig | 8 -- SelfieCam/Configuration/Release.xcconfig | 8 -- .../Configuration/Secrets.swift.template | 14 +++ .../Features/Camera/Views/ContentView.swift | 2 + .../Paywall/Views/ProPaywallView.swift | 100 ++++++++++++++++-- SelfieCam/Resources/Localizable.xcstrings | 44 ++++++++ .../Shared/Premium/PaywallPresenter.swift | 60 ++++++++++- SelfieCam/Shared/Premium/PremiumManager.swift | 9 +- 12 files changed, 224 insertions(+), 34 deletions(-) create mode 100644 SelfieCam/Configuration/Secrets.swift.template diff --git a/.gitignore b/.gitignore index 79e0e61..8234e3f 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ Pods/ Secrets.xcconfig Secrets.debug.xcconfig Secrets.release.xcconfig +**/Secrets.swift # OS generated files .DS_Store diff --git a/SelfieCam.xcodeproj/project.xcworkspace/xcuserdata/mattbruce.xcuserdatad/UserInterfaceState.xcuserstate b/SelfieCam.xcodeproj/project.xcworkspace/xcuserdata/mattbruce.xcuserdatad/UserInterfaceState.xcuserstate index 65cd1d7cec4041f9436ec1b9a7e8b07e7f82ab66..95d34f09bbfbe185a5b1b3165d8e4c2d2c790a9e 100644 GIT binary patch literal 38146 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 delta 11706 zcma)B2V9fK`@ehd?gfNE_9BEZ90Y<8Mpyv?B)A7u+#*Xwaf4fPudA(f$KBcrxLfzE zwXLnS%i3DhT5GMfZmp}`SbE7@4b2M?z!*xx#xNAZt{9=&pJ4_F9#3K zD{k)W1PVY`&>IwiV$cut2ZKNb7zT!eDliI+1LJ`cP%srt1FwMvU?Erp7K0^VJ@^#t z2cLlh;B)W=_!4{tz6J-uH{dWh4t@kDz!~rxI18?U>)-~s3H}84z}24_MSd=tI}m%}yi zUAPJEg1h1S@Dunc{0aU7&%aFE{cocEL;+o%-OgsE}Lu1 zwd2}zUAdlIA=j5H;>x*!oO3caotwoioQquh_&Y3?F- ziM!0*=I(Ki5JDIcBtrg3j`S!P8BhocM@D2qF(@7-pk$PSQjra{Mr}}A)Dd+--BAzJ z2bH2S)DI0r!%!ufgq(;X0lki9pf{Xo4w{b^qUGpq^bV>;YtVYM6>US?(Jr(H9Y)`y zI&=gbMaR$&=s5Zjoj^aKljvu33jKnvpsVP2bPZicH_%OV3*EyIa~NTa3FfgFORxfK za0E8uNF0StI2yOaW*mnTaU0wgx5Mpm2kh*KJK@ea59i}@JP;4U6?iZnf`{T^csL$` zCt)Y1SiqC;Pd;wp?H}Fk-3;&7l5E1bw0YpqBP9h~TA}0!>Bq|a}f{B5I5EF?e z2_%)IkxY_9?4&hmLpqRGNEgzb^d@~s3F%J;k?~{#nMfuPC!s_jlgUgni_9i-h>NTu z?~!d}7x{pEO!kv6$d}|-a)$gy&XRNFJh?zFl1t<=xk9cJ=NznFiEU(K)K*YaEWZTxnA5B~wbpZ}~&_>iiKiX%V-T)RmE z@};@1o8)b6bNU%gp{eu>c7I8~rl;w30XCtXY2LPt`~aBHaUF1gT+j-%25mrF&UkUF%4|~Xit-DP zzmTu={1RL!9_gxX(b@H@$ls+FEB{h;tVbZkl_CE8FK;J#-iEm*Nf!U5u_>OnO$)_& ztTC&rK%&U41MX2x2d{SRkZ&6?qG)UlEA(;$>(3B)9n1!aE5Qu#2ABzE(H1m<8foN8 zFbB*9^T2!>MK{n7=sud~D)8^+`rW_4xfE1`#5D}%whZMq&k@!$gmv}kt^qZm7OVuT zz-q7ttfeNJPfO@<>ZJ4NJ2bDc>vzFUkhmVa2R49>U=!F3wt%f*8`utZ&}iC{#?V-5 zrg1c$CeTD`Sr2vr6T8Ap-~+H1M6t(@K>|%;cHq9!XnT)cbnuu$rL;3_a|m>-2H(w#JrSSXft$Zua&SyWt6 z_ClXpsQs%(L9j(5rOy8kr!d09sR^3uDTT3&(pMTN?bbkPk(*Kvg)o6p3KMCU21;Qv zqZFpVRQF8_XjjkOOzbOedBjj};4ll?-Q&%MIkY?NQ3D+?m-eK6JfyZVwu7DAoL*#{ zo~K!i)681f8C-$+v=`&FH_fes1t0)+Wvp~lg*3^jgFRTWCo4`-g*bB>LG1&J+?{Z+ zFKt5$-Jq5*Q2Wx{f50gr;~T3CfP?CJ7|3`i{_iS{^26bTMra$6b^fpE0w*>=D>Ohm zjX_)50ByepXotRl_H{S3GiX^Ow6p$owlr$904@efc@10$7t#K7Kn+|1m(p@tQ4g#0 zZCLXXthEf*RdgVOb&wm@wG78~;5M`F=&Gvn(&Dj|Wg|O^#dq)gj^{pZb82VRP$4pC z#dnlJ{vH$E@UrM9&E{QRaWpe|{q~AZtsWh3L7&s%+zhwX^RbojG1v|64#vljm-&F_ z8%MMUept`PUdG3;|E|&~zaM`4Pd=X0;rti(1;18Z;#j1wQo7^)S)G=x`cUSlKVS~F5!XFq?S(;7Q) zdG+|SfWur93pll0KKBZ^Lf@b>83PReZVc1z%ojb=xm^?2ixqpbqA`fMrm&2g^^RY> zrBR<^uB;x>QikZ=F)|9Q4KerbJ4|gnaAVgMsiEIx7=`@cl&Y%7W3t` z+;Z-12GvV+hEFZS#}Vp*Yc(sbVa4I0FFZN7p4;H=Lp?c53ea~QB(Ew{r z1FY}9fc2Og)*onfBdjO>C#=76XWgEhJH!1(Yw5}w?i_cXuA*xjalOJ_e+kzc+)ak- zYPzN#*FPDMcNmbP3?5wXv*H6*tTy}w*T>v5H%J`!l-c?^1`>h{r1dX>B;j^9;DmVO z(M zbSvFP_ps#F95Jv53iV`6o=gKZMG>{A8ETGN(Cu_5-A#AYzmG%_E8Nd4D?-tzWuNCc zktemTMbYl$x?VIEMN}g*-9>k}?nWf}qC|JuLf>cQ6l0rMl!nsX`sPw)HOioSpC`sB z3)w+p4a!D2^h5el4O2CjDf+Q1c3i9lwPWmpD3-mlB~1P3I-xw)4(d!lsYdy9zq_61 zs|(c4vmp4?<@HK;C+dlMfy5OnUi4ImSl&{N`qBf{sF;3Eb9=YWv3DpMT$W!sqI^jI z?)haE{mRR-iw4(^sK2{!DMc^(9)t$F$H3lIqapOGhCB@oXG8S}?r2o93XRn>J3^z- zXf%c%q~Fj(wP+j~k0#J>=~4OsmA!O>>WZ3Ytp4qlatIbo46yo*rQo#T2N^ z&`c13W}(?^DHB~;KDaE*_Qg`e)r``Gw630#xo93su5*ei%WBYEMuew=3(c+<%=Rj! zb&VP=LQCB}Ek;Y|G5SLdT87@F$LR@APg*Hj!A4Zmo%J))0|oujb+RBTvA&m;XjP#& z;5qg6VAZ0PXtig}SYNJVs=6Cl$EvlMtZ9<#rDf$cR@MJ+Z zr$^HrjhbfkQ?Vg=-tBIDm+d?2h4vqy1340E@A)>RFS=rlUV7KG?m zbO!x~&e9w7CcQ=fSc%S~3+N)cL~qkS=^c8PY3<*AVJ7;+Jx^|=dHRSxX6@t^ikrD+73y6+Ju1!E3;X_4nrntd*q=q> zr}P=kD*=Y495B>jDG0zaEZ?@hhXy;@vWZPati-C`&-q~V7uB*?YS63R7>XJ=$66c$ z5?7-KScikqBOHtk^u7Q&0V4WPfLMTJH4epLI2<=+pJ4*z1?VL}Zvpyv9=zO2hq5_h zV+*oT<&|Sy9SWB^#n8hPj;UwL(k!5KCaoJPmax5rp`a8Lfw92AJ}cR0EW1)b0!TEp z1UAs#Gp}&G$Eh?bX~CHwaV<{5$v6e4;xuf<={Q4xegYH;&|iQ70u&2SB0#ACWotpW z31_huP1ugp+}|`+fO1AD`%*AT_Y>P4d9|+%7UNfNACR~Xcfkd?EAEE7;~uyt?uB~` zP$@u_0M!E22v94)KmqCm7$iVFvme|S7vW-Df=h83>!&~MEWls^+6CwkU>gCp72xOY z?!IJJVJzN~fJfpn%!qI$uEL}6XaO1o7$U$>0fw!_WAQll!vq0_3$Uf*}7FJmqa&tP)Z_)P&uvBZfP(A)SOyuvlBR8L^E08OsVrTHY& z;eE&M@-E8_Or3bOS(`dmns5#;)XkLEMOaE@60(tE%4= z_l6U$#VcLU`Wbn=3N!!VYT18bHeQQ2F+;`c@Ou0%eh+WJ8wD66z*qsA1sEs5cmXD? z#+&gLmS-j4?Z70!L;+e@SYXuH+=RQl2Gr!>kMU=24ENzr@TYjc0FwlmEWi{2rmn;X z@aL@VmjX-^V1~P1rke)^v+kU-;;R1kF~f?6l$MpcMwUmq!pgfi>uvQr{Czb(EI_LO z(|gp%2ZJX-xQB8SA7cx;hFyaEvdUWa<_FNLONV?`=x#BaODx4PF){Tmo$y#a!)E;W zm?(oOX=0RNd|V8Bj5S-5CQfWLgH!ld<~H#!3Vd3C*#gX|aobhBO|fCFuV`lq%N&;J zNp>6ST*6n~buQy80?ZX)D;7iXHM&)RtyvA}hKVg z3h+Y#e#BhWAeM}m5G$ZyS%9@D9)azG8yGaJaO z3|Es-5=O#70BK5^k>;cYi6BN238Gwvf#nhbmI<(z00#(gkN|tq6xTbI7CULlQautw zVu_hBBVn_;uK}+hqhQkOOaFNF)7WU2_I;3BD|Edv1?menT83)6O$&|Q}DSVMf z{Oiw0NJpkwC(@a){n(z38&F;H+I4rw_>q-GgNF&QpWp@iQ&1?bYS<=i=25bMbaidX zQ<56ejcFM&N~0n@NKewM$1nzFeIOR#KmnG!I*;mRa}TSK^zE5b@QS0nqRb6=Eh!?! zpx0ktvDn_JsAN!Csrz^PkjfEb>!~UwWv(ZKqA=;lx_UAwvOdov16Z;{$^|(1dFUe* zWF(uuWH1?mACh5YI2j?pAp#sKz>WfBgBdQs5vzfTRFP5a8dLAHM+&gg^Izqezt+(s zTcQSoCR50(EIN{@WEz<+z)=EZPIXKzd5yddi2%n6a5>AS^IWG#w~i%qS(ZlT32+>3 zc7iM*3&|p~m@HukFD1*!nEy0N-TsH`ldU+RWvW1_x^MhgOX!Da&RW*-OhS zhmMFISysUg7}(5l9hKf}Pp-Pj`(1!@X#K7=x$(bNZ{#j{z}86Q9${$=qkMi1c}N}! z&?T_#IM23qa~|-RCEh&bIUWgcfdCf@a8WH!cpfnCxLANo+^aaYcO3~EcdnJLV{(Hx zF9HF)KU>JL&1<&3z)M(B%8G1zx}d)hplmL@#XK{_Oq^{%|6>=zHF#Wvdy4Zd_z2hXacayP*|b|ePUSj0&dbb4@v$tj z@g_c+Z^^UJxI%!{0;~~W?MmLv$KfzOL4Yd-xX$CHwzA3LGu|4_r|~v-H7lRaXYiQ< zTqVHO0$d}&wJRB;QAq1yF@&mjhBR$iLABrMs_+dQrA3FrN=efh-EBP^Q zlB@VpJo5}Y1-MIqyK8w9NZ{FgUg3`J@3Yk%YxRZe@8vGJGA9K&r~NGm@{{@L|JGPE zYmCi-YIkEF(A==hX-obZJNa2WJ4$k|F2$gc?D_nH7h_{~_i@7lkze8-$5MWo0Qa%1 zX8((UEa$8Lt?wGvHzV^Cci*2fYZW=Q&G~ga+pBqb&>NX7W8yQn>;Wd*!5J#!cksL1 znlNO(sAd$q$(`bCs^j|B(MkfL{sl>l!y{9-{JGpE)}@(S7t3`?B?ho-GNC z?2I^?oen3k9SBRIE61y~SDsg)SCLnVSDDwZUe~;Cc-`{4 z?LF9goc9FpN#4}^iuXP52i}jopZHYxjP)7sGttNCbEgT{1UKQEcsKc`$%!T>o1AKL z+PBhI@SWm2&G%K`pMB5!Ui1CK_nFhr+pmeApP#><*iYjZ=ojP{>=)t}=9lj`(Qm2W zA-|veF8f{eyXJSp@0QnW(?0Tr@~DSTs~LTr^TtB^oVSAlfE6E_&b}H+N-j(Nq!H3Ysa2XG zwMnz3ZKUm_9i*M4dD5QJ0n+i(iPGs(mvpsst#pTUk94o}Bk6wW0qGagucSXpPf9OI zuSsu6Z%OY;?@J#_AIp4YYMDvaQWh(VlO@P3vSeAR%qq)}*<=N>Zn7S-Ua~&2zOrIj zsjQ!DfNY>sRw1jBjggI$O^{8K&6drT&6h2ZEs`yft&**my(il!+a}u~+a-Hn_OtAP zoR^E_0dk34CfCY!a=qLj50yvCljLpX?d1jX68R|k82L2$4Eap?Y`IImP`+5cRK8aJ zu6(!rL;1(@PvoD=zm$J1|3-dNeo6jB{!9TCPNX0TFNKf7S0PdaC?twziWUl^B1#dh zh*6jo@rp!6k|ITsrm!noDcUI7DY_{7DEcaj6{U)PiUEo$#W=+T#U#ZP#WcmMiq{pZ z6<;ZSP@GhpQk+(tQ(RD7Qe06yP&`)pD8)*tQm#}ggOtI_5M`LMsWMubs_fuYc2ag% z_EU~kj#s{>oTZ$joTpr*T%ug2d`tPRa<}r3@;l}C$|K5S%Hzrt%9F}d%G1g-%Dc+@ z%7@Cw%BL!z;#640tGrcBRDLRjO0CkWbgHJRXjP2Ltcq79s*+SGsx(z=RR>ikRi3J= zs=KPEs<&#KYPo8?YLm*jMYT<}TeU~ESM`zVkm|7Nl` zQwOL+)!}NBIz`=H-BI05-9z19U7;SL9;U8Rk5-RWk5|u7FIK;&-l*QJ-m2cN-l^WL z-lN{D{z$z~eN25^eL{UweM)^=eMWs&eO`T0eOY}~{iph#`hipZNP{&ZO@KzCk!chf zl_p#hp^4O(G-ge_CQ*~5>8Tl`nWmYknXQ?tnXg%@c~i4o^NyxkvtF}H^R?z1%`wey znmd|%TCDZa`f5d5saCF4YSr52TBFvYP1B}pGqrYYuC}$dt+t1@zjlIll9p;GYo}_b zYhTkkXJ}_?XKUwbS83O1*J?dg^-XN_G8o19Ss*6}nlvjk@o3Kj}{DPU$Y_uIX;*Zs~68 z?&zNCo&~|6CPA7YLr_FeR8VwKOi)QsMbOZo;XxyV-VE9qv_I(cpf7_C1|16eF6if= zGeOR?LFa?61pOX#J?N(1Pp{R7>6__W=#6@dK1H9V&(PcS+4@fUJpC*B0{vS34*gF3 zF8%xZ&-GvF59+_wAJ*6Df7M^n-`3yN-`773_6lwi>=zsmED4qc>w<%WLxRJCV}dQg z$-$|?*5I6AM{uj)Ho@(J2M4bT{x=t5~d0Z z4{I5g7G@924Qn0NHmrSEL0Gr29$~%0`h@ij8ymJDY*W~8VVAqAICm3``)Kq}P%bCM`)?mb5%+P15$HT}kgJ?M?bB=~&X~q~DUxCtXY~OdgOtDS1lr z(&U=t4au96x25=`NK^DFO;ci1vQyfnj82)5vLYmiP)XS-Vq~1xrpZYK@I4w1;Fl~6+oU~PG@24F|JDYYP?Q+`hX*bgTNV}7E zKkZ@KW2?fdwgy`D)(~sBwVAc0HOZQ4O}E;tIaY@?-&$bpZtZ0)w^mq(T1Qx&*2&hX zR_CkM1=hvZrPjBs>#XltH(9q>>#V1(e_HQZA6lQJgLIV6r~9P)r3a)-)0?M9q(`Pl zr^lqnr6;6Y(v#EE($mv3(@WE*q_0T-H2p$`Uq(zu$BZEvZ)7aUSe&seW2{*is3{gC~z{fPYs`w9C=`#Jk%`|tJ}_FML+4&v~3_&WR@3WwUEbp$zr z9T5(*Bg5fvw05*}baWIrx;uJ0`Z)SJMmWYhUUR(RnC+P7a5)w_wmJ4WS{`tG={V>( c?5J}bb^PGC?pbgGj;sIX57d7*e&&k*4@~ diff --git a/SelfieCam.xcodeproj/xcshareddata/xcschemes/SelfieCam.xcscheme b/SelfieCam.xcodeproj/xcshareddata/xcschemes/SelfieCam.xcscheme index 971051f..cc5f39d 100644 --- a/SelfieCam.xcodeproj/xcshareddata/xcschemes/SelfieCam.xcscheme +++ b/SelfieCam.xcodeproj/xcshareddata/xcschemes/SelfieCam.xcscheme @@ -78,7 +78,7 @@ + isEnabled = "NO"> diff --git a/SelfieCam/App/RootView.swift b/SelfieCam/App/RootView.swift index 3d9af52..5739ae8 100644 --- a/SelfieCam/App/RootView.swift +++ b/SelfieCam/App/RootView.swift @@ -44,7 +44,15 @@ struct RootView: View { } } .sheet(isPresented: $showPaywall) { - PaywallPresenter() + PaywallPresenter { + // Purchase successful during onboarding - complete it + if !hasCompletedOnboarding { + onboardingViewModel.completeOnboarding(settingsViewModel: settingsViewModel) + withAnimation(.easeInOut(duration: Design.Animation.standard)) { + hasCompletedOnboarding = true + } + } + } } } } diff --git a/SelfieCam/Configuration/Debug.xcconfig b/SelfieCam/Configuration/Debug.xcconfig index 3ad9177..8464628 100644 --- a/SelfieCam/Configuration/Debug.xcconfig +++ b/SelfieCam/Configuration/Debug.xcconfig @@ -2,11 +2,3 @@ // Configuration for Debug builds #include "Base.xcconfig" -#include? "Secrets.debug.xcconfig" - -// If Secrets.xcconfig doesn't exist (CI/CD), fall back to empty values -// CI/CD should set these via environment variables -REVENUECAT_API_KEY = $(REVENUECAT_API_KEY) - -// Expose the API key to Info.plist -REVENUECAT_API_KEY_PLIST = $(REVENUECAT_API_KEY) diff --git a/SelfieCam/Configuration/Release.xcconfig b/SelfieCam/Configuration/Release.xcconfig index be2a1ef..28a3a5a 100644 --- a/SelfieCam/Configuration/Release.xcconfig +++ b/SelfieCam/Configuration/Release.xcconfig @@ -2,11 +2,3 @@ // Configuration for Release builds #include "Base.xcconfig" -#include? "Secrets.release.xcconfig" - -// If Secrets.xcconfig doesn't exist (CI/CD), fall back to empty values -// CI/CD should set these via environment variables -REVENUECAT_API_KEY = $(REVENUECAT_API_KEY) - -// Expose the API key to Info.plist -REVENUECAT_API_KEY_PLIST = $(REVENUECAT_API_KEY) diff --git a/SelfieCam/Configuration/Secrets.swift.template b/SelfieCam/Configuration/Secrets.swift.template new file mode 100644 index 0000000..0bd741e --- /dev/null +++ b/SelfieCam/Configuration/Secrets.swift.template @@ -0,0 +1,14 @@ +// Secrets.swift +// ⚠️ DO NOT COMMIT THE ACTUAL Secrets.swift FILE TO VERSION CONTROL +// +// Copy this file to Secrets.swift and fill in your actual values. +// Secrets.swift is gitignored. + +import Foundation + +enum Secrets { + /// RevenueCat API Key + /// - Debug: Use your test/sandbox API key (starts with "test_") + /// - Release: Use your production API key (starts with "appl_") + static let revenueCatAPIKey = "YOUR_REVENUECAT_API_KEY_HERE" +} diff --git a/SelfieCam/Features/Camera/Views/ContentView.swift b/SelfieCam/Features/Camera/Views/ContentView.swift index 7e753ed..a3c7c8d 100644 --- a/SelfieCam/Features/Camera/Views/ContentView.swift +++ b/SelfieCam/Features/Camera/Views/ContentView.swift @@ -114,6 +114,8 @@ struct ContentView: View { } .sheet(isPresented: $showPaywall) { PaywallPresenter() + // No callback needed - paywall auto-dismisses on success + // and premium status updates automatically via PremiumManager } } diff --git a/SelfieCam/Features/Paywall/Views/ProPaywallView.swift b/SelfieCam/Features/Paywall/Views/ProPaywallView.swift index 75d7a60..55e9e5a 100644 --- a/SelfieCam/Features/Paywall/Views/ProPaywallView.swift +++ b/SelfieCam/Features/Paywall/Views/ProPaywallView.swift @@ -3,9 +3,24 @@ import RevenueCat import Bedrock struct ProPaywallView: View { + /// Callback triggered when a purchase or restore is successful + var onPurchaseSuccess: (() -> Void)? + @State private var manager = PremiumManager() @Environment(\.dismiss) private var dismiss @ScaledMetric(relativeTo: .body) private var bodyFontSize: CGFloat = Design.FontSize.body + + /// Error message to display + @State private var errorMessage: String? + + /// Whether to show the error alert + @State private var showError = false + + /// Whether a purchase is in progress + @State private var isPurchasing = false + + /// Whether a restore is in progress + @State private var isRestoring = false var body: some View { NavigationStack { @@ -41,12 +56,10 @@ struct ProPaywallView: View { ProductPackageButton( package: package, isPremiumUnlocked: manager.isPremiumUnlocked, + isLoading: isPurchasing, onPurchase: { Task { - _ = try? await manager.purchase(package) - if manager.isPremiumUnlocked { - dismiss() - } + await purchasePackage(package) } } ) @@ -54,11 +67,21 @@ struct ProPaywallView: View { } // Restore purchases - Button(String(localized: "Restore Purchases")) { - Task { try? await manager.restorePurchases() } + Button { + Task { + await restorePurchases() + } + } label: { + if isRestoring { + ProgressView() + .tint(.secondary) + } else { + Text(String(localized: "Restore Purchases")) + } } .font(.footnote) .foregroundStyle(.secondary) + .disabled(isRestoring || isPurchasing) } .padding(Design.Spacing.large) } @@ -68,11 +91,73 @@ struct ProPaywallView: View { ToolbarItem(placement: .cancellationAction) { Button(String(localized: "Cancel")) { dismiss() } .foregroundStyle(.white) + .disabled(isPurchasing || isRestoring) } } } .font(.system(size: bodyFontSize)) .task { try? await manager.loadProducts() } + .alert( + String(localized: "Purchase Error"), + isPresented: $showError, + presenting: errorMessage + ) { _ in + Button(String(localized: "OK"), role: .cancel) { + errorMessage = nil + } + } message: { message in + Text(message) + } + } + + // MARK: - Purchase Logic + + private func purchasePackage(_ package: Package) async { + isPurchasing = true + defer { isPurchasing = false } + + do { + let success = try await manager.purchase(package) + if success { + #if DEBUG + print("✅ [ProPaywallView] Purchase completed") + #endif + onPurchaseSuccess?() + dismiss() + } + } catch { + #if DEBUG + print("❌ [ProPaywallView] Purchase failed: \(error.localizedDescription)") + #endif + // Check if user cancelled (don't show error for cancellation) + let nsError = error as NSError + if nsError.code != 2 { // SKError.paymentCancelled = 2 + errorMessage = error.localizedDescription + showError = true + } + } + } + + private func restorePurchases() async { + isRestoring = true + defer { isRestoring = false } + + do { + try await manager.restorePurchases() + if manager.isPremiumUnlocked { + #if DEBUG + print("✅ [ProPaywallView] Restore completed - premium unlocked") + #endif + onPurchaseSuccess?() + dismiss() + } + } catch { + #if DEBUG + print("❌ [ProPaywallView] Restore failed: \(error.localizedDescription)") + #endif + errorMessage = error.localizedDescription + showError = true + } } } @@ -81,6 +166,7 @@ struct ProPaywallView: View { private struct ProductPackageButton: View { let package: Package let isPremiumUnlocked: Bool + let isLoading: Bool let onPurchase: () -> Void private var isLifetime: Bool { @@ -142,7 +228,9 @@ private struct ProductPackageButton: View { RoundedRectangle(cornerRadius: Design.CornerRadius.large) .strokeBorder(borderColor, lineWidth: Design.LineWidth.thin) ) + .opacity(isLoading ? 0.6 : 1.0) } + .disabled(isLoading) .accessibilityLabel(isLifetime ? String(localized: "Purchase \(package.storeProduct.localizedTitle) for \(package.localizedPriceString), one-time payment") : String(localized: "Subscribe to \(package.storeProduct.localizedTitle) for \(package.localizedPriceString)")) diff --git a/SelfieCam/Resources/Localizable.xcstrings b/SelfieCam/Resources/Localizable.xcstrings index 6067634..9d72fcb 100644 --- a/SelfieCam/Resources/Localizable.xcstrings +++ b/SelfieCam/Resources/Localizable.xcstrings @@ -1696,6 +1696,10 @@ } } }, + "Loading..." : { + "comment" : "A placeholder text displayed while waiting for content to load.", + "isCommentAutoGenerated" : true + }, "Locked. Tap to unlock with Pro." : { "comment" : "A hint that appears when a user taps on a color preset button.", "isCommentAutoGenerated" : true, @@ -1720,6 +1724,10 @@ } } }, + "Manage Subscription" : { + "comment" : "A button label that, when tapped, allows a user to manage their subscription.", + "isCommentAutoGenerated" : true + }, "Maybe Later" : { "comment" : "Text for a button that allows a user to dismiss a paywall without purchasing a premium subscription.", "isCommentAutoGenerated" : true @@ -1793,6 +1801,10 @@ "comment" : "A message displayed in an alert when the \"Reset Onboarding\" button is tapped.", "isCommentAutoGenerated" : true }, + "ONE TIME" : { + "comment" : "A label for a badge indicating a one-time purchase.", + "isCommentAutoGenerated" : true + }, "Open Settings" : { "comment" : "Text for an action button that opens the user's device settings to grant a denied permission.", "isCommentAutoGenerated" : true @@ -1845,6 +1857,10 @@ } } }, + "Pay once, own forever" : { + "comment" : "A subtitle displayed below a lifetime product package button, describing the permanence of the purchase.", + "isCommentAutoGenerated" : true + }, "Perfect lighting for every shot" : { "comment" : "Subtitle for a feature highlight that emphasizes the app's ability to provide perfect lighting for every selfie.", "isCommentAutoGenerated" : true @@ -2007,10 +2023,34 @@ } } }, + "Pro Active" : { + "comment" : "A label indicating that the user has an active Pro subscription.", + "isCommentAutoGenerated" : true + }, "Pro Features" : { "comment" : "Subtitle for a feature row in the \"Welcome\" view, highlighting the app's advanced features.", "isCommentAutoGenerated" : true }, + "Pro subscription active" : { + "comment" : "An accessibility label for the section of the settings view that displays that their Pro subscription is active.", + "isCommentAutoGenerated" : true + }, + "Purchase %@ for %@, one-time payment" : { + "comment" : "An accessibility label for a button that purchases a product package. The label includes the product name and price, with a note that it's a one-time payment if the package is a lifetime subscription.", + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Purchase %1$@ for %2$@, one-time payment" + } + } + } + }, + "Purchase Error" : { + "comment" : "The title of an alert that appears when there is an error during a purchase process.", + "isCommentAutoGenerated" : true + }, "Purchase successful! Pro features unlocked." : { "comment" : "Announcement read out to the user when a premium purchase is successful.", "isCommentAutoGenerated" : true, @@ -3122,6 +3162,10 @@ } } }, + "Tap Manage Subscription to view or cancel" : { + "comment" : "A hint that appears when the user interacts with the \"Pro Active\" view, instructing them to tap the \"Manage Subscription\" button to view or cancel their subscription.", + "isCommentAutoGenerated" : true + }, "Tap to collapse settings" : { "extractionState" : "stale", "localizations" : { diff --git a/SelfieCam/Shared/Premium/PaywallPresenter.swift b/SelfieCam/Shared/Premium/PaywallPresenter.swift index 3e7a554..7686be8 100644 --- a/SelfieCam/Shared/Premium/PaywallPresenter.swift +++ b/SelfieCam/Shared/Premium/PaywallPresenter.swift @@ -16,7 +16,10 @@ import Bedrock /// Usage: /// ```swift /// .sheet(isPresented: $showPaywall) { -/// PaywallPresenter() +/// PaywallPresenter { +/// // Called on successful purchase or restore +/// print("Purchase successful!") +/// } /// } /// ``` /// @@ -25,10 +28,21 @@ import Bedrock /// 2. If successful, display the native RevenueCat PaywallView /// 3. If it fails (network error, no paywall configured), fall back to ProPaywallView struct PaywallPresenter: View { + /// Callback triggered when a purchase or restore is successful + var onPurchaseSuccess: (() -> Void)? + @Environment(\.dismiss) private var dismiss @State private var offering: Offering? @State private var isLoading = true @State private var useFallback = false + @State private var errorMessage: String? + @State private var showError = false + + /// Creates a PaywallPresenter with an optional success callback + /// - Parameter onPurchaseSuccess: Called when purchase or restore completes successfully + init(onPurchaseSuccess: (() -> Void)? = nil) { + self.onPurchaseSuccess = onPurchaseSuccess + } var body: some View { Group { @@ -37,18 +51,29 @@ struct PaywallPresenter: View { loadingView } else if useFallback { // Fallback to custom paywall on error - ProPaywallView() + ProPaywallView(onPurchaseSuccess: onPurchaseSuccess) } else if let offering { // RevenueCat native paywall paywallView(for: offering) } else { // No offering available, use fallback - ProPaywallView() + ProPaywallView(onPurchaseSuccess: onPurchaseSuccess) } } .task { await loadOffering() } + .alert( + String(localized: "Purchase Error"), + isPresented: $showError, + presenting: errorMessage + ) { _ in + Button(String(localized: "OK"), role: .cancel) { + errorMessage = nil + } + } message: { message in + Text(message) + } } // MARK: - Loading View @@ -72,17 +97,42 @@ struct PaywallPresenter: View { private func paywallView(for offering: Offering) -> some View { PaywallView(offering: offering) - .onPurchaseCompleted { customerInfo in + .onPurchaseCompleted { _ in #if DEBUG print("✅ [PaywallPresenter] Purchase completed") #endif + onPurchaseSuccess?() dismiss() } + .onPurchaseCancelled { + #if DEBUG + print("ℹ️ [PaywallPresenter] Purchase cancelled by user") + #endif + // User cancelled - paywall stays open, no action needed + } + .onPurchaseFailure { error in + #if DEBUG + print("❌ [PaywallPresenter] Purchase failed: \(error.localizedDescription)") + #endif + errorMessage = error.localizedDescription + showError = true + } .onRestoreCompleted { customerInfo in #if DEBUG print("✅ [PaywallPresenter] Restore completed") #endif - dismiss() + // Only call success if user actually has entitlements after restore + if !customerInfo.entitlements.active.isEmpty { + onPurchaseSuccess?() + dismiss() + } + } + .onRestoreFailure { error in + #if DEBUG + print("❌ [PaywallPresenter] Restore failed: \(error.localizedDescription)") + #endif + errorMessage = error.localizedDescription + showError = true } } diff --git a/SelfieCam/Shared/Premium/PremiumManager.swift b/SelfieCam/Shared/Premium/PremiumManager.swift index 65ac5c8..1519049 100644 --- a/SelfieCam/Shared/Premium/PremiumManager.swift +++ b/SelfieCam/Shared/Premium/PremiumManager.swift @@ -14,13 +14,12 @@ final class PremiumManager: PremiumManaging { /// Task for listening to customer info updates @ObservationIgnored private var customerInfoTask: Task? - /// Reads the RevenueCat API key from Info.plist (injected at build time from Secrets.xcconfig) + /// Reads the RevenueCat API key from the Secrets configuration file private static var apiKey: String { - guard let key = Bundle.main.object(forInfoDictionaryKey: "RevenueCatAPIKey") as? String, - !key.isEmpty, - key != "your_revenuecat_public_api_key_here" else { + let key = Secrets.revenueCatAPIKey + guard !key.isEmpty, key != "YOUR_REVENUECAT_API_KEY_HERE" else { #if DEBUG - print("⚠️ [PremiumManager] RevenueCat API key not configured. See Configuration/Secrets.xcconfig.template") + print("⚠️ [PremiumManager] RevenueCat API key not configured. See Configuration/Secrets.swift.template") #endif return "" }