@umituz/react-native-subscription 2.16.8 → 2.17.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/PaywallContainer.tsx +12 -28
- package/src/domains/paywall/components/PaywallContainer.types.ts +2 -0
- package/src/domains/paywall/hooks/usePaywallActions.ts +10 -7
- package/src/presentation/hooks/index.ts +2 -1
- package/src/presentation/hooks/useCompletePendingPurchase.ts +127 -0
- package/src/presentation/hooks/usePendingPurchase.ts +88 -0
- package/src/presentation/hooks/paywall/index.ts +0 -3
- package/src/presentation/hooks/paywall/types.ts +0 -29
- package/src/presentation/hooks/paywall/usePaywallOperations.ts +0 -139
- package/src/presentation/hooks/paywall/usePaywallRefs.ts +0 -53
- package/src/presentation/hooks/usePaywallOperations.ts +0 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.17.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",
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PaywallContainer Component
|
|
3
|
+
* Uses centralized pending purchase state - no local auth handling
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
|
-
import React, {
|
|
6
|
+
import React, { useMemo } from "react";
|
|
6
7
|
import { usePaywallVisibility } from "../../../presentation/hooks/usePaywallVisibility";
|
|
7
8
|
import { useSubscriptionPackages } from "../../../revenuecat/presentation/hooks/useSubscriptionPackages";
|
|
8
9
|
import { filterPackagesByMode } from "../../../utils/packageFilter";
|
|
@@ -11,46 +12,29 @@ import { PaywallModal } from "./PaywallModal";
|
|
|
11
12
|
import { usePaywallActions } from "../hooks/usePaywallActions";
|
|
12
13
|
import type { PaywallContainerProps } from "./PaywallContainer.types";
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
15
|
export const PaywallContainer: React.FC<PaywallContainerProps> = (props) => {
|
|
17
|
-
const { userId, isAnonymous = false, translations, mode = "subscription", legalUrls, features, heroImage, bestValueIdentifier, creditsLabel, creditAmounts, packageFilterConfig, onPurchaseSuccess, onPurchaseError, onAuthRequired, visible, onClose } = props;
|
|
18
|
-
|
|
16
|
+
const { userId, isAnonymous = false, translations, mode = "subscription", legalUrls, features, heroImage, bestValueIdentifier, creditsLabel, creditAmounts, packageFilterConfig, source = "inApp", onPurchaseSuccess, onPurchaseError, onAuthRequired, visible, onClose } = props;
|
|
17
|
+
|
|
19
18
|
const { showPaywall, closePaywall } = usePaywallVisibility();
|
|
20
19
|
const isVisible = visible ?? showPaywall;
|
|
21
20
|
const handleClose = onClose ?? closePaywall;
|
|
22
21
|
|
|
23
22
|
const { data: allPackages = [], isLoading } = useSubscriptionPackages(userId ?? undefined);
|
|
24
|
-
const { handlePurchase, handleRestore
|
|
25
|
-
userId: userId ?? undefined,
|
|
23
|
+
const { handlePurchase, handleRestore } = usePaywallActions({
|
|
24
|
+
userId: userId ?? undefined,
|
|
25
|
+
isAnonymous,
|
|
26
|
+
source,
|
|
27
|
+
onPurchaseSuccess,
|
|
28
|
+
onPurchaseError,
|
|
29
|
+
onAuthRequired,
|
|
30
|
+
onClose: handleClose,
|
|
26
31
|
});
|
|
27
32
|
|
|
28
|
-
const wasAnonymousRef = useRef(isAnonymous);
|
|
29
33
|
const { filteredPackages, computedCreditAmounts } = useMemo(() => ({
|
|
30
34
|
filteredPackages: filterPackagesByMode(allPackages, mode, packageFilterConfig),
|
|
31
35
|
computedCreditAmounts: mode !== "subscription" && !creditAmounts ? createCreditAmountsFromPackages(allPackages) : creditAmounts
|
|
32
36
|
}), [allPackages, mode, packageFilterConfig, creditAmounts]);
|
|
33
37
|
|
|
34
|
-
// Auto-purchase after auth
|
|
35
|
-
useEffect(() => {
|
|
36
|
-
const wasAuth = wasAnonymousRef.current && !isAnonymous;
|
|
37
|
-
wasAnonymousRef.current = isAnonymous;
|
|
38
|
-
|
|
39
|
-
if (wasAuth && pendingPackage && userId) {
|
|
40
|
-
(async () => {
|
|
41
|
-
try {
|
|
42
|
-
const res = await purchasePackage(pendingPackage);
|
|
43
|
-
if (res.success) {
|
|
44
|
-
onPurchaseSuccess?.();
|
|
45
|
-
handleClose();
|
|
46
|
-
}
|
|
47
|
-
} catch (err: any) {
|
|
48
|
-
onPurchaseError?.(err.message || String(err));
|
|
49
|
-
} finally { setPendingPackage(null); }
|
|
50
|
-
})();
|
|
51
|
-
}
|
|
52
|
-
}, [isAnonymous, userId, pendingPackage, purchasePackage, onPurchaseSuccess, onPurchaseError, handleClose, setPendingPackage]);
|
|
53
|
-
|
|
54
38
|
if (!isVisible) return null;
|
|
55
39
|
|
|
56
40
|
return (
|
|
@@ -30,6 +30,8 @@ export interface PaywallContainerProps {
|
|
|
30
30
|
readonly creditAmounts?: Record<string, number>;
|
|
31
31
|
/** Custom filter config for package categorization */
|
|
32
32
|
readonly packageFilterConfig?: PackageFilterConfig;
|
|
33
|
+
/** Source of the paywall - affects pending purchase handling */
|
|
34
|
+
readonly source?: "postOnboarding" | "inApp";
|
|
33
35
|
/** Callback when purchase succeeds */
|
|
34
36
|
readonly onPurchaseSuccess?: () => void;
|
|
35
37
|
/** Callback when purchase fails */
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { useCallback
|
|
1
|
+
import { useCallback } from "react";
|
|
2
2
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
3
3
|
import { usePurchasePackage } from "../../../revenuecat/presentation/hooks/usePurchasePackage";
|
|
4
4
|
import { useRestorePurchase } from "../../../revenuecat/presentation/hooks/useRestorePurchase";
|
|
5
|
+
import { usePendingPurchase } from "../../../presentation/hooks/usePendingPurchase";
|
|
5
6
|
|
|
6
7
|
declare const __DEV__: boolean;
|
|
7
8
|
|
|
8
9
|
interface UsePaywallActionsProps {
|
|
9
10
|
userId?: string;
|
|
10
11
|
isAnonymous: boolean;
|
|
12
|
+
source?: "postOnboarding" | "inApp";
|
|
11
13
|
onPurchaseSuccess?: () => void;
|
|
12
14
|
onPurchaseError?: (error: string) => void;
|
|
13
15
|
onAuthRequired?: () => void;
|
|
@@ -17,6 +19,7 @@ interface UsePaywallActionsProps {
|
|
|
17
19
|
export const usePaywallActions = ({
|
|
18
20
|
userId,
|
|
19
21
|
isAnonymous,
|
|
22
|
+
source = "inApp",
|
|
20
23
|
onPurchaseSuccess,
|
|
21
24
|
onPurchaseError,
|
|
22
25
|
onAuthRequired,
|
|
@@ -24,18 +27,18 @@ export const usePaywallActions = ({
|
|
|
24
27
|
}: UsePaywallActionsProps) => {
|
|
25
28
|
const { mutateAsync: purchasePackage } = usePurchasePackage(userId);
|
|
26
29
|
const { mutateAsync: restorePurchases } = useRestorePurchase(userId);
|
|
27
|
-
const
|
|
30
|
+
const { pendingPackage, setPendingPurchase, clearPendingPurchase } = usePendingPurchase();
|
|
28
31
|
|
|
29
32
|
const handlePurchase = useCallback(async (pkg: PurchasesPackage) => {
|
|
30
33
|
if (isAnonymous) {
|
|
31
|
-
if (__DEV__) console.log("[PaywallActions] Anonymous user, storing package:", pkg.identifier);
|
|
32
|
-
|
|
34
|
+
if (__DEV__) console.log("[PaywallActions] Anonymous user, storing package:", pkg.product.identifier, "source:", source);
|
|
35
|
+
setPendingPurchase(pkg, source);
|
|
33
36
|
onAuthRequired?.();
|
|
34
37
|
return;
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
try {
|
|
38
|
-
if (__DEV__) console.log("[PaywallActions] Purchase started:", pkg.identifier);
|
|
41
|
+
if (__DEV__) console.log("[PaywallActions] Purchase started:", pkg.product.identifier);
|
|
39
42
|
const res = await purchasePackage(pkg);
|
|
40
43
|
if (res.success) {
|
|
41
44
|
onPurchaseSuccess?.();
|
|
@@ -44,7 +47,7 @@ export const usePaywallActions = ({
|
|
|
44
47
|
} catch (err: any) {
|
|
45
48
|
onPurchaseError?.(err.message || String(err));
|
|
46
49
|
}
|
|
47
|
-
}, [isAnonymous, purchasePackage, onClose, onPurchaseSuccess, onPurchaseError, onAuthRequired]);
|
|
50
|
+
}, [isAnonymous, source, purchasePackage, onClose, onPurchaseSuccess, onPurchaseError, onAuthRequired, setPendingPurchase]);
|
|
48
51
|
|
|
49
52
|
const handleRestore = useCallback(async () => {
|
|
50
53
|
try {
|
|
@@ -59,5 +62,5 @@ export const usePaywallActions = ({
|
|
|
59
62
|
}
|
|
60
63
|
}, [restorePurchases, onClose, onPurchaseSuccess, onPurchaseError]);
|
|
61
64
|
|
|
62
|
-
return { handlePurchase, handleRestore, pendingPackage,
|
|
65
|
+
return { handlePurchase, handleRestore, pendingPackage, clearPendingPurchase, purchasePackage };
|
|
63
66
|
};
|
|
@@ -8,7 +8,8 @@ export * from "./useDeductCredit";
|
|
|
8
8
|
export * from "./useInitializeCredits";
|
|
9
9
|
export * from "./useDevTestCallbacks";
|
|
10
10
|
export * from "./useFeatureGate";
|
|
11
|
-
export * from "./
|
|
11
|
+
export * from "./usePendingPurchase";
|
|
12
|
+
export * from "./useCompletePendingPurchase";
|
|
12
13
|
export * from "./usePaywallVisibility";
|
|
13
14
|
export * from "./usePremium";
|
|
14
15
|
export * from "./usePremiumGate";
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Complete Pending Purchase Hook
|
|
3
|
+
* Centralized hook for completing pending purchases after authentication
|
|
4
|
+
* This is the SINGLE source of truth for post-auth purchase completion
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useCallback, useRef, useEffect } from "react";
|
|
8
|
+
import { usePurchasePackage } from "../../revenuecat/presentation/hooks/usePurchasePackage";
|
|
9
|
+
import { usePendingPurchase, pendingPurchaseControl } from "./usePendingPurchase";
|
|
10
|
+
import { usePaywallVisibility } from "./usePaywallVisibility";
|
|
11
|
+
|
|
12
|
+
declare const __DEV__: boolean;
|
|
13
|
+
|
|
14
|
+
export interface UseCompletePendingPurchaseProps {
|
|
15
|
+
userId: string | undefined;
|
|
16
|
+
isAnonymous: boolean;
|
|
17
|
+
onPurchaseSuccess?: () => void;
|
|
18
|
+
onPurchaseError?: (error: string) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface UseCompletePendingPurchaseResult {
|
|
22
|
+
completePendingPurchase: () => Promise<boolean>;
|
|
23
|
+
hasPendingPurchase: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function useCompletePendingPurchase({
|
|
27
|
+
userId,
|
|
28
|
+
isAnonymous,
|
|
29
|
+
onPurchaseSuccess,
|
|
30
|
+
onPurchaseError,
|
|
31
|
+
}: UseCompletePendingPurchaseProps): UseCompletePendingPurchaseResult {
|
|
32
|
+
const { mutateAsync: purchasePackage } = usePurchasePackage(userId);
|
|
33
|
+
const { clearPendingPurchase, hasPendingPurchase } = usePendingPurchase();
|
|
34
|
+
const { closePaywall } = usePaywallVisibility();
|
|
35
|
+
|
|
36
|
+
const wasAnonymousRef = useRef(isAnonymous);
|
|
37
|
+
const isProcessingRef = useRef(false);
|
|
38
|
+
|
|
39
|
+
const completePendingPurchase = useCallback(async (): Promise<boolean> => {
|
|
40
|
+
// Get current state directly to avoid stale closure
|
|
41
|
+
const currentState = pendingPurchaseControl.get();
|
|
42
|
+
|
|
43
|
+
if (!currentState.package) {
|
|
44
|
+
if (__DEV__) console.log("[CompletePendingPurchase] No pending package");
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!userId) {
|
|
49
|
+
if (__DEV__) console.log("[CompletePendingPurchase] No userId");
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (isProcessingRef.current) {
|
|
54
|
+
if (__DEV__) console.log("[CompletePendingPurchase] Already processing");
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
isProcessingRef.current = true;
|
|
59
|
+
const pkg = currentState.package;
|
|
60
|
+
const source = currentState.source;
|
|
61
|
+
|
|
62
|
+
if (__DEV__) {
|
|
63
|
+
console.log("[CompletePendingPurchase] Completing purchase:", {
|
|
64
|
+
identifier: pkg.product.identifier,
|
|
65
|
+
source,
|
|
66
|
+
userId,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Clear pending BEFORE purchase to prevent double processing
|
|
71
|
+
clearPendingPurchase();
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const result = await purchasePackage(pkg);
|
|
75
|
+
|
|
76
|
+
if (result.success) {
|
|
77
|
+
if (__DEV__) console.log("[CompletePendingPurchase] Purchase SUCCESS");
|
|
78
|
+
onPurchaseSuccess?.();
|
|
79
|
+
closePaywall();
|
|
80
|
+
return true;
|
|
81
|
+
} else {
|
|
82
|
+
if (__DEV__) console.log("[CompletePendingPurchase] Purchase FAILED");
|
|
83
|
+
onPurchaseError?.("Purchase failed");
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
} catch (err: any) {
|
|
87
|
+
if (__DEV__) console.error("[CompletePendingPurchase] Purchase ERROR:", err);
|
|
88
|
+
onPurchaseError?.(err.message || String(err));
|
|
89
|
+
return false;
|
|
90
|
+
} finally {
|
|
91
|
+
isProcessingRef.current = false;
|
|
92
|
+
}
|
|
93
|
+
}, [userId, purchasePackage, clearPendingPurchase, closePaywall, onPurchaseSuccess, onPurchaseError]);
|
|
94
|
+
|
|
95
|
+
// Auto-complete when user transitions from anonymous to authenticated
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
const wasAnonymous = wasAnonymousRef.current;
|
|
98
|
+
const isNowAuthenticated = userId && !isAnonymous;
|
|
99
|
+
wasAnonymousRef.current = isAnonymous;
|
|
100
|
+
|
|
101
|
+
if (__DEV__) {
|
|
102
|
+
console.log("[CompletePendingPurchase] Auth state check:", {
|
|
103
|
+
wasAnonymous,
|
|
104
|
+
isNowAuthenticated,
|
|
105
|
+
hasPending: pendingPurchaseControl.hasPending(),
|
|
106
|
+
pendingId: pendingPurchaseControl.get().package?.product.identifier,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Only trigger if user just authenticated AND there's a pending purchase
|
|
111
|
+
if (wasAnonymous && isNowAuthenticated && pendingPurchaseControl.hasPending()) {
|
|
112
|
+
if (__DEV__) {
|
|
113
|
+
console.log("[CompletePendingPurchase] Auth completed, auto-completing purchase");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Small delay to ensure all state is settled
|
|
117
|
+
setTimeout(() => {
|
|
118
|
+
completePendingPurchase();
|
|
119
|
+
}, 300);
|
|
120
|
+
}
|
|
121
|
+
}, [userId, isAnonymous, completePendingPurchase]);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
completePendingPurchase,
|
|
125
|
+
hasPendingPurchase,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pending Purchase Hook
|
|
3
|
+
* Centralized global state for pending package purchase
|
|
4
|
+
* Used by both post-onboarding paywall and in-app paywall
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useCallback, useSyncExternalStore } from "react";
|
|
8
|
+
import type { PurchasesPackage } from "react-native-purchases";
|
|
9
|
+
|
|
10
|
+
type Listener = () => void;
|
|
11
|
+
|
|
12
|
+
interface PendingPurchaseState {
|
|
13
|
+
package: PurchasesPackage | null;
|
|
14
|
+
source: "postOnboarding" | "inApp" | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let state: PendingPurchaseState = {
|
|
18
|
+
package: null,
|
|
19
|
+
source: null,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const listeners = new Set<Listener>();
|
|
23
|
+
|
|
24
|
+
const subscribe = (listener: Listener): (() => void) => {
|
|
25
|
+
listeners.add(listener);
|
|
26
|
+
return () => listeners.delete(listener);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const getSnapshot = (): PendingPurchaseState => state;
|
|
30
|
+
|
|
31
|
+
const setState = (newState: Partial<PendingPurchaseState>): void => {
|
|
32
|
+
state = { ...state, ...newState };
|
|
33
|
+
listeners.forEach((listener) => listener());
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Direct pending purchase control for non-React contexts
|
|
38
|
+
*/
|
|
39
|
+
export const pendingPurchaseControl = {
|
|
40
|
+
set: (pkg: PurchasesPackage, source: "postOnboarding" | "inApp") => {
|
|
41
|
+
if (__DEV__) {
|
|
42
|
+
console.log("[PendingPurchase] Setting pending package:", {
|
|
43
|
+
identifier: pkg.product.identifier,
|
|
44
|
+
source,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
setState({ package: pkg, source });
|
|
48
|
+
},
|
|
49
|
+
clear: () => {
|
|
50
|
+
if (__DEV__) {
|
|
51
|
+
console.log("[PendingPurchase] Clearing pending package");
|
|
52
|
+
}
|
|
53
|
+
setState({ package: null, source: null });
|
|
54
|
+
},
|
|
55
|
+
get: () => state,
|
|
56
|
+
hasPending: () => state.package !== null,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export interface UsePendingPurchaseResult {
|
|
60
|
+
pendingPackage: PurchasesPackage | null;
|
|
61
|
+
pendingSource: "postOnboarding" | "inApp" | null;
|
|
62
|
+
setPendingPurchase: (pkg: PurchasesPackage, source: "postOnboarding" | "inApp") => void;
|
|
63
|
+
clearPendingPurchase: () => void;
|
|
64
|
+
hasPendingPurchase: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function usePendingPurchase(): UsePendingPurchaseResult {
|
|
68
|
+
const currentState = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
69
|
+
|
|
70
|
+
const setPendingPurchase = useCallback(
|
|
71
|
+
(pkg: PurchasesPackage, source: "postOnboarding" | "inApp") => {
|
|
72
|
+
pendingPurchaseControl.set(pkg, source);
|
|
73
|
+
},
|
|
74
|
+
[]
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const clearPendingPurchase = useCallback(() => {
|
|
78
|
+
pendingPurchaseControl.clear();
|
|
79
|
+
}, []);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
pendingPackage: currentState.package,
|
|
83
|
+
pendingSource: currentState.source,
|
|
84
|
+
setPendingPurchase,
|
|
85
|
+
clearPendingPurchase,
|
|
86
|
+
hasPendingPurchase: currentState.package !== null,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -1,29 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,139 +0,0 @@
|
|
|
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: _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
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
const success = await purchasePackage(pkg);
|
|
53
|
-
if (success) onPurchaseSuccess?.();
|
|
54
|
-
else showError();
|
|
55
|
-
return success;
|
|
56
|
-
},
|
|
57
|
-
[isAuthenticated, purchasePackage, onPurchaseSuccess, onAuthRequired, showError]
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
const handleRestore = useCallback(async (): Promise<boolean> => {
|
|
61
|
-
const success = await restorePurchase();
|
|
62
|
-
Alert.alert(
|
|
63
|
-
success ? t("premium.restoreSuccess") : t("premium.restoreError"),
|
|
64
|
-
success ? t("premium.restoreMessage") : t("premium.restoreErrorMessage")
|
|
65
|
-
);
|
|
66
|
-
if (success) onPurchaseSuccess?.();
|
|
67
|
-
return success;
|
|
68
|
-
}, [restorePurchase, onPurchaseSuccess, t]);
|
|
69
|
-
|
|
70
|
-
const handleInAppPurchase = useCallback(
|
|
71
|
-
async (pkg: PurchasesPackage): Promise<boolean> => {
|
|
72
|
-
if (!isAuthenticated()) {
|
|
73
|
-
setPendingPackage(pkg);
|
|
74
|
-
setPendingSource("inApp");
|
|
75
|
-
onAuthRequired?.();
|
|
76
|
-
return false;
|
|
77
|
-
}
|
|
78
|
-
const success = await purchasePackage(pkg);
|
|
79
|
-
if (success) closePaywall();
|
|
80
|
-
else showError();
|
|
81
|
-
return success;
|
|
82
|
-
},
|
|
83
|
-
[isAuthenticated, purchasePackage, closePaywall, onAuthRequired, showError]
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
const handleInAppRestore = useCallback(async (): Promise<boolean> => {
|
|
87
|
-
const success = await restorePurchase();
|
|
88
|
-
Alert.alert(
|
|
89
|
-
success ? t("premium.restoreSuccess") : t("premium.restoreError"),
|
|
90
|
-
success ? t("premium.restoreMessage") : t("premium.restoreErrorMessage")
|
|
91
|
-
);
|
|
92
|
-
if (success) closePaywall();
|
|
93
|
-
return success;
|
|
94
|
-
}, [restorePurchase, closePaywall, t]);
|
|
95
|
-
|
|
96
|
-
const completePendingPurchase = useCallback(async (): Promise<boolean> => {
|
|
97
|
-
if (!pendingPackage) {
|
|
98
|
-
if (__DEV__) console.log("[usePaywallOperations] No pending package");
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const pkg = pendingPackage;
|
|
103
|
-
const source = pendingSource;
|
|
104
|
-
setPendingPackage(null);
|
|
105
|
-
setPendingSource(null);
|
|
106
|
-
|
|
107
|
-
await new Promise((resolve) => setTimeout(resolve, RERENDER_DELAY_MS));
|
|
108
|
-
|
|
109
|
-
if (__DEV__) {
|
|
110
|
-
console.log("[usePaywallOperations] Completing:", pkg.identifier, source);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const success = await refs.purchasePackageRef.current(pkg);
|
|
114
|
-
|
|
115
|
-
if (success) {
|
|
116
|
-
source === "inApp"
|
|
117
|
-
? refs.closePaywallRef.current()
|
|
118
|
-
: refs.onPurchaseSuccessRef.current?.();
|
|
119
|
-
} else {
|
|
120
|
-
showError();
|
|
121
|
-
}
|
|
122
|
-
return success;
|
|
123
|
-
}, [pendingPackage, pendingSource, refs, showError]);
|
|
124
|
-
|
|
125
|
-
const clearPendingPackage = useCallback(() => {
|
|
126
|
-
setPendingPackage(null);
|
|
127
|
-
setPendingSource(null);
|
|
128
|
-
}, []);
|
|
129
|
-
|
|
130
|
-
return {
|
|
131
|
-
pendingPackage,
|
|
132
|
-
handlePurchase,
|
|
133
|
-
handleRestore,
|
|
134
|
-
handleInAppPurchase,
|
|
135
|
-
handleInAppRestore,
|
|
136
|
-
completePendingPurchase,
|
|
137
|
-
clearPendingPackage,
|
|
138
|
-
};
|
|
139
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
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
|
-
}
|