@umituz/react-native-subscription 2.11.7 → 2.11.8
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/presentation/components/details/CreditRow.tsx +83 -0
- package/src/presentation/components/details/DetailRow.tsx +53 -0
- package/src/presentation/components/details/PremiumDetailsCard.tsx +5 -155
- package/src/presentation/components/details/PremiumDetailsCardTypes.ts +41 -0
- package/src/presentation/components/paywall/PaywallLegalFooter.tsx +5 -62
- package/src/presentation/components/paywall/PaywallLegalFooterStyles.ts +53 -0
- package/src/presentation/components/paywall/PaywallLegalFooterTypes.ts +19 -0
- package/src/presentation/components/paywall/PaywallModal.tsx +1 -1
- package/src/presentation/components/paywall/SubscriptionFooter.tsx +0 -4
- package/src/presentation/components/paywall/SubscriptionPlanCard.tsx +5 -67
- package/src/presentation/components/paywall/SubscriptionPlanCardStyles.ts +61 -0
- package/src/presentation/components/paywall/SubscriptionPlanCardTypes.ts +15 -0
- package/src/revenuecat/presentation/hooks/subscriptionQueryKeys.ts +16 -0
- package/src/revenuecat/presentation/hooks/useInitializeSubscription.ts +56 -0
- package/src/revenuecat/presentation/hooks/usePurchasePackage.ts +79 -0
- package/src/revenuecat/presentation/hooks/useRestorePurchase.ts +68 -0
- package/src/revenuecat/presentation/hooks/useSubscriptionPackages.ts +44 -0
- package/src/revenuecat/presentation/hooks/useSubscriptionQueries.ts +9 -216
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.11.
|
|
3
|
+
"version": "2.11.8",
|
|
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,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credit Row Component
|
|
3
|
+
* Displays credit information with progress bar
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, Text, StyleSheet } from "react-native";
|
|
8
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
9
|
+
|
|
10
|
+
export interface CreditRowProps {
|
|
11
|
+
label: string;
|
|
12
|
+
current: number;
|
|
13
|
+
total: number;
|
|
14
|
+
remainingLabel?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const CreditRow: React.FC<CreditRowProps> = ({
|
|
18
|
+
label,
|
|
19
|
+
current,
|
|
20
|
+
total,
|
|
21
|
+
remainingLabel = "remaining",
|
|
22
|
+
}) => {
|
|
23
|
+
const tokens = useAppDesignTokens();
|
|
24
|
+
const percentage = total > 0 ? (current / total) * 100 : 0;
|
|
25
|
+
const isLow = percentage <= 20;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<View style={styles.container}>
|
|
29
|
+
<View style={styles.header}>
|
|
30
|
+
<Text style={[styles.label, { color: tokens.colors.text }]}>
|
|
31
|
+
{label}
|
|
32
|
+
</Text>
|
|
33
|
+
<Text
|
|
34
|
+
style={[
|
|
35
|
+
styles.count,
|
|
36
|
+
{ color: isLow ? tokens.colors.warning : tokens.colors.textSecondary },
|
|
37
|
+
]}
|
|
38
|
+
>
|
|
39
|
+
{current} / {total} {remainingLabel}
|
|
40
|
+
</Text>
|
|
41
|
+
</View>
|
|
42
|
+
<View
|
|
43
|
+
style={[styles.progressBar, { backgroundColor: tokens.colors.surfaceSecondary }]}
|
|
44
|
+
>
|
|
45
|
+
<View
|
|
46
|
+
style={[
|
|
47
|
+
styles.progressFill,
|
|
48
|
+
{
|
|
49
|
+
width: `${percentage}%`,
|
|
50
|
+
backgroundColor: isLow ? tokens.colors.warning : tokens.colors.primary,
|
|
51
|
+
},
|
|
52
|
+
]}
|
|
53
|
+
/>
|
|
54
|
+
</View>
|
|
55
|
+
</View>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const styles = StyleSheet.create({
|
|
60
|
+
container: {
|
|
61
|
+
gap: 4,
|
|
62
|
+
},
|
|
63
|
+
header: {
|
|
64
|
+
flexDirection: "row",
|
|
65
|
+
justifyContent: "space-between",
|
|
66
|
+
alignItems: "center",
|
|
67
|
+
},
|
|
68
|
+
label: {
|
|
69
|
+
fontSize: 13,
|
|
70
|
+
},
|
|
71
|
+
count: {
|
|
72
|
+
fontSize: 12,
|
|
73
|
+
},
|
|
74
|
+
progressBar: {
|
|
75
|
+
height: 6,
|
|
76
|
+
borderRadius: 3,
|
|
77
|
+
overflow: "hidden",
|
|
78
|
+
},
|
|
79
|
+
progressFill: {
|
|
80
|
+
height: "100%",
|
|
81
|
+
borderRadius: 3,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detail Row Component
|
|
3
|
+
* Displays a single detail row with label and value
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, Text, StyleSheet } from "react-native";
|
|
8
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
9
|
+
|
|
10
|
+
export interface DetailRowProps {
|
|
11
|
+
label: string;
|
|
12
|
+
value: string;
|
|
13
|
+
highlight?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const DetailRow: React.FC<DetailRowProps> = ({
|
|
17
|
+
label,
|
|
18
|
+
value,
|
|
19
|
+
highlight = false,
|
|
20
|
+
}) => {
|
|
21
|
+
const tokens = useAppDesignTokens();
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<View style={styles.container}>
|
|
25
|
+
<Text style={[styles.label, { color: tokens.colors.textSecondary }]}>
|
|
26
|
+
{label}
|
|
27
|
+
</Text>
|
|
28
|
+
<Text
|
|
29
|
+
style={[
|
|
30
|
+
styles.value,
|
|
31
|
+
{ color: highlight ? tokens.colors.warning : tokens.colors.text },
|
|
32
|
+
]}
|
|
33
|
+
>
|
|
34
|
+
{value}
|
|
35
|
+
</Text>
|
|
36
|
+
</View>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const styles = StyleSheet.create({
|
|
41
|
+
container: {
|
|
42
|
+
flexDirection: "row",
|
|
43
|
+
justifyContent: "space-between",
|
|
44
|
+
alignItems: "center",
|
|
45
|
+
},
|
|
46
|
+
label: {
|
|
47
|
+
fontSize: 14,
|
|
48
|
+
},
|
|
49
|
+
value: {
|
|
50
|
+
fontSize: 14,
|
|
51
|
+
fontWeight: "500",
|
|
52
|
+
},
|
|
53
|
+
});
|
|
@@ -7,45 +7,12 @@
|
|
|
7
7
|
import React from "react";
|
|
8
8
|
import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
|
|
9
9
|
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from "./
|
|
10
|
+
import { PremiumStatusBadge } from "./PremiumStatusBadge";
|
|
11
|
+
import { DetailRow } from "./DetailRow";
|
|
12
|
+
import { CreditRow } from "./CreditRow";
|
|
13
|
+
import type { PremiumDetailsCardProps } from "./PremiumDetailsCardTypes";
|
|
14
14
|
|
|
15
|
-
export
|
|
16
|
-
id: string;
|
|
17
|
-
label: string;
|
|
18
|
-
current: number;
|
|
19
|
-
total: number;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface PremiumDetailsTranslations {
|
|
23
|
-
title: string;
|
|
24
|
-
statusLabel: string;
|
|
25
|
-
expiresLabel: string;
|
|
26
|
-
purchasedLabel: string;
|
|
27
|
-
creditsTitle?: string;
|
|
28
|
-
remainingLabel?: string;
|
|
29
|
-
manageButton?: string;
|
|
30
|
-
upgradeButton?: string;
|
|
31
|
-
lifetimeLabel?: string;
|
|
32
|
-
statusActive?: string;
|
|
33
|
-
statusExpired?: string;
|
|
34
|
-
statusFree?: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface PremiumDetailsCardProps {
|
|
38
|
-
statusType: SubscriptionStatusType;
|
|
39
|
-
isPremium: boolean;
|
|
40
|
-
expirationDate?: string | null;
|
|
41
|
-
purchaseDate?: string | null;
|
|
42
|
-
isLifetime?: boolean;
|
|
43
|
-
daysRemaining?: number | null;
|
|
44
|
-
credits?: CreditInfo[];
|
|
45
|
-
translations: PremiumDetailsTranslations;
|
|
46
|
-
onManageSubscription?: () => void;
|
|
47
|
-
onUpgrade?: () => void;
|
|
48
|
-
}
|
|
15
|
+
export type { CreditInfo, PremiumDetailsTranslations, PremiumDetailsCardProps } from "./PremiumDetailsCardTypes";
|
|
49
16
|
|
|
50
17
|
export const PremiumDetailsCard: React.FC<PremiumDetailsCardProps> = ({
|
|
51
18
|
statusType,
|
|
@@ -82,7 +49,6 @@ export const PremiumDetailsCard: React.FC<PremiumDetailsCardProps> = ({
|
|
|
82
49
|
<DetailRow
|
|
83
50
|
label={translations.statusLabel}
|
|
84
51
|
value={translations.lifetimeLabel || "Lifetime"}
|
|
85
|
-
tokens={tokens}
|
|
86
52
|
/>
|
|
87
53
|
) : (
|
|
88
54
|
<>
|
|
@@ -95,14 +61,12 @@ export const PremiumDetailsCard: React.FC<PremiumDetailsCardProps> = ({
|
|
|
95
61
|
daysRemaining !== undefined &&
|
|
96
62
|
daysRemaining <= 7
|
|
97
63
|
}
|
|
98
|
-
tokens={tokens}
|
|
99
64
|
/>
|
|
100
65
|
)}
|
|
101
66
|
{purchaseDate && (
|
|
102
67
|
<DetailRow
|
|
103
68
|
label={translations.purchasedLabel}
|
|
104
69
|
value={purchaseDate}
|
|
105
|
-
tokens={tokens}
|
|
106
70
|
/>
|
|
107
71
|
)}
|
|
108
72
|
</>
|
|
@@ -126,7 +90,6 @@ export const PremiumDetailsCard: React.FC<PremiumDetailsCardProps> = ({
|
|
|
126
90
|
current={credit.current}
|
|
127
91
|
total={credit.total}
|
|
128
92
|
remainingLabel={translations.remainingLabel}
|
|
129
|
-
tokens={tokens}
|
|
130
93
|
/>
|
|
131
94
|
))}
|
|
132
95
|
</View>
|
|
@@ -163,84 +126,6 @@ export const PremiumDetailsCard: React.FC<PremiumDetailsCardProps> = ({
|
|
|
163
126
|
);
|
|
164
127
|
};
|
|
165
128
|
|
|
166
|
-
interface DetailRowProps {
|
|
167
|
-
label: string;
|
|
168
|
-
value: string;
|
|
169
|
-
highlight?: boolean;
|
|
170
|
-
tokens: ReturnType<typeof useAppDesignTokens>;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const DetailRow: React.FC<DetailRowProps> = ({
|
|
174
|
-
label,
|
|
175
|
-
value,
|
|
176
|
-
highlight = false,
|
|
177
|
-
tokens,
|
|
178
|
-
}) => (
|
|
179
|
-
<View style={styles.detailRow}>
|
|
180
|
-
<Text style={[styles.detailLabel, { color: tokens.colors.textSecondary }]}>
|
|
181
|
-
{label}
|
|
182
|
-
</Text>
|
|
183
|
-
<Text
|
|
184
|
-
style={[
|
|
185
|
-
styles.detailValue,
|
|
186
|
-
{ color: highlight ? tokens.colors.warning : tokens.colors.text },
|
|
187
|
-
]}
|
|
188
|
-
>
|
|
189
|
-
{value}
|
|
190
|
-
</Text>
|
|
191
|
-
</View>
|
|
192
|
-
);
|
|
193
|
-
|
|
194
|
-
interface CreditRowProps {
|
|
195
|
-
label: string;
|
|
196
|
-
current: number;
|
|
197
|
-
total: number;
|
|
198
|
-
remainingLabel?: string;
|
|
199
|
-
tokens: ReturnType<typeof useAppDesignTokens>;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const CreditRow: React.FC<CreditRowProps> = ({
|
|
203
|
-
label,
|
|
204
|
-
current,
|
|
205
|
-
total,
|
|
206
|
-
remainingLabel = "remaining",
|
|
207
|
-
tokens,
|
|
208
|
-
}) => {
|
|
209
|
-
const percentage = total > 0 ? (current / total) * 100 : 0;
|
|
210
|
-
const isLow = percentage <= 20;
|
|
211
|
-
|
|
212
|
-
return (
|
|
213
|
-
<View style={styles.creditRow}>
|
|
214
|
-
<View style={styles.creditHeader}>
|
|
215
|
-
<Text style={[styles.creditLabel, { color: tokens.colors.text }]}>
|
|
216
|
-
{label}
|
|
217
|
-
</Text>
|
|
218
|
-
<Text
|
|
219
|
-
style={[
|
|
220
|
-
styles.creditCount,
|
|
221
|
-
{ color: isLow ? tokens.colors.warning : tokens.colors.textSecondary },
|
|
222
|
-
]}
|
|
223
|
-
>
|
|
224
|
-
{current} / {total} {remainingLabel}
|
|
225
|
-
</Text>
|
|
226
|
-
</View>
|
|
227
|
-
<View
|
|
228
|
-
style={[styles.progressBar, { backgroundColor: tokens.colors.surfaceSecondary }]}
|
|
229
|
-
>
|
|
230
|
-
<View
|
|
231
|
-
style={[
|
|
232
|
-
styles.progressFill,
|
|
233
|
-
{
|
|
234
|
-
width: `${percentage}%`,
|
|
235
|
-
backgroundColor: isLow ? tokens.colors.warning : tokens.colors.primary,
|
|
236
|
-
},
|
|
237
|
-
]}
|
|
238
|
-
/>
|
|
239
|
-
</View>
|
|
240
|
-
</View>
|
|
241
|
-
);
|
|
242
|
-
};
|
|
243
|
-
|
|
244
129
|
const styles = StyleSheet.create({
|
|
245
130
|
card: {
|
|
246
131
|
borderRadius: 12,
|
|
@@ -264,46 +149,11 @@ const styles = StyleSheet.create({
|
|
|
264
149
|
fontWeight: "600",
|
|
265
150
|
marginBottom: 4,
|
|
266
151
|
},
|
|
267
|
-
detailRow: {
|
|
268
|
-
flexDirection: "row",
|
|
269
|
-
justifyContent: "space-between",
|
|
270
|
-
alignItems: "center",
|
|
271
|
-
},
|
|
272
|
-
detailLabel: {
|
|
273
|
-
fontSize: 14,
|
|
274
|
-
},
|
|
275
|
-
detailValue: {
|
|
276
|
-
fontSize: 14,
|
|
277
|
-
fontWeight: "500",
|
|
278
|
-
},
|
|
279
152
|
creditsSection: {
|
|
280
153
|
gap: 8,
|
|
281
154
|
paddingTop: 12,
|
|
282
155
|
borderTopWidth: 1,
|
|
283
156
|
},
|
|
284
|
-
creditRow: {
|
|
285
|
-
gap: 4,
|
|
286
|
-
},
|
|
287
|
-
creditHeader: {
|
|
288
|
-
flexDirection: "row",
|
|
289
|
-
justifyContent: "space-between",
|
|
290
|
-
alignItems: "center",
|
|
291
|
-
},
|
|
292
|
-
creditLabel: {
|
|
293
|
-
fontSize: 13,
|
|
294
|
-
},
|
|
295
|
-
creditCount: {
|
|
296
|
-
fontSize: 12,
|
|
297
|
-
},
|
|
298
|
-
progressBar: {
|
|
299
|
-
height: 6,
|
|
300
|
-
borderRadius: 3,
|
|
301
|
-
overflow: "hidden",
|
|
302
|
-
},
|
|
303
|
-
progressFill: {
|
|
304
|
-
height: "100%",
|
|
305
|
-
borderRadius: 3,
|
|
306
|
-
},
|
|
307
157
|
actionsSection: {
|
|
308
158
|
gap: 8,
|
|
309
159
|
},
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Premium Details Card Types
|
|
3
|
+
* Type definitions for premium subscription details display
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { SubscriptionStatusType } from "./PremiumStatusBadge";
|
|
7
|
+
|
|
8
|
+
export interface CreditInfo {
|
|
9
|
+
id: string;
|
|
10
|
+
label: string;
|
|
11
|
+
current: number;
|
|
12
|
+
total: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface PremiumDetailsTranslations {
|
|
16
|
+
title: string;
|
|
17
|
+
statusLabel: string;
|
|
18
|
+
expiresLabel: string;
|
|
19
|
+
purchasedLabel: string;
|
|
20
|
+
creditsTitle?: string;
|
|
21
|
+
remainingLabel?: string;
|
|
22
|
+
manageButton?: string;
|
|
23
|
+
upgradeButton?: string;
|
|
24
|
+
lifetimeLabel?: string;
|
|
25
|
+
statusActive?: string;
|
|
26
|
+
statusExpired?: string;
|
|
27
|
+
statusFree?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PremiumDetailsCardProps {
|
|
31
|
+
statusType: SubscriptionStatusType;
|
|
32
|
+
isPremium: boolean;
|
|
33
|
+
expirationDate?: string | null;
|
|
34
|
+
purchaseDate?: string | null;
|
|
35
|
+
isLifetime?: boolean;
|
|
36
|
+
daysRemaining?: number | null;
|
|
37
|
+
credits?: CreditInfo[];
|
|
38
|
+
translations: PremiumDetailsTranslations;
|
|
39
|
+
onManageSubscription?: () => void;
|
|
40
|
+
onUpgrade?: () => void;
|
|
41
|
+
}
|
|
@@ -4,24 +4,14 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import React from "react";
|
|
7
|
-
import { View,
|
|
7
|
+
import { View, TouchableOpacity, Linking } from "react-native";
|
|
8
8
|
import { AtomicText } from "@umituz/react-native-design-system";
|
|
9
9
|
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
10
|
+
import type { PaywallLegalFooterProps } from "./PaywallLegalFooterTypes";
|
|
11
|
+
import { DEFAULT_TERMS } from "./PaywallLegalFooterTypes";
|
|
12
|
+
import { styles } from "./PaywallLegalFooterStyles";
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
termsText?: string;
|
|
13
|
-
privacyUrl?: string;
|
|
14
|
-
termsUrl?: string;
|
|
15
|
-
privacyText?: string;
|
|
16
|
-
termsOfServiceText?: string;
|
|
17
|
-
showRestoreButton?: boolean;
|
|
18
|
-
restoreButtonText?: string;
|
|
19
|
-
onRestore?: () => void;
|
|
20
|
-
isProcessing?: boolean;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const DEFAULT_TERMS =
|
|
24
|
-
"Payment will be charged to your account. Subscription automatically renews unless cancelled.";
|
|
14
|
+
export type { PaywallLegalFooterProps } from "./PaywallLegalFooterTypes";
|
|
25
15
|
|
|
26
16
|
export const PaywallLegalFooter: React.FC<PaywallLegalFooterProps> = React.memo(
|
|
27
17
|
({
|
|
@@ -153,50 +143,3 @@ export const PaywallLegalFooter: React.FC<PaywallLegalFooterProps> = React.memo(
|
|
|
153
143
|
);
|
|
154
144
|
|
|
155
145
|
PaywallLegalFooter.displayName = "PaywallLegalFooter";
|
|
156
|
-
|
|
157
|
-
const styles = StyleSheet.create({
|
|
158
|
-
container: {
|
|
159
|
-
alignItems: "center",
|
|
160
|
-
paddingHorizontal: 24,
|
|
161
|
-
paddingBottom: 24,
|
|
162
|
-
paddingTop: 8,
|
|
163
|
-
width: "100%",
|
|
164
|
-
},
|
|
165
|
-
termsText: {
|
|
166
|
-
textAlign: "center",
|
|
167
|
-
fontSize: 10,
|
|
168
|
-
lineHeight: 14,
|
|
169
|
-
marginBottom: 16,
|
|
170
|
-
opacity: 0.7,
|
|
171
|
-
},
|
|
172
|
-
legalLinksWrapper: {
|
|
173
|
-
width: "100%",
|
|
174
|
-
alignItems: "center",
|
|
175
|
-
},
|
|
176
|
-
legalLinksContainer: {
|
|
177
|
-
flexDirection: "row",
|
|
178
|
-
alignItems: "center",
|
|
179
|
-
justifyContent: "center",
|
|
180
|
-
paddingVertical: 8,
|
|
181
|
-
paddingHorizontal: 16,
|
|
182
|
-
borderRadius: 20,
|
|
183
|
-
backgroundColor: "rgba(255, 255, 255, 0.03)",
|
|
184
|
-
borderWidth: 1,
|
|
185
|
-
borderColor: "rgba(255, 255, 255, 0.05)",
|
|
186
|
-
},
|
|
187
|
-
linkItem: {
|
|
188
|
-
paddingVertical: 2,
|
|
189
|
-
},
|
|
190
|
-
linkText: {
|
|
191
|
-
fontSize: 11,
|
|
192
|
-
fontWeight: "500",
|
|
193
|
-
letterSpacing: 0.3,
|
|
194
|
-
},
|
|
195
|
-
dot: {
|
|
196
|
-
width: 3,
|
|
197
|
-
height: 3,
|
|
198
|
-
borderRadius: 1.5,
|
|
199
|
-
marginHorizontal: 12,
|
|
200
|
-
opacity: 0.3,
|
|
201
|
-
},
|
|
202
|
-
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paywall Legal Footer Styles
|
|
3
|
+
* StyleSheet definitions for paywall legal footer
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { StyleSheet } from "react-native";
|
|
7
|
+
|
|
8
|
+
export const styles = StyleSheet.create({
|
|
9
|
+
container: {
|
|
10
|
+
alignItems: "center",
|
|
11
|
+
paddingHorizontal: 24,
|
|
12
|
+
paddingBottom: 24,
|
|
13
|
+
paddingTop: 8,
|
|
14
|
+
width: "100%",
|
|
15
|
+
},
|
|
16
|
+
termsText: {
|
|
17
|
+
textAlign: "center",
|
|
18
|
+
fontSize: 10,
|
|
19
|
+
lineHeight: 14,
|
|
20
|
+
marginBottom: 16,
|
|
21
|
+
opacity: 0.7,
|
|
22
|
+
},
|
|
23
|
+
legalLinksWrapper: {
|
|
24
|
+
width: "100%",
|
|
25
|
+
alignItems: "center",
|
|
26
|
+
},
|
|
27
|
+
legalLinksContainer: {
|
|
28
|
+
flexDirection: "row",
|
|
29
|
+
alignItems: "center",
|
|
30
|
+
justifyContent: "center",
|
|
31
|
+
paddingVertical: 8,
|
|
32
|
+
paddingHorizontal: 16,
|
|
33
|
+
borderRadius: 20,
|
|
34
|
+
backgroundColor: "rgba(255, 255, 255, 0.03)",
|
|
35
|
+
borderWidth: 1,
|
|
36
|
+
borderColor: "rgba(255, 255, 255, 0.05)",
|
|
37
|
+
},
|
|
38
|
+
linkItem: {
|
|
39
|
+
paddingVertical: 2,
|
|
40
|
+
},
|
|
41
|
+
linkText: {
|
|
42
|
+
fontSize: 11,
|
|
43
|
+
fontWeight: "500",
|
|
44
|
+
letterSpacing: 0.3,
|
|
45
|
+
},
|
|
46
|
+
dot: {
|
|
47
|
+
width: 3,
|
|
48
|
+
height: 3,
|
|
49
|
+
borderRadius: 1.5,
|
|
50
|
+
marginHorizontal: 12,
|
|
51
|
+
opacity: 0.3,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paywall Legal Footer Types
|
|
3
|
+
* Type definitions for paywall legal footer
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface PaywallLegalFooterProps {
|
|
7
|
+
termsText?: string;
|
|
8
|
+
privacyUrl?: string;
|
|
9
|
+
termsUrl?: string;
|
|
10
|
+
privacyText?: string;
|
|
11
|
+
termsOfServiceText?: string;
|
|
12
|
+
showRestoreButton?: boolean;
|
|
13
|
+
restoreButtonText?: string;
|
|
14
|
+
onRestore?: () => void;
|
|
15
|
+
isProcessing?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_TERMS =
|
|
19
|
+
"Payment will be charged to your account. Subscription automatically renews unless cancelled.";
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Displays paywall with Credits and Subscription tabs
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React
|
|
6
|
+
import React from "react";
|
|
7
7
|
import { View, StyleSheet } from "react-native";
|
|
8
8
|
import { BaseModal } from "@umituz/react-native-design-system";
|
|
9
9
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
@@ -108,10 +108,6 @@ const styles = StyleSheet.create({
|
|
|
108
108
|
borderRadius: 16,
|
|
109
109
|
alignItems: "center",
|
|
110
110
|
justifyContent: "center",
|
|
111
|
-
shadowColor: "#000",
|
|
112
|
-
shadowOffset: { width: 0, height: 4 },
|
|
113
|
-
shadowOpacity: 0.2,
|
|
114
|
-
shadowRadius: 8,
|
|
115
111
|
elevation: 4,
|
|
116
112
|
},
|
|
117
113
|
restoreButton: {
|
|
@@ -4,25 +4,18 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import React from "react";
|
|
7
|
-
import { View, TouchableOpacity
|
|
8
|
-
import type { PurchasesPackage } from "react-native-purchases";
|
|
7
|
+
import { View, TouchableOpacity } from "react-native";
|
|
9
8
|
import { AtomicText } from "@umituz/react-native-design-system";
|
|
10
9
|
import { useAppDesignTokens, withAlpha } from "@umituz/react-native-design-system";
|
|
11
10
|
import { formatPrice } from "../../../utils/priceUtils";
|
|
12
11
|
import { useLocalization } from "@umituz/react-native-localization";
|
|
13
12
|
import { BestValueBadge } from "./BestValueBadge";
|
|
14
|
-
|
|
15
13
|
import { getPeriodLabel, isYearlyPackage } from "../../../utils/packagePeriodUtils";
|
|
16
14
|
import { LinearGradient } from "expo-linear-gradient";
|
|
15
|
+
import type { SubscriptionPlanCardProps } from "./SubscriptionPlanCardTypes";
|
|
16
|
+
import { styles } from "./SubscriptionPlanCardStyles";
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
package: PurchasesPackage;
|
|
20
|
-
isSelected: boolean;
|
|
21
|
-
onSelect: () => void;
|
|
22
|
-
isBestValue?: boolean;
|
|
23
|
-
/** Optional: Number of credits/generations included with this package */
|
|
24
|
-
creditAmount?: number;
|
|
25
|
-
}
|
|
18
|
+
export type { SubscriptionPlanCardProps } from "./SubscriptionPlanCardTypes";
|
|
26
19
|
|
|
27
20
|
export const SubscriptionPlanCard: React.FC<SubscriptionPlanCardProps> =
|
|
28
21
|
React.memo(({ package: pkg, isSelected, onSelect, isBestValue = false, creditAmount }) => {
|
|
@@ -147,59 +140,4 @@ export const SubscriptionPlanCard: React.FC<SubscriptionPlanCardProps> =
|
|
|
147
140
|
});
|
|
148
141
|
|
|
149
142
|
|
|
150
|
-
SubscriptionPlanCard.displayName = "SubscriptionPlanCard";
|
|
151
|
-
|
|
152
|
-
const styles = StyleSheet.create({
|
|
153
|
-
container: {
|
|
154
|
-
borderRadius: 16,
|
|
155
|
-
position: "relative",
|
|
156
|
-
overflow: "hidden", // Important for gradient borders/corners
|
|
157
|
-
},
|
|
158
|
-
gradientWrapper: {
|
|
159
|
-
flex: 1,
|
|
160
|
-
padding: 18,
|
|
161
|
-
},
|
|
162
|
-
content: {
|
|
163
|
-
flexDirection: "row",
|
|
164
|
-
justifyContent: "space-between",
|
|
165
|
-
alignItems: "center",
|
|
166
|
-
},
|
|
167
|
-
leftSection: {
|
|
168
|
-
flexDirection: "row",
|
|
169
|
-
alignItems: "center",
|
|
170
|
-
flex: 1,
|
|
171
|
-
},
|
|
172
|
-
radio: {
|
|
173
|
-
width: 24,
|
|
174
|
-
height: 24,
|
|
175
|
-
borderRadius: 12,
|
|
176
|
-
borderWidth: 2,
|
|
177
|
-
alignItems: "center",
|
|
178
|
-
justifyContent: "center",
|
|
179
|
-
marginRight: 16,
|
|
180
|
-
},
|
|
181
|
-
radioInner: {
|
|
182
|
-
width: 12,
|
|
183
|
-
height: 12,
|
|
184
|
-
borderRadius: 6,
|
|
185
|
-
},
|
|
186
|
-
textContainer: {
|
|
187
|
-
flex: 1,
|
|
188
|
-
},
|
|
189
|
-
title: {
|
|
190
|
-
fontWeight: "600",
|
|
191
|
-
marginBottom: 2,
|
|
192
|
-
},
|
|
193
|
-
creditBadge: {
|
|
194
|
-
paddingHorizontal: 10,
|
|
195
|
-
paddingVertical: 4,
|
|
196
|
-
borderRadius: 12,
|
|
197
|
-
marginBottom: 4,
|
|
198
|
-
},
|
|
199
|
-
rightSection: {
|
|
200
|
-
alignItems: "flex-end",
|
|
201
|
-
},
|
|
202
|
-
price: {
|
|
203
|
-
fontWeight: "700",
|
|
204
|
-
},
|
|
205
|
-
});
|
|
143
|
+
SubscriptionPlanCard.displayName = "SubscriptionPlanCard";
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Plan Card Styles
|
|
3
|
+
* StyleSheet definitions for subscription plan card
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { StyleSheet } from "react-native";
|
|
7
|
+
|
|
8
|
+
export const styles = StyleSheet.create({
|
|
9
|
+
container: {
|
|
10
|
+
borderRadius: 16,
|
|
11
|
+
position: "relative",
|
|
12
|
+
overflow: "hidden",
|
|
13
|
+
},
|
|
14
|
+
gradientWrapper: {
|
|
15
|
+
flex: 1,
|
|
16
|
+
padding: 18,
|
|
17
|
+
},
|
|
18
|
+
content: {
|
|
19
|
+
flexDirection: "row",
|
|
20
|
+
justifyContent: "space-between",
|
|
21
|
+
alignItems: "center",
|
|
22
|
+
},
|
|
23
|
+
leftSection: {
|
|
24
|
+
flexDirection: "row",
|
|
25
|
+
alignItems: "center",
|
|
26
|
+
flex: 1,
|
|
27
|
+
},
|
|
28
|
+
radio: {
|
|
29
|
+
width: 24,
|
|
30
|
+
height: 24,
|
|
31
|
+
borderRadius: 12,
|
|
32
|
+
borderWidth: 2,
|
|
33
|
+
alignItems: "center",
|
|
34
|
+
justifyContent: "center",
|
|
35
|
+
marginRight: 16,
|
|
36
|
+
},
|
|
37
|
+
radioInner: {
|
|
38
|
+
width: 12,
|
|
39
|
+
height: 12,
|
|
40
|
+
borderRadius: 6,
|
|
41
|
+
},
|
|
42
|
+
textContainer: {
|
|
43
|
+
flex: 1,
|
|
44
|
+
},
|
|
45
|
+
title: {
|
|
46
|
+
fontWeight: "600",
|
|
47
|
+
marginBottom: 2,
|
|
48
|
+
},
|
|
49
|
+
creditBadge: {
|
|
50
|
+
paddingHorizontal: 10,
|
|
51
|
+
paddingVertical: 4,
|
|
52
|
+
borderRadius: 12,
|
|
53
|
+
marginBottom: 4,
|
|
54
|
+
},
|
|
55
|
+
rightSection: {
|
|
56
|
+
alignItems: "flex-end",
|
|
57
|
+
},
|
|
58
|
+
price: {
|
|
59
|
+
fontWeight: "700",
|
|
60
|
+
},
|
|
61
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Plan Card Types
|
|
3
|
+
* Type definitions for subscription plan display
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { PurchasesPackage } from "react-native-purchases";
|
|
7
|
+
|
|
8
|
+
export interface SubscriptionPlanCardProps {
|
|
9
|
+
package: PurchasesPackage;
|
|
10
|
+
isSelected: boolean;
|
|
11
|
+
onSelect: () => void;
|
|
12
|
+
isBestValue?: boolean;
|
|
13
|
+
/** Optional: Number of credits/generations included with this package */
|
|
14
|
+
creditAmount?: number;
|
|
15
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Query Keys
|
|
3
|
+
* TanStack Query keys and constants for subscription state
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Query keys for TanStack Query
|
|
8
|
+
*/
|
|
9
|
+
export const SUBSCRIPTION_QUERY_KEYS = {
|
|
10
|
+
packages: ["subscription", "packages"] as const,
|
|
11
|
+
initialized: (userId: string) =>
|
|
12
|
+
["subscription", "initialized", userId] as const,
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
export const STALE_TIME = 5 * 60 * 1000; // 5 minutes
|
|
16
|
+
export const GC_TIME = 30 * 60 * 1000; // 30 minutes
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Initialize Subscription Hook
|
|
3
|
+
* TanStack mutation for initializing RevenueCat
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
7
|
+
import { SubscriptionManager } from "../../infrastructure/managers/SubscriptionManager";
|
|
8
|
+
import {
|
|
9
|
+
trackPackageError,
|
|
10
|
+
addPackageBreadcrumb,
|
|
11
|
+
} from "@umituz/react-native-sentry";
|
|
12
|
+
import { SUBSCRIPTION_QUERY_KEYS } from "./subscriptionQueryKeys";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Initialize subscription with RevenueCat
|
|
16
|
+
*/
|
|
17
|
+
export const useInitializeSubscription = (userId: string | undefined) => {
|
|
18
|
+
const queryClient = useQueryClient();
|
|
19
|
+
|
|
20
|
+
return useMutation({
|
|
21
|
+
mutationFn: async () => {
|
|
22
|
+
if (!userId) {
|
|
23
|
+
throw new Error("User not authenticated");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
addPackageBreadcrumb("subscription", "Initialize mutation started", {
|
|
27
|
+
userId,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return SubscriptionManager.initialize(userId);
|
|
31
|
+
},
|
|
32
|
+
onSuccess: () => {
|
|
33
|
+
if (userId) {
|
|
34
|
+
queryClient.invalidateQueries({
|
|
35
|
+
queryKey: SUBSCRIPTION_QUERY_KEYS.packages,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
addPackageBreadcrumb(
|
|
39
|
+
"subscription",
|
|
40
|
+
"Initialize mutation success - packages invalidated",
|
|
41
|
+
{ userId }
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
onError: (error) => {
|
|
46
|
+
trackPackageError(
|
|
47
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
48
|
+
{
|
|
49
|
+
packageName: "subscription",
|
|
50
|
+
operation: "initialize_mutation",
|
|
51
|
+
userId: userId ?? "NO_USER",
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purchase Package Hook
|
|
3
|
+
* TanStack mutation for purchasing subscription packages
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
7
|
+
import type { PurchasesPackage } from "react-native-purchases";
|
|
8
|
+
import { SubscriptionManager } from "../../infrastructure/managers/SubscriptionManager";
|
|
9
|
+
import {
|
|
10
|
+
trackPackageError,
|
|
11
|
+
addPackageBreadcrumb,
|
|
12
|
+
} from "@umituz/react-native-sentry";
|
|
13
|
+
import { SUBSCRIPTION_QUERY_KEYS } from "./subscriptionQueryKeys";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Purchase a subscription package
|
|
17
|
+
*/
|
|
18
|
+
export const usePurchasePackage = (userId: string | undefined) => {
|
|
19
|
+
const queryClient = useQueryClient();
|
|
20
|
+
|
|
21
|
+
return useMutation({
|
|
22
|
+
mutationFn: async (pkg: PurchasesPackage) => {
|
|
23
|
+
if (!userId) {
|
|
24
|
+
throw new Error("User not authenticated");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
addPackageBreadcrumb("subscription", "Purchase started", {
|
|
28
|
+
packageId: pkg.identifier,
|
|
29
|
+
userId,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
addPackageBreadcrumb("subscription", "Purchase mutation started", {
|
|
33
|
+
packageId: pkg.identifier,
|
|
34
|
+
userId,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const success = await SubscriptionManager.purchasePackage(pkg);
|
|
38
|
+
|
|
39
|
+
if (success) {
|
|
40
|
+
addPackageBreadcrumb("subscription", "Purchase success", {
|
|
41
|
+
packageId: pkg.identifier,
|
|
42
|
+
userId,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
addPackageBreadcrumb("subscription", "Purchase mutation success", {
|
|
46
|
+
packageId: pkg.identifier,
|
|
47
|
+
userId,
|
|
48
|
+
});
|
|
49
|
+
} else {
|
|
50
|
+
addPackageBreadcrumb("subscription", "Purchase cancelled", {
|
|
51
|
+
packageId: pkg.identifier,
|
|
52
|
+
userId,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
addPackageBreadcrumb("subscription", "Purchase mutation failed", {
|
|
56
|
+
packageId: pkg.identifier,
|
|
57
|
+
userId,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return success;
|
|
62
|
+
},
|
|
63
|
+
onSuccess: () => {
|
|
64
|
+
queryClient.invalidateQueries({
|
|
65
|
+
queryKey: SUBSCRIPTION_QUERY_KEYS.packages,
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
onError: (error) => {
|
|
69
|
+
trackPackageError(
|
|
70
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
71
|
+
{
|
|
72
|
+
packageName: "subscription",
|
|
73
|
+
operation: "purchase_mutation",
|
|
74
|
+
userId: userId ?? "NO_USER",
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Restore Purchase Hook
|
|
3
|
+
* TanStack mutation for restoring previous purchases
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
7
|
+
import { SubscriptionManager } from "../../infrastructure/managers/SubscriptionManager";
|
|
8
|
+
import {
|
|
9
|
+
trackPackageError,
|
|
10
|
+
addPackageBreadcrumb,
|
|
11
|
+
} from "@umituz/react-native-sentry";
|
|
12
|
+
import { SUBSCRIPTION_QUERY_KEYS } from "./subscriptionQueryKeys";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Restore previous purchases
|
|
16
|
+
*/
|
|
17
|
+
export const useRestorePurchase = (userId: string | undefined) => {
|
|
18
|
+
const queryClient = useQueryClient();
|
|
19
|
+
|
|
20
|
+
return useMutation({
|
|
21
|
+
mutationFn: async () => {
|
|
22
|
+
if (!userId) {
|
|
23
|
+
throw new Error("User not authenticated");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
addPackageBreadcrumb("subscription", "Restore started", {
|
|
27
|
+
userId,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
addPackageBreadcrumb("subscription", "Restore mutation started", {
|
|
31
|
+
userId,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const success = await SubscriptionManager.restore();
|
|
35
|
+
|
|
36
|
+
if (success) {
|
|
37
|
+
addPackageBreadcrumb("subscription", "Restore success", {
|
|
38
|
+
userId,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
addPackageBreadcrumb("subscription", "Restore mutation success", {
|
|
42
|
+
userId,
|
|
43
|
+
});
|
|
44
|
+
} else {
|
|
45
|
+
addPackageBreadcrumb("subscription", "Restore mutation failed", {
|
|
46
|
+
userId,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return success;
|
|
51
|
+
},
|
|
52
|
+
onSuccess: () => {
|
|
53
|
+
queryClient.invalidateQueries({
|
|
54
|
+
queryKey: SUBSCRIPTION_QUERY_KEYS.packages,
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
onError: (error) => {
|
|
58
|
+
trackPackageError(
|
|
59
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
60
|
+
{
|
|
61
|
+
packageName: "subscription",
|
|
62
|
+
operation: "restore_mutation",
|
|
63
|
+
userId: userId ?? "NO_USER",
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Packages Hook
|
|
3
|
+
* TanStack query for fetching available packages
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useQuery } from "@tanstack/react-query";
|
|
7
|
+
import { SubscriptionManager } from "../../infrastructure/managers/SubscriptionManager";
|
|
8
|
+
import { addPackageBreadcrumb } from "@umituz/react-native-sentry";
|
|
9
|
+
import {
|
|
10
|
+
SUBSCRIPTION_QUERY_KEYS,
|
|
11
|
+
STALE_TIME,
|
|
12
|
+
GC_TIME,
|
|
13
|
+
} from "./subscriptionQueryKeys";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Fetch available subscription packages
|
|
17
|
+
*/
|
|
18
|
+
export const useSubscriptionPackages = (userId: string | undefined) => {
|
|
19
|
+
return useQuery({
|
|
20
|
+
queryKey: [...SUBSCRIPTION_QUERY_KEYS.packages, userId] as const,
|
|
21
|
+
queryFn: async () => {
|
|
22
|
+
addPackageBreadcrumb("subscription", "Fetch packages query started", {
|
|
23
|
+
userId: userId ?? "NO_USER",
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Skip if already initialized for this specific user
|
|
27
|
+
if (!userId || !SubscriptionManager.isInitializedForUser(userId)) {
|
|
28
|
+
await SubscriptionManager.initialize(userId);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const packages = await SubscriptionManager.getPackages();
|
|
32
|
+
|
|
33
|
+
addPackageBreadcrumb("subscription", "Fetch packages query success", {
|
|
34
|
+
userId: userId ?? "NO_USER",
|
|
35
|
+
count: packages.length,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return packages;
|
|
39
|
+
},
|
|
40
|
+
staleTime: STALE_TIME,
|
|
41
|
+
gcTime: GC_TIME,
|
|
42
|
+
enabled: !!userId, // Only run when userId is available
|
|
43
|
+
});
|
|
44
|
+
};
|
|
@@ -4,219 +4,12 @@
|
|
|
4
4
|
* Generic hooks for 100+ apps
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from "
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
* Query keys for TanStack Query
|
|
17
|
-
*/
|
|
18
|
-
export const SUBSCRIPTION_QUERY_KEYS = {
|
|
19
|
-
packages: ["subscription", "packages"] as const,
|
|
20
|
-
initialized: (userId: string) =>
|
|
21
|
-
["subscription", "initialized", userId] as const,
|
|
22
|
-
} as const;
|
|
23
|
-
|
|
24
|
-
const STALE_TIME = 5 * 60 * 1000; // 5 minutes
|
|
25
|
-
const GC_TIME = 30 * 60 * 1000; // 30 minutes
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Initialize subscription with RevenueCat
|
|
29
|
-
*/
|
|
30
|
-
export const useInitializeSubscription = (userId: string | undefined) => {
|
|
31
|
-
const queryClient = useQueryClient();
|
|
32
|
-
|
|
33
|
-
return useMutation({
|
|
34
|
-
mutationFn: async () => {
|
|
35
|
-
if (!userId) {
|
|
36
|
-
throw new Error("User not authenticated");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
addPackageBreadcrumb("subscription", "Initialize mutation started", {
|
|
40
|
-
userId,
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
return SubscriptionManager.initialize(userId);
|
|
44
|
-
},
|
|
45
|
-
onSuccess: () => {
|
|
46
|
-
if (userId) {
|
|
47
|
-
queryClient.invalidateQueries({
|
|
48
|
-
queryKey: SUBSCRIPTION_QUERY_KEYS.packages,
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
addPackageBreadcrumb(
|
|
52
|
-
"subscription",
|
|
53
|
-
"Initialize mutation success - packages invalidated",
|
|
54
|
-
{ userId }
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
},
|
|
58
|
-
onError: (error) => {
|
|
59
|
-
trackPackageError(
|
|
60
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
61
|
-
{
|
|
62
|
-
packageName: "subscription",
|
|
63
|
-
operation: "initialize_mutation",
|
|
64
|
-
userId: userId ?? "NO_USER",
|
|
65
|
-
}
|
|
66
|
-
);
|
|
67
|
-
},
|
|
68
|
-
});
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Fetch available subscription packages
|
|
73
|
-
*/
|
|
74
|
-
export const useSubscriptionPackages = (userId: string | undefined) => {
|
|
75
|
-
return useQuery({
|
|
76
|
-
queryKey: [...SUBSCRIPTION_QUERY_KEYS.packages, userId] as const,
|
|
77
|
-
queryFn: async () => {
|
|
78
|
-
addPackageBreadcrumb("subscription", "Fetch packages query started", {
|
|
79
|
-
userId: userId ?? "NO_USER",
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
// Skip if already initialized for this specific user
|
|
83
|
-
if (!userId || !SubscriptionManager.isInitializedForUser(userId)) {
|
|
84
|
-
await SubscriptionManager.initialize(userId);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const packages = await SubscriptionManager.getPackages();
|
|
88
|
-
|
|
89
|
-
addPackageBreadcrumb("subscription", "Fetch packages query success", {
|
|
90
|
-
userId: userId ?? "NO_USER",
|
|
91
|
-
count: packages.length,
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
return packages;
|
|
95
|
-
},
|
|
96
|
-
staleTime: STALE_TIME,
|
|
97
|
-
gcTime: GC_TIME,
|
|
98
|
-
enabled: !!userId, // Only run when userId is available
|
|
99
|
-
});
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Purchase a subscription package
|
|
104
|
-
*/
|
|
105
|
-
export const usePurchasePackage = (userId: string | undefined) => {
|
|
106
|
-
const queryClient = useQueryClient();
|
|
107
|
-
|
|
108
|
-
return useMutation({
|
|
109
|
-
mutationFn: async (pkg: PurchasesPackage) => {
|
|
110
|
-
if (!userId) {
|
|
111
|
-
throw new Error("User not authenticated");
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
addPackageBreadcrumb("subscription", "Purchase started", {
|
|
115
|
-
packageId: pkg.identifier,
|
|
116
|
-
userId,
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
addPackageBreadcrumb("subscription", "Purchase mutation started", {
|
|
120
|
-
packageId: pkg.identifier,
|
|
121
|
-
userId,
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
const success = await SubscriptionManager.purchasePackage(pkg);
|
|
125
|
-
|
|
126
|
-
if (success) {
|
|
127
|
-
addPackageBreadcrumb("subscription", "Purchase success", {
|
|
128
|
-
packageId: pkg.identifier,
|
|
129
|
-
userId,
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
addPackageBreadcrumb("subscription", "Purchase mutation success", {
|
|
133
|
-
packageId: pkg.identifier,
|
|
134
|
-
userId,
|
|
135
|
-
});
|
|
136
|
-
} else {
|
|
137
|
-
addPackageBreadcrumb("subscription", "Purchase cancelled", {
|
|
138
|
-
packageId: pkg.identifier,
|
|
139
|
-
userId,
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
addPackageBreadcrumb("subscription", "Purchase mutation failed", {
|
|
143
|
-
packageId: pkg.identifier,
|
|
144
|
-
userId,
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return success;
|
|
149
|
-
},
|
|
150
|
-
onSuccess: () => {
|
|
151
|
-
queryClient.invalidateQueries({
|
|
152
|
-
queryKey: SUBSCRIPTION_QUERY_KEYS.packages,
|
|
153
|
-
});
|
|
154
|
-
},
|
|
155
|
-
onError: (error) => {
|
|
156
|
-
trackPackageError(
|
|
157
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
158
|
-
{
|
|
159
|
-
packageName: "subscription",
|
|
160
|
-
operation: "purchase_mutation",
|
|
161
|
-
userId: userId ?? "NO_USER",
|
|
162
|
-
}
|
|
163
|
-
);
|
|
164
|
-
},
|
|
165
|
-
});
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Restore previous purchases
|
|
170
|
-
*/
|
|
171
|
-
export const useRestorePurchase = (userId: string | undefined) => {
|
|
172
|
-
const queryClient = useQueryClient();
|
|
173
|
-
|
|
174
|
-
return useMutation({
|
|
175
|
-
mutationFn: async () => {
|
|
176
|
-
if (!userId) {
|
|
177
|
-
throw new Error("User not authenticated");
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
addPackageBreadcrumb("subscription", "Restore started", {
|
|
181
|
-
userId,
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
addPackageBreadcrumb("subscription", "Restore mutation started", {
|
|
185
|
-
userId,
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
const success = await SubscriptionManager.restore();
|
|
189
|
-
|
|
190
|
-
if (success) {
|
|
191
|
-
addPackageBreadcrumb("subscription", "Restore success", {
|
|
192
|
-
userId,
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
addPackageBreadcrumb("subscription", "Restore mutation success", {
|
|
196
|
-
userId,
|
|
197
|
-
});
|
|
198
|
-
} else {
|
|
199
|
-
addPackageBreadcrumb("subscription", "Restore mutation failed", {
|
|
200
|
-
userId,
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return success;
|
|
205
|
-
},
|
|
206
|
-
onSuccess: () => {
|
|
207
|
-
queryClient.invalidateQueries({
|
|
208
|
-
queryKey: SUBSCRIPTION_QUERY_KEYS.packages,
|
|
209
|
-
});
|
|
210
|
-
},
|
|
211
|
-
onError: (error) => {
|
|
212
|
-
trackPackageError(
|
|
213
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
214
|
-
{
|
|
215
|
-
packageName: "subscription",
|
|
216
|
-
operation: "restore_mutation",
|
|
217
|
-
userId: userId ?? "NO_USER",
|
|
218
|
-
}
|
|
219
|
-
);
|
|
220
|
-
},
|
|
221
|
-
});
|
|
222
|
-
};
|
|
7
|
+
export {
|
|
8
|
+
SUBSCRIPTION_QUERY_KEYS,
|
|
9
|
+
STALE_TIME,
|
|
10
|
+
GC_TIME,
|
|
11
|
+
} from "./subscriptionQueryKeys";
|
|
12
|
+
export { useInitializeSubscription } from "./useInitializeSubscription";
|
|
13
|
+
export { useSubscriptionPackages } from "./useSubscriptionPackages";
|
|
14
|
+
export { usePurchasePackage } from "./usePurchasePackage";
|
|
15
|
+
export { useRestorePurchase } from "./useRestorePurchase";
|