@umituz/react-native-subscription 2.14.37 → 2.14.39
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/hooks/paywall/index.ts +3 -0
- package/src/presentation/hooks/paywall/types.ts +29 -0
- package/src/presentation/hooks/paywall/usePaywallOperations.ts +140 -0
- package/src/presentation/hooks/paywall/usePaywallRefs.ts +53 -0
- package/src/presentation/hooks/usePaywallOperations.ts +5 -218
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.14.
|
|
3
|
+
"version": "2.14.39",
|
|
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,29 @@
|
|
|
1
|
+
import type { PurchasesPackage } from "react-native-purchases";
|
|
2
|
+
|
|
3
|
+
export type PurchaseSource = "inApp" | "postOnboarding" | null;
|
|
4
|
+
|
|
5
|
+
export interface PaywallOperationsProps {
|
|
6
|
+
userId: string | undefined;
|
|
7
|
+
isAnonymous: boolean;
|
|
8
|
+
onPaywallClose?: () => void;
|
|
9
|
+
onPurchaseSuccess?: () => void;
|
|
10
|
+
onAuthRequired?: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PaywallOperationsResult {
|
|
14
|
+
pendingPackage: PurchasesPackage | null;
|
|
15
|
+
handlePurchase: (pkg: PurchasesPackage) => Promise<boolean>;
|
|
16
|
+
handleRestore: () => Promise<boolean>;
|
|
17
|
+
handleInAppPurchase: (pkg: PurchasesPackage) => Promise<boolean>;
|
|
18
|
+
handleInAppRestore: () => Promise<boolean>;
|
|
19
|
+
completePendingPurchase: () => Promise<boolean>;
|
|
20
|
+
clearPendingPackage: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PaywallRefs {
|
|
24
|
+
userIdRef: React.MutableRefObject<string | undefined>;
|
|
25
|
+
isAnonymousRef: React.MutableRefObject<boolean>;
|
|
26
|
+
purchasePackageRef: React.MutableRefObject<(pkg: PurchasesPackage) => Promise<boolean>>;
|
|
27
|
+
closePaywallRef: React.MutableRefObject<() => void>;
|
|
28
|
+
onPurchaseSuccessRef: React.MutableRefObject<(() => void) | undefined>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { useState, useCallback } from "react";
|
|
2
|
+
import { Alert } from "react-native";
|
|
3
|
+
import { useLocalization } from "@umituz/react-native-localization";
|
|
4
|
+
import type { PurchasesPackage } from "react-native-purchases";
|
|
5
|
+
import { usePremium } from "../usePremium";
|
|
6
|
+
import { usePaywallRefs } from "./usePaywallRefs";
|
|
7
|
+
import type {
|
|
8
|
+
PurchaseSource,
|
|
9
|
+
PaywallOperationsProps,
|
|
10
|
+
PaywallOperationsResult,
|
|
11
|
+
} from "./types";
|
|
12
|
+
|
|
13
|
+
const RERENDER_DELAY_MS = 100;
|
|
14
|
+
|
|
15
|
+
export function usePaywallOperations({
|
|
16
|
+
userId,
|
|
17
|
+
isAnonymous,
|
|
18
|
+
onPaywallClose,
|
|
19
|
+
onPurchaseSuccess,
|
|
20
|
+
onAuthRequired,
|
|
21
|
+
}: PaywallOperationsProps): PaywallOperationsResult {
|
|
22
|
+
const { t } = useLocalization();
|
|
23
|
+
const { purchasePackage, restorePurchase, closePaywall } = usePremium(userId);
|
|
24
|
+
|
|
25
|
+
const [pendingPackage, setPendingPackage] = useState<PurchasesPackage | null>(null);
|
|
26
|
+
const [pendingSource, setPendingSource] = useState<PurchaseSource>(null);
|
|
27
|
+
|
|
28
|
+
const refs = usePaywallRefs({
|
|
29
|
+
userId,
|
|
30
|
+
isAnonymous,
|
|
31
|
+
purchasePackage,
|
|
32
|
+
closePaywall,
|
|
33
|
+
onPurchaseSuccess,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const isAuthenticated = useCallback((): boolean => {
|
|
37
|
+
return !!refs.userIdRef.current && !refs.isAnonymousRef.current;
|
|
38
|
+
}, [refs]);
|
|
39
|
+
|
|
40
|
+
const showError = useCallback(() => {
|
|
41
|
+
Alert.alert(t("premium.purchaseError"), t("premium.purchaseErrorMessage"));
|
|
42
|
+
}, [t]);
|
|
43
|
+
|
|
44
|
+
const handlePurchase = useCallback(
|
|
45
|
+
async (pkg: PurchasesPackage): Promise<boolean> => {
|
|
46
|
+
if (!isAuthenticated()) {
|
|
47
|
+
setPendingPackage(pkg);
|
|
48
|
+
setPendingSource("postOnboarding");
|
|
49
|
+
onAuthRequired?.();
|
|
50
|
+
onPaywallClose?.();
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
const success = await purchasePackage(pkg);
|
|
54
|
+
if (success) onPurchaseSuccess?.();
|
|
55
|
+
else showError();
|
|
56
|
+
return success;
|
|
57
|
+
},
|
|
58
|
+
[isAuthenticated, purchasePackage, onPurchaseSuccess, onPaywallClose, onAuthRequired, showError]
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const handleRestore = useCallback(async (): Promise<boolean> => {
|
|
62
|
+
const success = await restorePurchase();
|
|
63
|
+
Alert.alert(
|
|
64
|
+
success ? t("premium.restoreSuccess") : t("premium.restoreError"),
|
|
65
|
+
success ? t("premium.restoreMessage") : t("premium.restoreErrorMessage")
|
|
66
|
+
);
|
|
67
|
+
if (success) onPurchaseSuccess?.();
|
|
68
|
+
return success;
|
|
69
|
+
}, [restorePurchase, onPurchaseSuccess, t]);
|
|
70
|
+
|
|
71
|
+
const handleInAppPurchase = useCallback(
|
|
72
|
+
async (pkg: PurchasesPackage): Promise<boolean> => {
|
|
73
|
+
if (!isAuthenticated()) {
|
|
74
|
+
setPendingPackage(pkg);
|
|
75
|
+
setPendingSource("inApp");
|
|
76
|
+
onAuthRequired?.();
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
const success = await purchasePackage(pkg);
|
|
80
|
+
if (success) closePaywall();
|
|
81
|
+
else showError();
|
|
82
|
+
return success;
|
|
83
|
+
},
|
|
84
|
+
[isAuthenticated, purchasePackage, closePaywall, onAuthRequired, showError]
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const handleInAppRestore = useCallback(async (): Promise<boolean> => {
|
|
88
|
+
const success = await restorePurchase();
|
|
89
|
+
Alert.alert(
|
|
90
|
+
success ? t("premium.restoreSuccess") : t("premium.restoreError"),
|
|
91
|
+
success ? t("premium.restoreMessage") : t("premium.restoreErrorMessage")
|
|
92
|
+
);
|
|
93
|
+
if (success) closePaywall();
|
|
94
|
+
return success;
|
|
95
|
+
}, [restorePurchase, closePaywall, t]);
|
|
96
|
+
|
|
97
|
+
const completePendingPurchase = useCallback(async (): Promise<boolean> => {
|
|
98
|
+
if (!pendingPackage) {
|
|
99
|
+
if (__DEV__) console.log("[usePaywallOperations] No pending package");
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const pkg = pendingPackage;
|
|
104
|
+
const source = pendingSource;
|
|
105
|
+
setPendingPackage(null);
|
|
106
|
+
setPendingSource(null);
|
|
107
|
+
|
|
108
|
+
await new Promise((resolve) => setTimeout(resolve, RERENDER_DELAY_MS));
|
|
109
|
+
|
|
110
|
+
if (__DEV__) {
|
|
111
|
+
console.log("[usePaywallOperations] Completing:", pkg.identifier, source);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const success = await refs.purchasePackageRef.current(pkg);
|
|
115
|
+
|
|
116
|
+
if (success) {
|
|
117
|
+
source === "inApp"
|
|
118
|
+
? refs.closePaywallRef.current()
|
|
119
|
+
: refs.onPurchaseSuccessRef.current?.();
|
|
120
|
+
} else {
|
|
121
|
+
showError();
|
|
122
|
+
}
|
|
123
|
+
return success;
|
|
124
|
+
}, [pendingPackage, pendingSource, refs, showError]);
|
|
125
|
+
|
|
126
|
+
const clearPendingPackage = useCallback(() => {
|
|
127
|
+
setPendingPackage(null);
|
|
128
|
+
setPendingSource(null);
|
|
129
|
+
}, []);
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
pendingPackage,
|
|
133
|
+
handlePurchase,
|
|
134
|
+
handleRestore,
|
|
135
|
+
handleInAppPurchase,
|
|
136
|
+
handleInAppRestore,
|
|
137
|
+
completePendingPurchase,
|
|
138
|
+
clearPendingPackage,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useRef, useEffect } from "react";
|
|
2
|
+
import type { PurchasesPackage } from "react-native-purchases";
|
|
3
|
+
import type { PaywallRefs } from "./types";
|
|
4
|
+
|
|
5
|
+
interface UsePaywallRefsProps {
|
|
6
|
+
userId: string | undefined;
|
|
7
|
+
isAnonymous: boolean;
|
|
8
|
+
purchasePackage: (pkg: PurchasesPackage) => Promise<boolean>;
|
|
9
|
+
closePaywall: () => void;
|
|
10
|
+
onPurchaseSuccess?: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function usePaywallRefs({
|
|
14
|
+
userId,
|
|
15
|
+
isAnonymous,
|
|
16
|
+
purchasePackage,
|
|
17
|
+
closePaywall,
|
|
18
|
+
onPurchaseSuccess,
|
|
19
|
+
}: UsePaywallRefsProps): PaywallRefs {
|
|
20
|
+
const userIdRef = useRef(userId);
|
|
21
|
+
const isAnonymousRef = useRef(isAnonymous);
|
|
22
|
+
const purchasePackageRef = useRef(purchasePackage);
|
|
23
|
+
const closePaywallRef = useRef(closePaywall);
|
|
24
|
+
const onPurchaseSuccessRef = useRef(onPurchaseSuccess);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
userIdRef.current = userId;
|
|
28
|
+
}, [userId]);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
isAnonymousRef.current = isAnonymous;
|
|
32
|
+
}, [isAnonymous]);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
purchasePackageRef.current = purchasePackage;
|
|
36
|
+
}, [purchasePackage]);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
closePaywallRef.current = closePaywall;
|
|
40
|
+
}, [closePaywall]);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
onPurchaseSuccessRef.current = onPurchaseSuccess;
|
|
44
|
+
}, [onPurchaseSuccess]);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
userIdRef,
|
|
48
|
+
isAnonymousRef,
|
|
49
|
+
purchasePackageRef,
|
|
50
|
+
closePaywallRef,
|
|
51
|
+
onPurchaseSuccessRef,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -1,218 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* Follows "Package Driven Design" by accepting dynamic props.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { useState, useCallback, useRef, useEffect } from "react";
|
|
10
|
-
import { Alert } from "react-native";
|
|
11
|
-
import { useLocalization } from "@umituz/react-native-localization";
|
|
12
|
-
import { usePremium } from "./usePremium";
|
|
13
|
-
import type { PurchasesPackage } from "react-native-purchases";
|
|
14
|
-
|
|
15
|
-
export interface UsePaywallOperationsProps {
|
|
16
|
-
/** Current User ID (or undefined) */
|
|
17
|
-
userId: string | undefined;
|
|
18
|
-
/** Whether the user is anonymous/guest */
|
|
19
|
-
isAnonymous: boolean;
|
|
20
|
-
/** Callback when paywall should close (e.g. close button pressed) */
|
|
21
|
-
onPaywallClose?: () => void;
|
|
22
|
-
/** Callback when purchase completes successfully */
|
|
23
|
-
onPurchaseSuccess?: () => void;
|
|
24
|
-
/** Callback when authentication is required (e.g. for purchase) */
|
|
25
|
-
onAuthRequired?: () => void;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface UsePaywallOperationsResult {
|
|
29
|
-
/** Package that was pending purchase before auth interrupt */
|
|
30
|
-
pendingPackage: PurchasesPackage | null;
|
|
31
|
-
/** Handle purchasing a package */
|
|
32
|
-
handlePurchase: (pkg: PurchasesPackage) => Promise<boolean>;
|
|
33
|
-
/** Handle restoring purchases */
|
|
34
|
-
handleRestore: () => Promise<boolean>;
|
|
35
|
-
/** Handle in-app purchase (with auto-close logic) */
|
|
36
|
-
handleInAppPurchase: (pkg: PurchasesPackage) => Promise<boolean>;
|
|
37
|
-
/** Handle in-app restore (with auto-close logic) */
|
|
38
|
-
handleInAppRestore: () => Promise<boolean>;
|
|
39
|
-
/** Complete pending purchase after authentication */
|
|
40
|
-
completePendingPurchase: () => Promise<boolean>;
|
|
41
|
-
/** Clear pending package without purchasing */
|
|
42
|
-
clearPendingPackage: () => void;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function usePaywallOperations({
|
|
46
|
-
userId,
|
|
47
|
-
isAnonymous,
|
|
48
|
-
onPaywallClose,
|
|
49
|
-
onPurchaseSuccess,
|
|
50
|
-
onAuthRequired,
|
|
51
|
-
}: UsePaywallOperationsProps): UsePaywallOperationsResult {
|
|
52
|
-
const { t } = useLocalization();
|
|
53
|
-
const { purchasePackage, restorePurchase, closePaywall } = usePremium(userId);
|
|
54
|
-
const [pendingPackage, setPendingPackage] = useState<PurchasesPackage | null>(null);
|
|
55
|
-
|
|
56
|
-
// Ref to always have latest purchasePackage function (avoids stale closure)
|
|
57
|
-
const purchasePackageRef = useRef(purchasePackage);
|
|
58
|
-
const onPurchaseSuccessRef = useRef(onPurchaseSuccess);
|
|
59
|
-
|
|
60
|
-
useEffect(() => {
|
|
61
|
-
purchasePackageRef.current = purchasePackage;
|
|
62
|
-
}, [purchasePackage]);
|
|
63
|
-
|
|
64
|
-
useEffect(() => {
|
|
65
|
-
onPurchaseSuccessRef.current = onPurchaseSuccess;
|
|
66
|
-
}, [onPurchaseSuccess]);
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Check if action requires authentication
|
|
70
|
-
* @returns true if authenticated, false if auth required
|
|
71
|
-
*/
|
|
72
|
-
const checkAuth = useCallback((): boolean => {
|
|
73
|
-
if (!userId || isAnonymous) {
|
|
74
|
-
if (__DEV__) {
|
|
75
|
-
console.log("[usePaywallOperations] User not authenticated, triggering onAuthRequired");
|
|
76
|
-
}
|
|
77
|
-
if (onAuthRequired) {
|
|
78
|
-
onAuthRequired();
|
|
79
|
-
}
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
return true;
|
|
83
|
-
}, [userId, isAnonymous, onAuthRequired]);
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Execute purchase flow with Alerts
|
|
87
|
-
*/
|
|
88
|
-
const executePurchase = useCallback(
|
|
89
|
-
async (pkg: PurchasesPackage, onSuccess?: () => void): Promise<boolean> => {
|
|
90
|
-
// 1. Auth Check
|
|
91
|
-
if (!checkAuth()) {
|
|
92
|
-
setPendingPackage(pkg);
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// 2. Purchase
|
|
97
|
-
const success = await purchasePackage(pkg);
|
|
98
|
-
|
|
99
|
-
// 3. Handle Result
|
|
100
|
-
if (success) {
|
|
101
|
-
if (onSuccess) onSuccess();
|
|
102
|
-
} else {
|
|
103
|
-
Alert.alert(
|
|
104
|
-
t("premium.purchaseError"),
|
|
105
|
-
t("premium.purchaseErrorMessage")
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
return success;
|
|
109
|
-
},
|
|
110
|
-
[checkAuth, purchasePackage, t]
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Execute restore flow with Alerts
|
|
115
|
-
*/
|
|
116
|
-
const executeRestore = useCallback(
|
|
117
|
-
async (onSuccess?: () => void): Promise<boolean> => {
|
|
118
|
-
// 1. Restore
|
|
119
|
-
const success = await restorePurchase();
|
|
120
|
-
|
|
121
|
-
// 2. Alert Result
|
|
122
|
-
Alert.alert(
|
|
123
|
-
success ? t("premium.restoreSuccess") : t("premium.restoreError"),
|
|
124
|
-
success
|
|
125
|
-
? t("premium.restoreMessage")
|
|
126
|
-
: t("premium.restoreErrorMessage")
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
// 3. Handle Success
|
|
130
|
-
if (success) {
|
|
131
|
-
if (onSuccess) onSuccess();
|
|
132
|
-
}
|
|
133
|
-
return success;
|
|
134
|
-
},
|
|
135
|
-
[restorePurchase, t]
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
// ============================================================================
|
|
139
|
-
// Public Handlers
|
|
140
|
-
// ============================================================================
|
|
141
|
-
|
|
142
|
-
const handlePurchase = useCallback(
|
|
143
|
-
async (pkg: PurchasesPackage): Promise<boolean> => {
|
|
144
|
-
const result = await executePurchase(pkg, onPurchaseSuccess);
|
|
145
|
-
if (!result && !checkAuth()) {
|
|
146
|
-
if (onPaywallClose) onPaywallClose();
|
|
147
|
-
}
|
|
148
|
-
return result;
|
|
149
|
-
},
|
|
150
|
-
[executePurchase, onPurchaseSuccess, checkAuth, onPaywallClose]
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
const handleRestore = useCallback(
|
|
154
|
-
async (): Promise<boolean> => executeRestore(onPurchaseSuccess),
|
|
155
|
-
[executeRestore, onPurchaseSuccess]
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
const handleInAppPurchase = useCallback(
|
|
159
|
-
async (pkg: PurchasesPackage): Promise<boolean> => {
|
|
160
|
-
const result = await executePurchase(pkg, closePaywall);
|
|
161
|
-
if (!result && !checkAuth()) {
|
|
162
|
-
closePaywall();
|
|
163
|
-
}
|
|
164
|
-
return result;
|
|
165
|
-
},
|
|
166
|
-
[executePurchase, closePaywall, checkAuth]
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
const handleInAppRestore = useCallback(
|
|
170
|
-
async (): Promise<boolean> => executeRestore(closePaywall),
|
|
171
|
-
[executeRestore, closePaywall]
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
const completePendingPurchase = useCallback(async (): Promise<boolean> => {
|
|
175
|
-
if (!pendingPackage) {
|
|
176
|
-
if (__DEV__) {
|
|
177
|
-
console.log("[usePaywallOperations] No pending package to complete");
|
|
178
|
-
}
|
|
179
|
-
return false;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (__DEV__) {
|
|
183
|
-
console.log("[usePaywallOperations] Completing pending purchase:", pendingPackage.identifier);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const pkg = pendingPackage;
|
|
187
|
-
setPendingPackage(null);
|
|
188
|
-
|
|
189
|
-
// Use ref to get latest purchasePackage (avoids stale closure after auth)
|
|
190
|
-
const success = await purchasePackageRef.current(pkg);
|
|
191
|
-
|
|
192
|
-
if (success) {
|
|
193
|
-
// Use ref to get latest callback
|
|
194
|
-
if (onPurchaseSuccessRef.current) onPurchaseSuccessRef.current();
|
|
195
|
-
} else {
|
|
196
|
-
Alert.alert(
|
|
197
|
-
t("premium.purchaseError"),
|
|
198
|
-
t("premium.purchaseErrorMessage")
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return success;
|
|
203
|
-
}, [pendingPackage, t]);
|
|
204
|
-
|
|
205
|
-
const clearPendingPackage = useCallback(() => {
|
|
206
|
-
setPendingPackage(null);
|
|
207
|
-
}, []);
|
|
208
|
-
|
|
209
|
-
return {
|
|
210
|
-
pendingPackage,
|
|
211
|
-
handlePurchase,
|
|
212
|
-
handleRestore,
|
|
213
|
-
handleInAppPurchase,
|
|
214
|
-
handleInAppRestore,
|
|
215
|
-
completePendingPurchase,
|
|
216
|
-
clearPendingPackage,
|
|
217
|
-
};
|
|
218
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
usePaywallOperations,
|
|
3
|
+
type PaywallOperationsProps as UsePaywallOperationsProps,
|
|
4
|
+
type PaywallOperationsResult as UsePaywallOperationsResult,
|
|
5
|
+
} from "./paywall";
|