1- import React , { useEffect , useCallback } from 'react' ;
1+ import React , { useEffect , useCallback , useState } from 'react' ;
22import {
33 ActivityIndicator ,
44 Alert ,
@@ -31,7 +31,7 @@ export function higherTier(a: SupportTier, b: SupportTier): SupportTier {
3131
3232const TIERS = [
3333 {
34- productId : 'haptic_support_bronze ' ,
34+ productId : 'support_0 ' ,
3535 tier : 'support_0' as const ,
3636 fallbackPrice : '$0.99' ,
3737 label : 'Supporter' ,
@@ -40,7 +40,7 @@ const TIERS = [
4040 color : '#b87333' ,
4141 } ,
4242 {
43- productId : 'haptic_support_silver ' ,
43+ productId : 'support_1 ' ,
4444 tier : 'support_1' as const ,
4545 fallbackPrice : '$2.99' ,
4646 label : 'Super Supporter' ,
@@ -49,7 +49,7 @@ const TIERS = [
4949 color : '#9ca3af' ,
5050 } ,
5151 {
52- productId : 'haptic_support_gold ' ,
52+ productId : 'support_2 ' ,
5353 tier : 'support_2' as const ,
5454 fallbackPrice : '$4.99' ,
5555 label : 'Best Supporter' ,
@@ -82,13 +82,18 @@ export default function SupportModal({
8282 currentTier,
8383 onTierChange,
8484} : Props ) {
85+ // ── Purchase in-progress state ───────────────────────────────────────────────
86+
87+ const [ purchasingId , setPurchasingId ] = useState < string | null > ( null ) ;
88+
8589 // ── useIAP hook ──────────────────────────────────────────────────────────────
8690 // The hook manages initConnection on mount and listener cleanup on unmount.
8791 // onPurchaseSuccess / onPurchaseError fire for every purchase event while
8892 // this component is mounted.
8993
9094 const onPurchaseSuccess = useCallback (
9195 async ( purchase : Purchase ) => {
96+ setPurchasingId ( null ) ;
9297 const newTier = tierForProduct ( purchase . productId ) ;
9398 const best = higherTier ( newTier , currentTier ) ;
9499 await AsyncStorage . setItem ( SUPPORT_STORAGE_KEY , best ?? '' ) ;
@@ -98,15 +103,20 @@ export default function SupportModal({
98103 'Thank you! 🙏' ,
99104 `You are now a ${ cfg ?. label ?? 'Supporter' } ! ${ cfg ?. emoji ?? '' } ` ,
100105 ) ;
106+ onClose ( ) ;
101107 } ,
102- [ currentTier , onTierChange ] ,
108+ [ currentTier , onTierChange , onClose , setPurchasingId ] ,
103109 ) ;
104110
105- const onPurchaseError = useCallback ( ( error : PurchaseError ) => {
106- if ( error . code !== ErrorCode . UserCancelled ) {
107- Alert . alert ( 'Purchase failed' , error . message ) ;
108- }
109- } , [ ] ) ;
111+ const onPurchaseError = useCallback (
112+ ( error : PurchaseError ) => {
113+ setPurchasingId ( null ) ;
114+ if ( error . code !== ErrorCode . UserCancelled ) {
115+ Alert . alert ( 'Purchase failed' , error . message ) ;
116+ }
117+ } ,
118+ [ setPurchasingId ] ,
119+ ) ;
110120
111121 const {
112122 connected,
@@ -147,6 +157,7 @@ export default function SupportModal({
147157
148158 const purchase = useCallback (
149159 async ( productId : string ) => {
160+ setPurchasingId ( productId ) ;
150161 try {
151162 await requestPurchase ( {
152163 type : 'in-app' ,
@@ -157,6 +168,7 @@ export default function SupportModal({
157168 } ) ;
158169 } catch ( e : any ) {
159170 // Non-cancellation errors surface via the onPurchaseError callback
171+ setPurchasingId ( null ) ;
160172 }
161173 } ,
162174 [ requestPurchase , finishTransaction ] , // eslint-disable-line react-hooks/exhaustive-deps
@@ -214,6 +226,8 @@ export default function SupportModal({
214226 const product = products . find ( p => p . id === cfg . productId ) ;
215227 const owned = tierRank >= TIER_RANK [ cfg . tier ] ;
216228 const isGold = cfg . tier === 'support_2' ;
229+ const isPurchasing = purchasingId === cfg . productId ;
230+ const isDisabled = owned || purchasingId !== null ;
217231
218232 return (
219233 < Pressable
@@ -226,14 +240,16 @@ export default function SupportModal({
226240 backgroundColor : cfg . color + '1a' ,
227241 borderColor : cfg . color ,
228242 } ,
243+ purchasingId !== null &&
244+ ! isPurchasing && { opacity : 0.45 } ,
229245 pressed &&
230- ! owned && {
246+ ! isDisabled && {
231247 opacity : 0.72 ,
232248 transform : [ { scale : 0.99 } ] ,
233249 } ,
234250 ] }
235- onPress = { ( ) => ! owned && purchase ( cfg . productId ) }
236- disabled = { owned }
251+ onPress = { ( ) => ! isDisabled && purchase ( cfg . productId ) }
252+ disabled = { isDisabled }
237253 >
238254 < Text style = { styles . tierEmoji } > { cfg . emoji } </ Text >
239255 < View style = { styles . tierInfo } >
@@ -262,9 +278,17 @@ export default function SupportModal({
262278 { backgroundColor : cfg . color } ,
263279 ] }
264280 >
265- < Text style = { styles . priceBtnText } >
266- { product ?. displayPrice ?? cfg . fallbackPrice }
267- </ Text >
281+ { isPurchasing ? (
282+ < ActivityIndicator
283+ size = "small"
284+ color = "#fff"
285+ style = { styles . priceBtnSpinner }
286+ />
287+ ) : (
288+ < Text style = { styles . priceBtnText } >
289+ { product ?. displayPrice ?? cfg . fallbackPrice }
290+ </ Text >
291+ ) }
268292 </ View >
269293 ) }
270294 </ View >
@@ -364,6 +388,7 @@ const styles = StyleSheet.create({
364388 paddingVertical : 8 ,
365389 } ,
366390 priceBtnText : { color : '#fff' , fontWeight : '700' , fontSize : 14 } ,
391+ priceBtnSpinner : { width : 20 , height : 20 } ,
367392 ownedLabel : { fontWeight : '700' , fontSize : 13 } ,
368393 legal : { fontSize : 11 , textAlign : 'center' , lineHeight : 16 } ,
369394 closeBtn : { alignItems : 'center' , paddingVertical : 10 } ,
0 commit comments