@umituz/react-native-subscription 2.44.0 → 2.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/domains/paywall/components/PaywallScreen.renderItem.tsx +115 -0
- package/src/domains/paywall/components/PaywallScreen.tsx +41 -119
- package/src/domains/paywall/hooks/usePaywallActions.ts +65 -61
- package/src/domains/paywall/hooks/usePaywallActions.utils.ts +39 -0
- package/src/domains/subscription/application/SubscriptionSyncProcessor.ts +84 -296
- package/src/domains/subscription/application/sync/CreditDocumentOperations.ts +64 -0
- package/src/domains/subscription/application/sync/PurchaseSyncHandler.ts +83 -0
- package/src/domains/subscription/application/sync/RenewalSyncHandler.ts +69 -0
- package/src/domains/subscription/application/sync/StatusChangeSyncHandler.ts +57 -0
- package/src/domains/subscription/application/sync/SyncProcessorLogger.ts +120 -0
- package/src/domains/subscription/application/sync/UserIdResolver.ts +31 -0
- package/src/domains/subscription/presentation/components/ManagedSubscriptionFlow.states.tsx +187 -0
- package/src/domains/subscription/presentation/components/ManagedSubscriptionFlow.tsx +20 -159
- package/src/domains/subscription/presentation/components/feedback/PaywallFeedbackScreen.parts.tsx +201 -0
- package/src/domains/subscription/presentation/components/feedback/PaywallFeedbackScreen.tsx +89 -185
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.45.0",
|
|
4
4
|
"description": "Complete subscription management with RevenueCat, paywall UI, and credits system for React Native apps",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PaywallScreen Render Item Component
|
|
3
|
+
* Separated for better maintainability
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, ListRenderItem } from "react-native";
|
|
8
|
+
import type { ImageSourcePropType } from "react-native";
|
|
9
|
+
import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
|
|
10
|
+
import { Image } from "expo-image";
|
|
11
|
+
import { PlanCard } from "./PlanCard";
|
|
12
|
+
import type { PaywallListItem } from "../utils/paywallLayoutUtils";
|
|
13
|
+
import { paywallScreenStyles as styles } from "./PaywallScreen.styles";
|
|
14
|
+
|
|
15
|
+
interface PaywallRenderItemProps {
|
|
16
|
+
item: PaywallListItem;
|
|
17
|
+
tokens: any;
|
|
18
|
+
translations: any;
|
|
19
|
+
heroImage?: ImageSourcePropType;
|
|
20
|
+
selectedPlanId?: string;
|
|
21
|
+
bestValueIdentifier?: string;
|
|
22
|
+
creditAmounts?: Record<string, number>;
|
|
23
|
+
creditsLabel?: string;
|
|
24
|
+
onSelectPlan?: (planId: string) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const PaywallRenderItem: React.FC<PaywallRenderItemProps> = React.memo(({
|
|
28
|
+
item,
|
|
29
|
+
tokens,
|
|
30
|
+
translations,
|
|
31
|
+
heroImage,
|
|
32
|
+
selectedPlanId,
|
|
33
|
+
bestValueIdentifier,
|
|
34
|
+
creditAmounts,
|
|
35
|
+
creditsLabel,
|
|
36
|
+
onSelectPlan,
|
|
37
|
+
}) => {
|
|
38
|
+
if (!translations) return null;
|
|
39
|
+
|
|
40
|
+
switch (item.type) {
|
|
41
|
+
case 'HEADER':
|
|
42
|
+
return (
|
|
43
|
+
<View key="header">
|
|
44
|
+
{/* Hero Image */}
|
|
45
|
+
{heroImage && (
|
|
46
|
+
<View style={styles.heroContainer}>
|
|
47
|
+
<Image source={heroImage} style={styles.heroImage} contentFit="cover" transition={200} />
|
|
48
|
+
</View>
|
|
49
|
+
)}
|
|
50
|
+
|
|
51
|
+
{/* Header Text */}
|
|
52
|
+
<View style={styles.header}>
|
|
53
|
+
<AtomicText type="headlineLarge" style={[styles.title, { color: tokens.colors.textPrimary }]}>
|
|
54
|
+
{translations.title}
|
|
55
|
+
</AtomicText>
|
|
56
|
+
{translations.subtitle && (
|
|
57
|
+
<AtomicText type="bodyMedium" style={[styles.subtitle, { color: tokens.colors.textSecondary }]}>
|
|
58
|
+
{translations.subtitle}
|
|
59
|
+
</AtomicText>
|
|
60
|
+
)}
|
|
61
|
+
</View>
|
|
62
|
+
</View>
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
case 'FEATURE_HEADER':
|
|
66
|
+
return (
|
|
67
|
+
<View key="feat-header" style={styles.sectionHeader}>
|
|
68
|
+
<AtomicText type="labelLarge" style={[styles.sectionTitle, { color: tokens.colors.textSecondary }]}>
|
|
69
|
+
{translations.featuresTitle || "WHAT'S INCLUDED"}
|
|
70
|
+
</AtomicText>
|
|
71
|
+
</View>
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
case 'FEATURE':
|
|
75
|
+
return (
|
|
76
|
+
<View key={`feat-${item.feature.text}`} style={[styles.featureRow, { marginHorizontal: 24, marginBottom: 16 }]}>
|
|
77
|
+
<View style={[styles.featureIcon, { backgroundColor: tokens.colors.primary }]}>
|
|
78
|
+
<AtomicIcon name={item.feature.icon} customSize={16} customColor={tokens.colors.onPrimary} />
|
|
79
|
+
</View>
|
|
80
|
+
<AtomicText type="bodyMedium" style={[styles.featureText, { color: tokens.colors.textPrimary }]}>
|
|
81
|
+
{item.feature.text}
|
|
82
|
+
</AtomicText>
|
|
83
|
+
</View>
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
case 'PLAN_HEADER':
|
|
87
|
+
return (
|
|
88
|
+
<View key="plan-header" style={styles.sectionHeader}>
|
|
89
|
+
<AtomicText type="labelLarge" style={[styles.sectionTitle, { color: tokens.colors.textSecondary }]}>
|
|
90
|
+
{translations.plansTitle || "CHOOSE YOUR PLAN"}
|
|
91
|
+
</AtomicText>
|
|
92
|
+
</View>
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
case 'PLAN': {
|
|
96
|
+
const pid = item.pkg.product.identifier;
|
|
97
|
+
return (
|
|
98
|
+
<PlanCard
|
|
99
|
+
key={pid}
|
|
100
|
+
pkg={item.pkg}
|
|
101
|
+
isSelected={selectedPlanId === pid}
|
|
102
|
+
badge={bestValueIdentifier === pid ? translations.bestValueBadgeText : undefined}
|
|
103
|
+
creditAmount={creditAmounts?.[pid]}
|
|
104
|
+
creditsLabel={creditsLabel}
|
|
105
|
+
onSelect={() => onSelectPlan?.(pid)}
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
default:
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
PaywallRenderItem.displayName = "PaywallRenderItem";
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Paywall Screen Component
|
|
3
3
|
*
|
|
4
4
|
* Full-screen paywall with optimized FlatList for performance and modern design.
|
|
5
|
+
* Render logic separated to PaywallScreen.renderItem.tsx for better maintainability.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import React, { useCallback, useEffect, useMemo } from "react";
|
|
@@ -14,11 +15,9 @@ import {
|
|
|
14
15
|
StatusBar,
|
|
15
16
|
} from "react-native";
|
|
16
17
|
import { useNavigation } from "@react-navigation/native";
|
|
17
|
-
import {
|
|
18
|
+
import { AtomicIcon, AtomicSpinner } from "@umituz/react-native-design-system/atoms";
|
|
18
19
|
import { useSafeAreaInsets } from "@umituz/react-native-design-system/safe-area";
|
|
19
20
|
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
20
|
-
import { Image } from "expo-image";
|
|
21
|
-
import { PlanCard } from "./PlanCard";
|
|
22
21
|
import { paywallScreenStyles as styles } from "./PaywallScreen.styles";
|
|
23
22
|
import { PaywallFooter } from "./PaywallFooter";
|
|
24
23
|
import { usePaywallActions } from "../hooks/usePaywallActions";
|
|
@@ -28,6 +27,7 @@ import {
|
|
|
28
27
|
type PaywallListItem
|
|
29
28
|
} from "../utils/paywallLayoutUtils";
|
|
30
29
|
import { hasItems } from "../../../shared/utils/arrayUtils";
|
|
30
|
+
import { PaywallRenderItem } from "./PaywallScreen.renderItem";
|
|
31
31
|
|
|
32
32
|
export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) => {
|
|
33
33
|
const navigation = useNavigation();
|
|
@@ -115,7 +115,7 @@ export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) =>
|
|
|
115
115
|
// Prepare flat data for the list
|
|
116
116
|
const flatData = useMemo(() => {
|
|
117
117
|
const data: PaywallListItem[] = [];
|
|
118
|
-
|
|
118
|
+
|
|
119
119
|
// 1. Header (Hero, Title, Subtitle)
|
|
120
120
|
data.push({ type: 'HEADER' });
|
|
121
121
|
|
|
@@ -139,82 +139,20 @@ export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) =>
|
|
|
139
139
|
}, [features, packages]);
|
|
140
140
|
|
|
141
141
|
const renderItem: ListRenderItem<PaywallListItem> = useCallback(({ item }) => {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
<AtomicText type="headlineLarge" style={[styles.title, { color: tokens.colors.textPrimary }]}>
|
|
157
|
-
{translations.title}
|
|
158
|
-
</AtomicText>
|
|
159
|
-
{translations.subtitle && (
|
|
160
|
-
<AtomicText type="bodyMedium" style={[styles.subtitle, { color: tokens.colors.textSecondary }]}>
|
|
161
|
-
{translations.subtitle}
|
|
162
|
-
</AtomicText>
|
|
163
|
-
)}
|
|
164
|
-
</View>
|
|
165
|
-
</View>
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
case 'FEATURE_HEADER':
|
|
169
|
-
return (
|
|
170
|
-
<View key="feat-header" style={styles.sectionHeader}>
|
|
171
|
-
<AtomicText type="labelLarge" style={[styles.sectionTitle, { color: tokens.colors.textSecondary }]}>
|
|
172
|
-
{translations.featuresTitle || "WHAT'S INCLUDED"}
|
|
173
|
-
</AtomicText>
|
|
174
|
-
</View>
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
case 'FEATURE':
|
|
178
|
-
return (
|
|
179
|
-
<View key={`feat-${item.feature.text}`} style={[styles.featureRow, { marginHorizontal: 24, marginBottom: 16 }]}>
|
|
180
|
-
<View style={[styles.featureIcon, { backgroundColor: tokens.colors.primary }]}>
|
|
181
|
-
<AtomicIcon name={item.feature.icon} customSize={16} customColor={tokens.colors.onPrimary} />
|
|
182
|
-
</View>
|
|
183
|
-
<AtomicText type="bodyMedium" style={[styles.featureText, { color: tokens.colors.textPrimary }]}>
|
|
184
|
-
{item.feature.text}
|
|
185
|
-
</AtomicText>
|
|
186
|
-
</View>
|
|
187
|
-
);
|
|
188
|
-
|
|
189
|
-
case 'PLAN_HEADER':
|
|
190
|
-
return (
|
|
191
|
-
<View key="plan-header" style={styles.sectionHeader}>
|
|
192
|
-
<AtomicText type="labelLarge" style={[styles.sectionTitle, { color: tokens.colors.textSecondary }]}>
|
|
193
|
-
{translations.plansTitle || "CHOOSE YOUR PLAN"}
|
|
194
|
-
</AtomicText>
|
|
195
|
-
</View>
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
case 'PLAN': {
|
|
199
|
-
const pid = item.pkg.product.identifier;
|
|
200
|
-
return (
|
|
201
|
-
<PlanCard
|
|
202
|
-
key={pid}
|
|
203
|
-
pkg={item.pkg}
|
|
204
|
-
isSelected={selectedPlanId === pid}
|
|
205
|
-
badge={bestValueIdentifier === pid ? translations.bestValueBadgeText : undefined}
|
|
206
|
-
creditAmount={creditAmounts?.[pid]}
|
|
207
|
-
creditsLabel={creditsLabel}
|
|
208
|
-
onSelect={() => setSelectedPlanId(pid)}
|
|
209
|
-
/>
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
default:
|
|
214
|
-
return null;
|
|
215
|
-
}
|
|
216
|
-
}, [heroImage, translations, tokens, selectedPlanId, bestValueIdentifier, creditAmounts, creditsLabel, setSelectedPlanId]);
|
|
217
|
-
|
|
142
|
+
return (
|
|
143
|
+
<PaywallRenderItem
|
|
144
|
+
item={item}
|
|
145
|
+
tokens={tokens}
|
|
146
|
+
translations={translations}
|
|
147
|
+
heroImage={heroImage}
|
|
148
|
+
selectedPlanId={selectedPlanId}
|
|
149
|
+
bestValueIdentifier={bestValueIdentifier}
|
|
150
|
+
creditAmounts={creditAmounts}
|
|
151
|
+
creditsLabel={creditsLabel}
|
|
152
|
+
onSelectPlan={setSelectedPlanId}
|
|
153
|
+
/>
|
|
154
|
+
);
|
|
155
|
+
}, [tokens, translations, heroImage, selectedPlanId, bestValueIdentifier, creditAmounts, creditsLabel, setSelectedPlanId]);
|
|
218
156
|
|
|
219
157
|
// Performance Optimization: getItemLayout for FlatList
|
|
220
158
|
const getItemLayout = useCallback((_data: ArrayLike<PaywallListItem> | null, index: number) => {
|
|
@@ -227,12 +165,13 @@ export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) =>
|
|
|
227
165
|
return `${item.type}-${index}`;
|
|
228
166
|
}, []);
|
|
229
167
|
|
|
230
|
-
// Defensive check for translations
|
|
168
|
+
// Defensive check for translations
|
|
231
169
|
if (!translations) {
|
|
232
170
|
if (__DEV__) console.warn("[PaywallScreen] Translations prop is missing");
|
|
233
171
|
return null;
|
|
234
172
|
}
|
|
235
173
|
|
|
174
|
+
// Loading state
|
|
236
175
|
if (isSyncing) {
|
|
237
176
|
return (
|
|
238
177
|
<View style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary, paddingTop: insets.top }]}>
|
|
@@ -246,12 +185,12 @@ export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) =>
|
|
|
246
185
|
return (
|
|
247
186
|
<View style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}>
|
|
248
187
|
<StatusBar barStyle="light-content" />
|
|
249
|
-
|
|
188
|
+
|
|
250
189
|
{/* Absolute Close Button */}
|
|
251
|
-
<View style={{
|
|
252
|
-
position: 'absolute',
|
|
253
|
-
top: Math.max(insets.top, 16),
|
|
254
|
-
right: 0,
|
|
190
|
+
<View style={{
|
|
191
|
+
position: 'absolute',
|
|
192
|
+
top: Math.max(insets.top, 16),
|
|
193
|
+
right: 0,
|
|
255
194
|
zIndex: 10,
|
|
256
195
|
}}>
|
|
257
196
|
<TouchableOpacity
|
|
@@ -275,46 +214,29 @@ export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) =>
|
|
|
275
214
|
maxToRenderPerBatch={10}
|
|
276
215
|
updateCellsBatchingPeriod={50}
|
|
277
216
|
contentContainerStyle={[
|
|
278
|
-
styles.listContent,
|
|
279
|
-
{
|
|
217
|
+
styles.listContent,
|
|
218
|
+
{
|
|
280
219
|
paddingTop: Math.max(insets.top, 20) + 40,
|
|
281
|
-
paddingBottom: 220
|
|
220
|
+
paddingBottom: 220
|
|
282
221
|
}
|
|
283
222
|
]}
|
|
284
223
|
showsVerticalScrollIndicator={false}
|
|
285
224
|
/>
|
|
286
225
|
|
|
287
226
|
{/* Fixed Footer */}
|
|
288
|
-
<
|
|
289
|
-
|
|
290
|
-
{
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
(isProcessing || !selectedPlanId) && styles.ctaDisabled
|
|
302
|
-
]}
|
|
303
|
-
activeOpacity={0.8}
|
|
304
|
-
>
|
|
305
|
-
<AtomicText type="titleLarge" style={[styles.ctaText, { color: tokens.colors.onPrimary }]}>
|
|
306
|
-
{isProcessing ? translations.processingText : translations.purchaseButtonText}
|
|
307
|
-
</AtomicText>
|
|
308
|
-
</TouchableOpacity>
|
|
309
|
-
|
|
310
|
-
<PaywallFooter
|
|
311
|
-
translations={translations}
|
|
312
|
-
legalUrls={legalUrls}
|
|
313
|
-
isProcessing={isProcessing}
|
|
314
|
-
onRestore={handleRestore}
|
|
315
|
-
onLegalClick={handleLegalUrl}
|
|
316
|
-
/>
|
|
317
|
-
</View>
|
|
227
|
+
<PaywallFooter
|
|
228
|
+
translations={translations}
|
|
229
|
+
legalUrls={legalUrls}
|
|
230
|
+
isProcessing={isProcessing}
|
|
231
|
+
selectedPlanId={selectedPlanId}
|
|
232
|
+
packages={packages}
|
|
233
|
+
onPurchase={handlePurchase}
|
|
234
|
+
onRestore={handleRestore}
|
|
235
|
+
onLegalUrl={handleLegalUrl}
|
|
236
|
+
insets={insets}
|
|
237
|
+
tokens={tokens}
|
|
238
|
+
styles={styles}
|
|
239
|
+
/>
|
|
318
240
|
</View>
|
|
319
241
|
);
|
|
320
242
|
});
|
|
@@ -1,14 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paywall Actions Hook
|
|
3
|
+
*
|
|
4
|
+
* Handles purchase and restore operations with premium verification.
|
|
5
|
+
* Ref management and success checking extracted to utilities.
|
|
6
|
+
*/
|
|
7
|
+
|
|
1
8
|
import { useState, useCallback, useRef, useMemo } from "react";
|
|
2
9
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
3
|
-
import
|
|
4
|
-
import { useSubscriptionStatus } from "../../subscription/presentation/useSubscriptionStatus";
|
|
5
|
-
import { useCredits } from "../../credits/presentation/useCredits";
|
|
10
|
+
import { usePremiumVerification } from "./usePaywallActions.utils";
|
|
6
11
|
|
|
7
12
|
interface UsePaywallActionsParams {
|
|
8
13
|
packages?: PurchasesPackage[];
|
|
9
14
|
purchasePackage: (pkg: PurchasesPackage) => Promise<boolean>;
|
|
10
15
|
restorePurchase: () => Promise<boolean>;
|
|
11
|
-
source?: PurchaseSource
|
|
16
|
+
source?: string; // PurchaseSource
|
|
12
17
|
onPurchaseSuccess?: () => void;
|
|
13
18
|
onPurchaseError?: (error: Error | string) => void;
|
|
14
19
|
onAuthRequired?: () => void;
|
|
@@ -30,34 +35,42 @@ export function usePaywallActions({
|
|
|
30
35
|
const isProcessingRef = useRef(isProcessing);
|
|
31
36
|
isProcessingRef.current = isProcessing;
|
|
32
37
|
|
|
33
|
-
const {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
38
|
+
const { verifyPremiumStatus } = usePremiumVerification();
|
|
39
|
+
|
|
40
|
+
// Ref management
|
|
41
|
+
const callbacksRef = useRef({
|
|
42
|
+
purchasePackage,
|
|
43
|
+
restorePurchase,
|
|
44
|
+
onPurchaseSuccess,
|
|
45
|
+
onPurchaseError,
|
|
46
|
+
onAuthRequired,
|
|
47
|
+
onClose,
|
|
48
|
+
packages,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Update refs
|
|
52
|
+
callbacksRef.current = {
|
|
53
|
+
purchasePackage,
|
|
54
|
+
restorePurchase,
|
|
55
|
+
onPurchaseSuccess,
|
|
56
|
+
onPurchaseError,
|
|
57
|
+
onAuthRequired,
|
|
58
|
+
onClose,
|
|
59
|
+
packages,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// ─────────────────────────────────────────────────────────────
|
|
63
|
+
// PURCHASE HANDLER
|
|
64
|
+
// ─────────────────────────────────────────────────────────────
|
|
51
65
|
|
|
52
66
|
const handlePurchase = useCallback(async () => {
|
|
53
67
|
const currentSelectedId = selectedPlanId;
|
|
54
68
|
if (!currentSelectedId) return;
|
|
55
|
-
|
|
56
69
|
if (isProcessingRef.current) return;
|
|
57
70
|
|
|
58
|
-
const pkg =
|
|
71
|
+
const pkg = callbacksRef.current.packages.find((p) => p.product.identifier === currentSelectedId);
|
|
59
72
|
if (!pkg) {
|
|
60
|
-
|
|
73
|
+
callbacksRef.current.onPurchaseError?.(new Error(`Package not found: ${currentSelectedId}`));
|
|
61
74
|
return;
|
|
62
75
|
}
|
|
63
76
|
|
|
@@ -70,7 +83,7 @@ export function usePaywallActions({
|
|
|
70
83
|
setIsProcessing(true);
|
|
71
84
|
|
|
72
85
|
try {
|
|
73
|
-
const success = await
|
|
86
|
+
const success = await callbacksRef.current.purchasePackage(pkg);
|
|
74
87
|
|
|
75
88
|
if (__DEV__) {
|
|
76
89
|
console.log('[usePaywallActions] ✅ Purchase completed', { success });
|
|
@@ -78,36 +91,17 @@ export function usePaywallActions({
|
|
|
78
91
|
|
|
79
92
|
let isActuallySuccessful = success === true;
|
|
80
93
|
|
|
94
|
+
// Fallback verification if success is undefined
|
|
81
95
|
if (success === undefined) {
|
|
82
|
-
|
|
83
|
-
console.log('[usePaywallActions] 🔍 Checking premium status as fallback...');
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const [statusResult, creditsResult] = await Promise.all([
|
|
87
|
-
refetchStatus(),
|
|
88
|
-
refetchCredits()
|
|
89
|
-
]);
|
|
90
|
-
|
|
91
|
-
const isSubscriptionPremium = statusResult.data?.isPremium ?? false;
|
|
92
|
-
const isCreditsPremium = creditsResult.data?.isPremium ?? false;
|
|
93
|
-
|
|
94
|
-
isActuallySuccessful = isSubscriptionPremium || isCreditsPremium;
|
|
95
|
-
|
|
96
|
-
if (__DEV__) {
|
|
97
|
-
console.log('[usePaywallActions] 📊 Fallback check result:', {
|
|
98
|
-
isSubscriptionPremium,
|
|
99
|
-
isCreditsPremium,
|
|
100
|
-
isActuallySuccessful
|
|
101
|
-
});
|
|
102
|
-
}
|
|
96
|
+
isActuallySuccessful = await verifyPremiumStatus();
|
|
103
97
|
}
|
|
104
98
|
|
|
105
99
|
if (isActuallySuccessful) {
|
|
106
100
|
if (__DEV__) {
|
|
107
101
|
console.log('[usePaywallActions] 🎉 Purchase successful, closing paywall');
|
|
108
102
|
}
|
|
109
|
-
|
|
110
|
-
|
|
103
|
+
callbacksRef.current.onPurchaseSuccess?.();
|
|
104
|
+
callbacksRef.current.onClose?.();
|
|
111
105
|
} else {
|
|
112
106
|
if (__DEV__) {
|
|
113
107
|
console.warn('[usePaywallActions] ⚠️ Purchase did not indicate success');
|
|
@@ -117,11 +111,15 @@ export function usePaywallActions({
|
|
|
117
111
|
if (__DEV__) {
|
|
118
112
|
console.error('[usePaywallActions] ❌ Purchase error:', error);
|
|
119
113
|
}
|
|
120
|
-
|
|
114
|
+
callbacksRef.current.onPurchaseError?.(error instanceof Error ? error : new Error(String(error)));
|
|
121
115
|
} finally {
|
|
122
116
|
setIsProcessing(false);
|
|
123
117
|
}
|
|
124
|
-
}, [selectedPlanId,
|
|
118
|
+
}, [selectedPlanId, verifyPremiumStatus]);
|
|
119
|
+
|
|
120
|
+
// ─────────────────────────────────────────────────────────────
|
|
121
|
+
// RESTORE HANDLER
|
|
122
|
+
// ─────────────────────────────────────────────────────────────
|
|
125
123
|
|
|
126
124
|
const handleRestore = useCallback(async () => {
|
|
127
125
|
if (isProcessingRef.current) return;
|
|
@@ -131,8 +129,9 @@ export function usePaywallActions({
|
|
|
131
129
|
}
|
|
132
130
|
|
|
133
131
|
setIsProcessing(true);
|
|
132
|
+
|
|
134
133
|
try {
|
|
135
|
-
const success = await
|
|
134
|
+
const success = await callbacksRef.current.restorePurchase();
|
|
136
135
|
|
|
137
136
|
if (__DEV__) {
|
|
138
137
|
console.log('[usePaywallActions] ✅ Restore completed', { success });
|
|
@@ -140,20 +139,17 @@ export function usePaywallActions({
|
|
|
140
139
|
|
|
141
140
|
let isActuallySuccessful = success === true;
|
|
142
141
|
|
|
142
|
+
// Fallback verification if success is undefined
|
|
143
143
|
if (success === undefined) {
|
|
144
|
-
|
|
145
|
-
refetchStatus(),
|
|
146
|
-
refetchCredits()
|
|
147
|
-
]);
|
|
148
|
-
isActuallySuccessful = (statusResult.data?.isPremium ?? false) || (creditsResult.data?.isPremium ?? false);
|
|
144
|
+
isActuallySuccessful = await verifyPremiumStatus();
|
|
149
145
|
}
|
|
150
146
|
|
|
151
147
|
if (isActuallySuccessful) {
|
|
152
148
|
if (__DEV__) {
|
|
153
149
|
console.log('[usePaywallActions] 🎉 Restore successful, closing paywall');
|
|
154
150
|
}
|
|
155
|
-
|
|
156
|
-
|
|
151
|
+
callbacksRef.current.onPurchaseSuccess?.();
|
|
152
|
+
callbacksRef.current.onClose?.();
|
|
157
153
|
} else {
|
|
158
154
|
if (__DEV__) {
|
|
159
155
|
console.warn('[usePaywallActions] ⚠️ Restore did not indicate success');
|
|
@@ -163,11 +159,15 @@ export function usePaywallActions({
|
|
|
163
159
|
if (__DEV__) {
|
|
164
160
|
console.error('[usePaywallActions] ❌ Restore error:', error);
|
|
165
161
|
}
|
|
166
|
-
|
|
162
|
+
callbacksRef.current.onPurchaseError?.(error instanceof Error ? error : new Error(String(error)));
|
|
167
163
|
} finally {
|
|
168
164
|
setIsProcessing(false);
|
|
169
165
|
}
|
|
170
|
-
}, [
|
|
166
|
+
}, [verifyPremiumStatus]);
|
|
167
|
+
|
|
168
|
+
// ─────────────────────────────────────────────────────────────
|
|
169
|
+
// RESET
|
|
170
|
+
// ─────────────────────────────────────────────────────────────
|
|
171
171
|
|
|
172
172
|
const resetState = useCallback(() => {
|
|
173
173
|
if (__DEV__) {
|
|
@@ -177,6 +177,10 @@ export function usePaywallActions({
|
|
|
177
177
|
setIsProcessing(false);
|
|
178
178
|
}, []);
|
|
179
179
|
|
|
180
|
+
// ─────────────────────────────────────────────────────────────
|
|
181
|
+
// RETURN
|
|
182
|
+
// ─────────────────────────────────────────────────────────────
|
|
183
|
+
|
|
180
184
|
return useMemo(() => ({
|
|
181
185
|
selectedPlanId,
|
|
182
186
|
setSelectedPlanId,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paywall Actions Utilities
|
|
3
|
+
* Helper functions for paywall purchase/restore operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useSubscriptionStatus } from "../../subscription/presentation/useSubscriptionStatus";
|
|
7
|
+
import { useCredits } from "../../credits/presentation/useCredits";
|
|
8
|
+
|
|
9
|
+
export function usePremiumVerification() {
|
|
10
|
+
const { refetch: refetchStatus } = useSubscriptionStatus();
|
|
11
|
+
const { refetch: refetchCredits } = useCredits();
|
|
12
|
+
|
|
13
|
+
const verifyPremiumStatus = async (): Promise<boolean> => {
|
|
14
|
+
if (__DEV__) {
|
|
15
|
+
console.log('[PremiumVerification] 🔍 Checking premium status as fallback...');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const [statusResult, creditsResult] = await Promise.all([
|
|
19
|
+
refetchStatus(),
|
|
20
|
+
refetchCredits()
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const isSubscriptionPremium = statusResult.data?.isPremium ?? false;
|
|
24
|
+
const isCreditsPremium = creditsResult.data?.isPremium ?? false;
|
|
25
|
+
const isActuallySuccessful = isSubscriptionPremium || isCreditsPremium;
|
|
26
|
+
|
|
27
|
+
if (__DEV__) {
|
|
28
|
+
console.log('[PremiumVerification] 📊 Fallback check result:', {
|
|
29
|
+
isSubscriptionPremium,
|
|
30
|
+
isCreditsPremium,
|
|
31
|
+
isActuallySuccessful
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return isActuallySuccessful;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return { verifyPremiumStatus };
|
|
39
|
+
}
|