@umituz/react-native-subscription 2.20.1 → 2.20.2
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/hooks/usePaywallActions.ts +1 -9
- package/src/index.ts +5 -7
- package/src/presentation/hooks/useAuthAwarePurchase.ts +87 -83
- package/src/revenuecat/infrastructure/services/PurchaseHandler.ts +7 -8
- package/src/infrastructure/stores/PendingPurchaseStore.ts +0 -63
- package/src/presentation/hooks/usePendingPurchaseHandler.ts +0 -79
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.20.
|
|
3
|
+
"version": "2.20.2",
|
|
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",
|
|
@@ -2,7 +2,6 @@ import { useCallback } from "react";
|
|
|
2
2
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
3
3
|
import { useRestorePurchase } from "../../../revenuecat/presentation/hooks/useRestorePurchase";
|
|
4
4
|
import { useAuthAwarePurchase } from "../../../presentation/hooks/useAuthAwarePurchase";
|
|
5
|
-
import { usePendingPurchaseStore } from "../../../infrastructure/stores/PendingPurchaseStore";
|
|
6
5
|
import type { PurchaseSource } from "../../../domain/entities/Credits";
|
|
7
6
|
|
|
8
7
|
declare const __DEV__: boolean;
|
|
@@ -26,15 +25,8 @@ export const usePaywallActions = ({
|
|
|
26
25
|
}: UsePaywallActionsProps) => {
|
|
27
26
|
const { handlePurchase: authAwarePurchase } = useAuthAwarePurchase({ source, userId });
|
|
28
27
|
const { mutateAsync: restorePurchases } = useRestorePurchase(userId);
|
|
29
|
-
const { isExecuting } = usePendingPurchaseStore();
|
|
30
28
|
|
|
31
29
|
const handlePurchase = useCallback(async (pkg: PurchasesPackage) => {
|
|
32
|
-
// Block if a pending purchase is already being executed
|
|
33
|
-
if (isExecuting) {
|
|
34
|
-
if (__DEV__) console.log("[PaywallActions] Purchase blocked - pending purchase in progress");
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
30
|
try {
|
|
39
31
|
if (__DEV__) console.log("[PaywallActions] Purchase started:", pkg.product.identifier);
|
|
40
32
|
const res = await authAwarePurchase(pkg, source);
|
|
@@ -46,7 +38,7 @@ export const usePaywallActions = ({
|
|
|
46
38
|
const message = err instanceof Error ? err.message : String(err);
|
|
47
39
|
onPurchaseError?.(message);
|
|
48
40
|
}
|
|
49
|
-
}, [authAwarePurchase, source, onClose, onPurchaseSuccess, onPurchaseError
|
|
41
|
+
}, [authAwarePurchase, source, onClose, onPurchaseSuccess, onPurchaseError]);
|
|
50
42
|
|
|
51
43
|
const handleRestore = useCallback(async () => {
|
|
52
44
|
try {
|
package/src/index.ts
CHANGED
|
@@ -18,13 +18,11 @@ export { initializeSubscription, type SubscriptionInitConfig, type CreditPackage
|
|
|
18
18
|
export { CreditsRepository, createCreditsRepository } from "./infrastructure/repositories/CreditsRepository";
|
|
19
19
|
export { configureCreditsRepository, getCreditsRepository, getCreditsConfig, resetCreditsRepository, isCreditsRepositoryConfigured } from "./infrastructure/repositories/CreditsRepositoryProvider";
|
|
20
20
|
export {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
type UsePendingPurchaseHandlerParams,
|
|
27
|
-
} from "./presentation/hooks/usePendingPurchaseHandler";
|
|
21
|
+
getSavedPurchase,
|
|
22
|
+
clearSavedPurchase,
|
|
23
|
+
configureAuthProvider,
|
|
24
|
+
type PurchaseAuthProvider,
|
|
25
|
+
} from "./presentation/hooks/useAuthAwarePurchase";
|
|
28
26
|
|
|
29
27
|
// Presentation Layer - Hooks
|
|
30
28
|
export * from "./presentation/hooks";
|
|
@@ -1,31 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auth-Aware Purchase Hook
|
|
3
|
-
*
|
|
4
|
-
* Configure once at app start with configureAuthProvider()
|
|
3
|
+
* Handles purchase flow with authentication requirement
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
6
|
import { useCallback } from "react";
|
|
8
7
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
9
8
|
import { usePremium } from "./usePremium";
|
|
10
|
-
import { usePendingPurchaseStore } from "../../infrastructure/stores/PendingPurchaseStore";
|
|
11
9
|
import type { PurchaseSource } from "../../domain/entities/Credits";
|
|
12
10
|
|
|
13
11
|
declare const __DEV__: boolean;
|
|
14
12
|
|
|
15
13
|
export interface PurchaseAuthProvider {
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
isAuthenticated: () => boolean;
|
|
15
|
+
showAuthModal: () => void;
|
|
18
16
|
}
|
|
19
17
|
|
|
20
|
-
// Global auth provider - configured once at app start
|
|
21
18
|
let globalAuthProvider: PurchaseAuthProvider | null = null;
|
|
19
|
+
let savedPackage: PurchasesPackage | null = null;
|
|
20
|
+
let savedSource: PurchaseSource | null = null;
|
|
22
21
|
|
|
23
|
-
/**
|
|
24
|
-
* Configure auth provider for purchases
|
|
25
|
-
* Call this once at app initialization
|
|
26
|
-
*/
|
|
27
22
|
export const configureAuthProvider = (provider: PurchaseAuthProvider): void => {
|
|
28
|
-
|
|
23
|
+
globalAuthProvider = provider;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const getSavedPurchase = (): { pkg: PurchasesPackage; source: PurchaseSource } | null => {
|
|
27
|
+
if (savedPackage && savedSource) {
|
|
28
|
+
return { pkg: savedPackage, source: savedSource };
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const clearSavedPurchase = (): void => {
|
|
34
|
+
savedPackage = null;
|
|
35
|
+
savedSource = null;
|
|
29
36
|
};
|
|
30
37
|
|
|
31
38
|
export interface UseAuthAwarePurchaseParams {
|
|
@@ -34,84 +41,81 @@ export interface UseAuthAwarePurchaseParams {
|
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
export interface UseAuthAwarePurchaseResult {
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
handlePurchase: (pkg: PurchasesPackage, source?: PurchaseSource) => Promise<boolean>;
|
|
45
|
+
handleRestore: () => Promise<boolean>;
|
|
46
|
+
executeSavedPurchase: () => Promise<boolean>;
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
export const useAuthAwarePurchase = (
|
|
42
50
|
params?: UseAuthAwarePurchaseParams
|
|
43
51
|
): UseAuthAwarePurchaseResult => {
|
|
44
|
-
|
|
45
|
-
const { setPendingPurchase } = usePendingPurchaseStore();
|
|
46
|
-
|
|
47
|
-
const handlePurchase = useCallback(
|
|
48
|
-
async (pkg: PurchasesPackage, source?: PurchaseSource): Promise<boolean> => {
|
|
49
|
-
// SECURITY: Block purchase if auth provider not configured
|
|
50
|
-
if (!globalAuthProvider) {
|
|
51
|
-
if (__DEV__) {
|
|
52
|
-
console.error(
|
|
53
|
-
"[useAuthAwarePurchase] CRITICAL: Auth provider not configured. " +
|
|
54
|
-
"Call configureAuthProvider() at app start. Purchase blocked for security.",
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Block purchase if user not authenticated (anonymous users cannot purchase)
|
|
61
|
-
if (!globalAuthProvider.isAuthenticated()) {
|
|
62
|
-
if (__DEV__) {
|
|
63
|
-
console.log(
|
|
64
|
-
"[useAuthAwarePurchase] User not authenticated, saving pending purchase and opening auth modal",
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Save pending purchase
|
|
69
|
-
setPendingPurchase({
|
|
70
|
-
package: pkg,
|
|
71
|
-
source: source || params?.source || "settings",
|
|
72
|
-
selectedAt: Date.now(),
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
closePaywall();
|
|
76
|
-
globalAuthProvider.showAuthModal();
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return purchasePackage(pkg);
|
|
81
|
-
},
|
|
82
|
-
[purchasePackage, closePaywall, setPendingPurchase, params?.source],
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
const handleRestore = useCallback(async (): Promise<boolean> => {
|
|
86
|
-
// SECURITY: Block restore if auth provider not configured
|
|
87
|
-
if (!globalAuthProvider) {
|
|
88
|
-
if (__DEV__) {
|
|
89
|
-
console.error(
|
|
90
|
-
"[useAuthAwarePurchase] CRITICAL: Auth provider not configured. " +
|
|
91
|
-
"Call configureAuthProvider() at app start. Restore blocked for security.",
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
// Block restore - never allow without auth provider
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
52
|
+
const { purchasePackage, restorePurchase, closePaywall } = usePremium(params?.userId);
|
|
97
53
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
closePaywall();
|
|
106
|
-
globalAuthProvider.showAuthModal();
|
|
107
|
-
return false;
|
|
54
|
+
const handlePurchase = useCallback(
|
|
55
|
+
async (pkg: PurchasesPackage, source?: PurchaseSource): Promise<boolean> => {
|
|
56
|
+
if (!globalAuthProvider) {
|
|
57
|
+
if (__DEV__) {
|
|
58
|
+
console.error("[useAuthAwarePurchase] Auth provider not configured");
|
|
108
59
|
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
109
62
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
63
|
+
if (!globalAuthProvider.isAuthenticated()) {
|
|
64
|
+
if (__DEV__) {
|
|
65
|
+
console.log("[useAuthAwarePurchase] Not authenticated, saving and showing auth");
|
|
66
|
+
}
|
|
67
|
+
savedPackage = pkg;
|
|
68
|
+
savedSource = source || params?.source || "settings";
|
|
69
|
+
closePaywall();
|
|
70
|
+
globalAuthProvider.showAuthModal();
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return purchasePackage(pkg);
|
|
75
|
+
},
|
|
76
|
+
[purchasePackage, closePaywall, params?.source]
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const handleRestore = useCallback(async (): Promise<boolean> => {
|
|
80
|
+
if (!globalAuthProvider) {
|
|
81
|
+
if (__DEV__) {
|
|
82
|
+
console.error("[useAuthAwarePurchase] Auth provider not configured");
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!globalAuthProvider.isAuthenticated()) {
|
|
88
|
+
if (__DEV__) {
|
|
89
|
+
console.log("[useAuthAwarePurchase] Not authenticated for restore");
|
|
90
|
+
}
|
|
91
|
+
closePaywall();
|
|
92
|
+
globalAuthProvider.showAuthModal();
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return restorePurchase();
|
|
97
|
+
}, [restorePurchase, closePaywall]);
|
|
98
|
+
|
|
99
|
+
const executeSavedPurchase = useCallback(async (): Promise<boolean> => {
|
|
100
|
+
const saved = getSavedPurchase();
|
|
101
|
+
if (!saved) {
|
|
102
|
+
if (__DEV__) {
|
|
103
|
+
console.log("[useAuthAwarePurchase] No saved purchase to execute");
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (__DEV__) {
|
|
109
|
+
console.log("[useAuthAwarePurchase] Executing saved purchase:", saved.pkg.product.identifier);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
clearSavedPurchase();
|
|
113
|
+
return purchasePackage(saved.pkg);
|
|
114
|
+
}, [purchasePackage]);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
handlePurchase,
|
|
118
|
+
handleRestore,
|
|
119
|
+
executeSavedPurchase,
|
|
120
|
+
};
|
|
117
121
|
};
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
syncPremiumStatus,
|
|
19
19
|
notifyPurchaseCompleted,
|
|
20
20
|
} from "../utils/PremiumStatusSyncer";
|
|
21
|
-
import {
|
|
21
|
+
import { getSavedPurchase, clearSavedPurchase } from "../../../presentation/hooks/useAuthAwarePurchase";
|
|
22
22
|
|
|
23
23
|
export interface PurchaseHandlerDeps {
|
|
24
24
|
config: RevenueCatConfig;
|
|
@@ -77,10 +77,9 @@ export async function handlePurchase(
|
|
|
77
77
|
});
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
// Get purchase source from
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
const source = pending?.source;
|
|
80
|
+
// Get purchase source from saved purchase
|
|
81
|
+
const savedPurchase = getSavedPurchase();
|
|
82
|
+
const source = savedPurchase?.source;
|
|
84
83
|
|
|
85
84
|
if (isConsumable) {
|
|
86
85
|
if (__DEV__) {
|
|
@@ -94,7 +93,7 @@ export async function handlePurchase(
|
|
|
94
93
|
source
|
|
95
94
|
);
|
|
96
95
|
// Clear pending purchase after successful purchase
|
|
97
|
-
|
|
96
|
+
clearSavedPurchase();
|
|
98
97
|
return {
|
|
99
98
|
success: true,
|
|
100
99
|
isPremium: false,
|
|
@@ -129,7 +128,7 @@ export async function handlePurchase(
|
|
|
129
128
|
source
|
|
130
129
|
);
|
|
131
130
|
// Clear pending purchase after successful purchase
|
|
132
|
-
|
|
131
|
+
clearSavedPurchase();
|
|
133
132
|
return { success: true, isPremium: true, customerInfo };
|
|
134
133
|
}
|
|
135
134
|
|
|
@@ -147,7 +146,7 @@ export async function handlePurchase(
|
|
|
147
146
|
source
|
|
148
147
|
);
|
|
149
148
|
// Clear pending purchase after successful purchase
|
|
150
|
-
|
|
149
|
+
clearSavedPurchase();
|
|
151
150
|
return { success: true, isPremium: false, customerInfo };
|
|
152
151
|
}
|
|
153
152
|
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pending Purchase Store
|
|
3
|
-
* Manages pending purchase state for auth-required purchases
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { createStore } from "@umituz/react-native-design-system";
|
|
7
|
-
import type { PurchasesPackage } from "react-native-purchases";
|
|
8
|
-
import type { PurchaseSource } from "../../domain/entities/Credits";
|
|
9
|
-
|
|
10
|
-
export interface PendingPurchaseData {
|
|
11
|
-
package: PurchasesPackage;
|
|
12
|
-
source: PurchaseSource;
|
|
13
|
-
selectedAt: number;
|
|
14
|
-
metadata?: Record<string, unknown>;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface PendingPurchaseState {
|
|
18
|
-
pending: PendingPurchaseData | null;
|
|
19
|
-
isExecuting: boolean;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
interface PendingPurchaseActions {
|
|
23
|
-
setPendingPurchase: (data: PendingPurchaseData) => void;
|
|
24
|
-
getPendingPurchase: () => PendingPurchaseData | null;
|
|
25
|
-
clearPendingPurchase: () => void;
|
|
26
|
-
hasPendingPurchase: () => boolean;
|
|
27
|
-
setExecuting: (executing: boolean) => void;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const initialState: PendingPurchaseState = {
|
|
31
|
-
pending: null,
|
|
32
|
-
isExecuting: false,
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export const usePendingPurchaseStore = createStore<
|
|
36
|
-
PendingPurchaseState,
|
|
37
|
-
PendingPurchaseActions
|
|
38
|
-
>({
|
|
39
|
-
name: "pending-purchase-store",
|
|
40
|
-
initialState,
|
|
41
|
-
persist: false,
|
|
42
|
-
actions: (set, get) => ({
|
|
43
|
-
setPendingPurchase: (data: PendingPurchaseData) => {
|
|
44
|
-
set({ pending: data });
|
|
45
|
-
},
|
|
46
|
-
|
|
47
|
-
getPendingPurchase: () => {
|
|
48
|
-
return get().pending;
|
|
49
|
-
},
|
|
50
|
-
|
|
51
|
-
clearPendingPurchase: () => {
|
|
52
|
-
set({ pending: null });
|
|
53
|
-
},
|
|
54
|
-
|
|
55
|
-
hasPendingPurchase: () => {
|
|
56
|
-
return get().pending !== null;
|
|
57
|
-
},
|
|
58
|
-
|
|
59
|
-
setExecuting: (executing: boolean) => {
|
|
60
|
-
set({ isExecuting: executing });
|
|
61
|
-
},
|
|
62
|
-
}),
|
|
63
|
-
});
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pending Purchase Handler Hook
|
|
3
|
-
* Automatically executes pending purchase after successful authentication
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { useEffect, useRef } from "react";
|
|
7
|
-
import { usePendingPurchaseStore } from "../../infrastructure/stores/PendingPurchaseStore";
|
|
8
|
-
import { usePremium } from "./usePremium";
|
|
9
|
-
|
|
10
|
-
declare const __DEV__: boolean;
|
|
11
|
-
|
|
12
|
-
export interface UsePendingPurchaseHandlerParams {
|
|
13
|
-
userId: string | undefined;
|
|
14
|
-
isAuthenticated: boolean;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Hook to handle pending purchases after authentication
|
|
19
|
-
* Call this in app root after auth initialization
|
|
20
|
-
*/
|
|
21
|
-
export const usePendingPurchaseHandler = ({
|
|
22
|
-
userId,
|
|
23
|
-
isAuthenticated,
|
|
24
|
-
}: UsePendingPurchaseHandlerParams): void => {
|
|
25
|
-
const { pending, clearPendingPurchase, setExecuting } = usePendingPurchaseStore();
|
|
26
|
-
const { purchasePackage } = usePremium(userId);
|
|
27
|
-
const isExecutingRef = useRef(false);
|
|
28
|
-
const executedPackageIdRef = useRef<string | null>(null);
|
|
29
|
-
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
if (!isAuthenticated || !userId || !pending) {
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Prevent duplicate executions
|
|
36
|
-
if (isExecutingRef.current) {
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Prevent re-executing the same package
|
|
41
|
-
if (executedPackageIdRef.current === pending.package.identifier) {
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const executePendingPurchase = async () => {
|
|
46
|
-
isExecutingRef.current = true;
|
|
47
|
-
executedPackageIdRef.current = pending.package.identifier;
|
|
48
|
-
setExecuting(true);
|
|
49
|
-
|
|
50
|
-
if (__DEV__) {
|
|
51
|
-
console.log(
|
|
52
|
-
"[usePendingPurchaseHandler] Executing pending purchase:",
|
|
53
|
-
{
|
|
54
|
-
packageId: pending.package.identifier,
|
|
55
|
-
source: pending.source,
|
|
56
|
-
selectedAt: new Date(pending.selectedAt).toISOString(),
|
|
57
|
-
}
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
await purchasePackage(pending.package);
|
|
63
|
-
} catch (error) {
|
|
64
|
-
if (__DEV__) {
|
|
65
|
-
console.error(
|
|
66
|
-
"[usePendingPurchaseHandler] Failed to execute pending purchase:",
|
|
67
|
-
error
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
} finally {
|
|
71
|
-
clearPendingPurchase();
|
|
72
|
-
isExecutingRef.current = false;
|
|
73
|
-
setExecuting(false);
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
void executePendingPurchase();
|
|
78
|
-
}, [isAuthenticated, userId, pending, clearPendingPurchase, purchasePackage, setExecuting]);
|
|
79
|
-
};
|