@umituz/react-native-subscription 2.20.7 → 2.21.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 +1 -1
- package/src/domains/paywall/components/PaywallModal.tsx +50 -7
- package/src/index.ts +4 -0
- package/src/presentation/components/overlay/PurchaseLoadingOverlay.tsx +63 -0
- package/src/presentation/components/overlay/index.ts +5 -0
- package/src/presentation/hooks/useSavedPurchaseAutoExecution.ts +33 -7
- package/src/presentation/stores/index.ts +13 -0
- package/src/presentation/stores/purchaseLoadingStore.ts +73 -0
- package/src/revenuecat/infrastructure/services/PurchaseHandler.ts +16 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.21.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",
|
|
@@ -13,7 +13,7 @@ import { usePaywallActions } from "../hooks/usePaywallActions";
|
|
|
13
13
|
import type { PaywallContainerProps } from "./PaywallContainer.types";
|
|
14
14
|
|
|
15
15
|
export const PaywallContainer: React.FC<PaywallContainerProps> = (props) => {
|
|
16
|
-
const { userId,
|
|
16
|
+
const { userId, translations, mode = "subscription", legalUrls, features, heroImage, bestValueIdentifier, creditsLabel, creditAmounts, packageFilterConfig, source, onPurchaseSuccess, onPurchaseError, onAuthRequired, visible, onClose } = props;
|
|
17
17
|
|
|
18
18
|
const { showPaywall, closePaywall, currentSource } = usePaywallVisibility();
|
|
19
19
|
const isVisible = visible ?? showPaywall;
|
|
@@ -12,6 +12,9 @@ import type { SubscriptionFeature, PaywallTranslations, PaywallLegalUrls } from
|
|
|
12
12
|
import { paywallModalStyles as styles } from "./PaywallModal.styles";
|
|
13
13
|
import { PaywallFeatures } from "./PaywallFeatures";
|
|
14
14
|
import { PaywallFooter } from "./PaywallFooter";
|
|
15
|
+
import { usePurchaseLoadingStore, selectIsPurchasing } from "../../../presentation/stores";
|
|
16
|
+
|
|
17
|
+
declare const __DEV__: boolean;
|
|
15
18
|
|
|
16
19
|
export interface PaywallModalProps {
|
|
17
20
|
visible: boolean;
|
|
@@ -33,21 +36,61 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
|
|
|
33
36
|
const { visible, onClose, translations, packages = [], features = [], isLoading = false, legalUrls = {}, bestValueIdentifier, creditAmounts, creditsLabel, heroImage, onPurchase, onRestore } = props;
|
|
34
37
|
const tokens = useAppDesignTokens();
|
|
35
38
|
const [selectedPlanId, setSelectedPlanId] = useState<string | null>(null);
|
|
36
|
-
const [
|
|
39
|
+
const [isLocalProcessing, setIsLocalProcessing] = useState(false);
|
|
40
|
+
|
|
41
|
+
// Global purchase loading state (for auto-execution after auth)
|
|
42
|
+
const isGlobalPurchasing = usePurchaseLoadingStore(selectIsPurchasing);
|
|
43
|
+
const { startPurchase, endPurchase } = usePurchaseLoadingStore();
|
|
44
|
+
|
|
45
|
+
// Combined processing state
|
|
46
|
+
const isProcessing = isLocalProcessing || isGlobalPurchasing;
|
|
37
47
|
|
|
38
48
|
const handlePurchase = useCallback(async () => {
|
|
39
49
|
if (!selectedPlanId || !onPurchase) return;
|
|
40
|
-
|
|
50
|
+
|
|
51
|
+
if (__DEV__) {
|
|
52
|
+
console.log("[PaywallModal] handlePurchase starting:", { selectedPlanId });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
setIsLocalProcessing(true);
|
|
56
|
+
startPurchase(selectedPlanId, "manual");
|
|
57
|
+
|
|
41
58
|
try {
|
|
42
59
|
const pkg = packages.find((p) => p.product.identifier === selectedPlanId);
|
|
43
|
-
if (pkg)
|
|
44
|
-
|
|
45
|
-
|
|
60
|
+
if (pkg) {
|
|
61
|
+
if (__DEV__) {
|
|
62
|
+
console.log("[PaywallModal] Calling onPurchase:", { productId: pkg.product.identifier });
|
|
63
|
+
}
|
|
64
|
+
await onPurchase(pkg);
|
|
65
|
+
if (__DEV__) {
|
|
66
|
+
console.log("[PaywallModal] onPurchase completed");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} finally {
|
|
70
|
+
setIsLocalProcessing(false);
|
|
71
|
+
endPurchase();
|
|
72
|
+
if (__DEV__) {
|
|
73
|
+
console.log("[PaywallModal] handlePurchase finished");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}, [selectedPlanId, packages, onPurchase, startPurchase, endPurchase]);
|
|
46
77
|
|
|
47
78
|
const handleRestore = useCallback(async () => {
|
|
48
79
|
if (!onRestore || isProcessing) return;
|
|
49
|
-
|
|
50
|
-
|
|
80
|
+
|
|
81
|
+
if (__DEV__) {
|
|
82
|
+
console.log("[PaywallModal] handleRestore starting");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
setIsLocalProcessing(true);
|
|
86
|
+
try {
|
|
87
|
+
await onRestore();
|
|
88
|
+
if (__DEV__) {
|
|
89
|
+
console.log("[PaywallModal] handleRestore completed");
|
|
90
|
+
}
|
|
91
|
+
} finally {
|
|
92
|
+
setIsLocalProcessing(false);
|
|
93
|
+
}
|
|
51
94
|
}, [onRestore, isProcessing]);
|
|
52
95
|
|
|
53
96
|
const handleLegalUrl = useCallback(async (url: string | undefined) => {
|
package/src/index.ts
CHANGED
|
@@ -32,9 +32,13 @@ export * from "./presentation/components/details/PremiumDetailsCard";
|
|
|
32
32
|
export * from "./presentation/components/details/PremiumStatusBadge";
|
|
33
33
|
export * from "./presentation/components/sections/SubscriptionSection";
|
|
34
34
|
export * from "./presentation/components/feedback/PaywallFeedbackModal";
|
|
35
|
+
export * from "./presentation/components/overlay";
|
|
35
36
|
export * from "./presentation/screens/SubscriptionDetailScreen";
|
|
36
37
|
export * from "./presentation/types/SubscriptionDetailTypes";
|
|
37
38
|
|
|
39
|
+
// Presentation Layer - Stores
|
|
40
|
+
export * from "./presentation/stores";
|
|
41
|
+
|
|
38
42
|
// Credits Domain
|
|
39
43
|
export type {
|
|
40
44
|
CreditType,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purchase Loading Overlay
|
|
3
|
+
* Full-screen overlay shown during purchase operations
|
|
4
|
+
* Locks the UI and shows a spinner with optional message
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { View, Modal, StyleSheet } from "react-native";
|
|
9
|
+
import { AtomicSpinner, AtomicText, useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
10
|
+
import { usePurchaseLoadingStore, selectIsPurchasing } from "../../stores";
|
|
11
|
+
|
|
12
|
+
export interface PurchaseLoadingOverlayProps {
|
|
13
|
+
/** Loading message to display */
|
|
14
|
+
loadingText?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const PurchaseLoadingOverlay: React.FC<PurchaseLoadingOverlayProps> = React.memo(
|
|
18
|
+
({ loadingText }) => {
|
|
19
|
+
const tokens = useAppDesignTokens();
|
|
20
|
+
const isPurchasing = usePurchaseLoadingStore(selectIsPurchasing);
|
|
21
|
+
|
|
22
|
+
if (!isPurchasing) return null;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<Modal visible transparent animationType="fade" statusBarTranslucent>
|
|
26
|
+
<View style={[styles.container, { backgroundColor: "rgba(0, 0, 0, 0.7)" }]}>
|
|
27
|
+
<View style={[styles.content, { backgroundColor: tokens.colors.surface }]}>
|
|
28
|
+
<AtomicSpinner size="lg" color="primary" />
|
|
29
|
+
{loadingText && (
|
|
30
|
+
<AtomicText
|
|
31
|
+
type="bodyLarge"
|
|
32
|
+
style={[styles.text, { color: tokens.colors.textPrimary }]}
|
|
33
|
+
>
|
|
34
|
+
{loadingText}
|
|
35
|
+
</AtomicText>
|
|
36
|
+
)}
|
|
37
|
+
</View>
|
|
38
|
+
</View>
|
|
39
|
+
</Modal>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
PurchaseLoadingOverlay.displayName = "PurchaseLoadingOverlay";
|
|
45
|
+
|
|
46
|
+
const styles = StyleSheet.create({
|
|
47
|
+
container: {
|
|
48
|
+
flex: 1,
|
|
49
|
+
justifyContent: "center",
|
|
50
|
+
alignItems: "center",
|
|
51
|
+
},
|
|
52
|
+
content: {
|
|
53
|
+
paddingHorizontal: 32,
|
|
54
|
+
paddingVertical: 24,
|
|
55
|
+
borderRadius: 16,
|
|
56
|
+
alignItems: "center",
|
|
57
|
+
minWidth: 200,
|
|
58
|
+
},
|
|
59
|
+
text: {
|
|
60
|
+
marginTop: 16,
|
|
61
|
+
textAlign: "center",
|
|
62
|
+
},
|
|
63
|
+
});
|
|
@@ -7,6 +7,7 @@ import { useEffect, useRef, useCallback } from "react";
|
|
|
7
7
|
import { getSavedPurchase, clearSavedPurchase } from "./useAuthAwarePurchase";
|
|
8
8
|
import { usePremium } from "./usePremium";
|
|
9
9
|
import { SubscriptionManager } from "../../revenuecat";
|
|
10
|
+
import { usePurchaseLoadingStore } from "../stores";
|
|
10
11
|
|
|
11
12
|
declare const __DEV__: boolean;
|
|
12
13
|
|
|
@@ -26,6 +27,7 @@ export const useSavedPurchaseAutoExecution = (
|
|
|
26
27
|
): UseSavedPurchaseAutoExecutionResult => {
|
|
27
28
|
const { userId, isAnonymous, onSuccess, onError } = params;
|
|
28
29
|
const { purchasePackage } = usePremium(userId);
|
|
30
|
+
const { startPurchase, endPurchase } = usePurchaseLoadingStore();
|
|
29
31
|
|
|
30
32
|
const prevIsAnonymousRef = useRef<boolean | undefined>(undefined);
|
|
31
33
|
const isExecutingRef = useRef(false);
|
|
@@ -36,7 +38,8 @@ export const useSavedPurchaseAutoExecution = (
|
|
|
36
38
|
|
|
37
39
|
if (__DEV__) {
|
|
38
40
|
console.log(
|
|
39
|
-
"[SavedPurchaseAutoExecution] Waiting for RevenueCat initialization..."
|
|
41
|
+
"[SavedPurchaseAutoExecution] Waiting for RevenueCat initialization...",
|
|
42
|
+
{ userId: userId.slice(0, 8), productId: savedPurchase.pkg.product.identifier }
|
|
40
43
|
);
|
|
41
44
|
}
|
|
42
45
|
|
|
@@ -60,16 +63,26 @@ export const useSavedPurchaseAutoExecution = (
|
|
|
60
63
|
|
|
61
64
|
if (__DEV__) {
|
|
62
65
|
console.log(
|
|
63
|
-
"[SavedPurchaseAutoExecution] RevenueCat ready,
|
|
64
|
-
pkg.product.identifier
|
|
66
|
+
"[SavedPurchaseAutoExecution] RevenueCat ready, starting purchase...",
|
|
67
|
+
{ productId: pkg.product.identifier, userId: userId.slice(0, 8) }
|
|
65
68
|
);
|
|
66
69
|
}
|
|
67
70
|
|
|
71
|
+
// Start global loading state
|
|
72
|
+
startPurchase(pkg.product.identifier, "auto-execution");
|
|
73
|
+
|
|
68
74
|
try {
|
|
75
|
+
if (__DEV__) {
|
|
76
|
+
console.log("[SavedPurchaseAutoExecution] Calling purchasePackage...");
|
|
77
|
+
}
|
|
78
|
+
|
|
69
79
|
const success = await purchasePackage(pkg);
|
|
70
80
|
|
|
71
81
|
if (__DEV__) {
|
|
72
|
-
console.log("[SavedPurchaseAutoExecution] Purchase
|
|
82
|
+
console.log("[SavedPurchaseAutoExecution] Purchase completed:", {
|
|
83
|
+
success,
|
|
84
|
+
productId: pkg.product.identifier
|
|
85
|
+
});
|
|
73
86
|
}
|
|
74
87
|
|
|
75
88
|
if (success && onSuccess) {
|
|
@@ -77,13 +90,22 @@ export const useSavedPurchaseAutoExecution = (
|
|
|
77
90
|
}
|
|
78
91
|
} catch (error) {
|
|
79
92
|
if (__DEV__) {
|
|
80
|
-
console.error("[SavedPurchaseAutoExecution] Purchase
|
|
93
|
+
console.error("[SavedPurchaseAutoExecution] Purchase error:", {
|
|
94
|
+
error,
|
|
95
|
+
productId: pkg.product.identifier
|
|
96
|
+
});
|
|
81
97
|
}
|
|
82
98
|
if (onError && error instanceof Error) {
|
|
83
99
|
onError(error);
|
|
84
100
|
}
|
|
85
101
|
} finally {
|
|
102
|
+
// End global loading state
|
|
103
|
+
endPurchase();
|
|
86
104
|
isExecutingRef.current = false;
|
|
105
|
+
|
|
106
|
+
if (__DEV__) {
|
|
107
|
+
console.log("[SavedPurchaseAutoExecution] Purchase flow finished");
|
|
108
|
+
}
|
|
87
109
|
}
|
|
88
110
|
|
|
89
111
|
return;
|
|
@@ -99,7 +121,7 @@ export const useSavedPurchaseAutoExecution = (
|
|
|
99
121
|
}
|
|
100
122
|
clearSavedPurchase();
|
|
101
123
|
isExecutingRef.current = false;
|
|
102
|
-
}, [userId, purchasePackage, onSuccess, onError]);
|
|
124
|
+
}, [userId, purchasePackage, onSuccess, onError, startPurchase, endPurchase]);
|
|
103
125
|
|
|
104
126
|
useEffect(() => {
|
|
105
127
|
const isAuthenticated = !!userId && !isAnonymous;
|
|
@@ -110,7 +132,7 @@ export const useSavedPurchaseAutoExecution = (
|
|
|
110
132
|
const becameAuthenticated = wasAnonymous && isAuthenticated;
|
|
111
133
|
|
|
112
134
|
if (__DEV__) {
|
|
113
|
-
console.log("[SavedPurchaseAutoExecution]
|
|
135
|
+
console.log("[SavedPurchaseAutoExecution] Auth state check:", {
|
|
114
136
|
userId: userId?.slice(0, 8),
|
|
115
137
|
prevIsAnonymous,
|
|
116
138
|
isAnonymous,
|
|
@@ -118,11 +140,15 @@ export const useSavedPurchaseAutoExecution = (
|
|
|
118
140
|
wasAnonymous,
|
|
119
141
|
becameAuthenticated,
|
|
120
142
|
hasSavedPurchase: !!savedPurchase,
|
|
143
|
+
savedProductId: savedPurchase?.pkg.product.identifier,
|
|
121
144
|
willExecute: becameAuthenticated && !!savedPurchase && !isExecutingRef.current,
|
|
122
145
|
});
|
|
123
146
|
}
|
|
124
147
|
|
|
125
148
|
if (becameAuthenticated && savedPurchase && !isExecutingRef.current) {
|
|
149
|
+
if (__DEV__) {
|
|
150
|
+
console.log("[SavedPurchaseAutoExecution] Triggering auto-execution...");
|
|
151
|
+
}
|
|
126
152
|
executeWithWait();
|
|
127
153
|
}
|
|
128
154
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presentation Layer - Stores
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
usePurchaseLoadingStore,
|
|
7
|
+
selectIsPurchasing,
|
|
8
|
+
selectPurchasingProductId,
|
|
9
|
+
selectPurchaseSource,
|
|
10
|
+
type PurchaseLoadingState,
|
|
11
|
+
type PurchaseLoadingActions,
|
|
12
|
+
type PurchaseLoadingStore,
|
|
13
|
+
} from "./purchaseLoadingStore";
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purchase Loading Store
|
|
3
|
+
* Global state for tracking purchase loading across the app
|
|
4
|
+
* Used by both PaywallModal and useSavedPurchaseAutoExecution
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { create } from "zustand";
|
|
8
|
+
|
|
9
|
+
declare const __DEV__: boolean;
|
|
10
|
+
|
|
11
|
+
export interface PurchaseLoadingState {
|
|
12
|
+
/** Whether a purchase is in progress */
|
|
13
|
+
isPurchasing: boolean;
|
|
14
|
+
/** Product identifier being purchased */
|
|
15
|
+
purchasingProductId: string | null;
|
|
16
|
+
/** Source of the purchase (manual, auto-execution, etc.) */
|
|
17
|
+
purchaseSource: "manual" | "auto-execution" | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PurchaseLoadingActions {
|
|
21
|
+
/** Start purchase loading state */
|
|
22
|
+
startPurchase: (productId: string, source: "manual" | "auto-execution") => void;
|
|
23
|
+
/** End purchase loading state */
|
|
24
|
+
endPurchase: () => void;
|
|
25
|
+
/** Reset all state */
|
|
26
|
+
reset: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type PurchaseLoadingStore = PurchaseLoadingState & PurchaseLoadingActions;
|
|
30
|
+
|
|
31
|
+
const initialState: PurchaseLoadingState = {
|
|
32
|
+
isPurchasing: false,
|
|
33
|
+
purchasingProductId: null,
|
|
34
|
+
purchaseSource: null,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const usePurchaseLoadingStore = create<PurchaseLoadingStore>((set) => ({
|
|
38
|
+
...initialState,
|
|
39
|
+
|
|
40
|
+
startPurchase: (productId, source) => {
|
|
41
|
+
if (__DEV__) {
|
|
42
|
+
console.log("[PurchaseLoadingStore] startPurchase:", { productId, source });
|
|
43
|
+
}
|
|
44
|
+
set({
|
|
45
|
+
isPurchasing: true,
|
|
46
|
+
purchasingProductId: productId,
|
|
47
|
+
purchaseSource: source,
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
endPurchase: () => {
|
|
52
|
+
if (__DEV__) {
|
|
53
|
+
console.log("[PurchaseLoadingStore] endPurchase");
|
|
54
|
+
}
|
|
55
|
+
set({
|
|
56
|
+
isPurchasing: false,
|
|
57
|
+
purchasingProductId: null,
|
|
58
|
+
purchaseSource: null,
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
reset: () => {
|
|
63
|
+
if (__DEV__) {
|
|
64
|
+
console.log("[PurchaseLoadingStore] reset");
|
|
65
|
+
}
|
|
66
|
+
set(initialState);
|
|
67
|
+
},
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
// Selectors for optimized re-renders
|
|
71
|
+
export const selectIsPurchasing = (state: PurchaseLoadingStore) => state.isPurchasing;
|
|
72
|
+
export const selectPurchasingProductId = (state: PurchaseLoadingStore) => state.purchasingProductId;
|
|
73
|
+
export const selectPurchaseSource = (state: PurchaseLoadingStore) => state.purchaseSource;
|
|
@@ -65,11 +65,26 @@ export async function handlePurchase(
|
|
|
65
65
|
|
|
66
66
|
try {
|
|
67
67
|
if (__DEV__) {
|
|
68
|
-
console.log('[DEBUG PurchaseHandler] Calling Purchases.purchasePackage...'
|
|
68
|
+
console.log('[DEBUG PurchaseHandler] Calling Purchases.purchasePackage...', {
|
|
69
|
+
productId: pkg.product.identifier,
|
|
70
|
+
packageIdentifier: pkg.identifier,
|
|
71
|
+
offeringIdentifier: pkg.offeringIdentifier,
|
|
72
|
+
timestamp: new Date().toISOString()
|
|
73
|
+
});
|
|
69
74
|
}
|
|
75
|
+
|
|
76
|
+
const startTime = Date.now();
|
|
70
77
|
const purchaseResult = await Purchases.purchasePackage(pkg);
|
|
78
|
+
const duration = Date.now() - startTime;
|
|
71
79
|
const customerInfo = purchaseResult.customerInfo;
|
|
72
80
|
|
|
81
|
+
if (__DEV__) {
|
|
82
|
+
console.log('[DEBUG PurchaseHandler] Purchases.purchasePackage returned', {
|
|
83
|
+
duration: `${duration}ms`,
|
|
84
|
+
productId: pkg.product.identifier
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
73
88
|
if (__DEV__) {
|
|
74
89
|
console.log('[DEBUG PurchaseHandler] Purchase completed', {
|
|
75
90
|
productId: pkg.product.identifier,
|