@umituz/react-native-subscription 2.12.41 → 2.12.43
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/index.ts +6 -0
- package/src/presentation/hooks/usePaywallOperations.ts +165 -0
- package/src/revenuecat/infrastructure/handlers/PackageHandler.ts +17 -1
- package/src/revenuecat/presentation/hooks/useInitializeSubscription.ts +1 -1
- package/src/revenuecat/presentation/hooks/usePurchasePackage.ts +7 -1
- package/src/revenuecat/presentation/hooks/useRestorePurchase.ts +1 -1
- package/src/revenuecat/presentation/hooks/useSubscriptionPackages.ts +24 -17
- package/src/utils/premiumStatusUtils.ts +0 -15
- /package/src/utils/{dateValidationUtils.test.ts → __tests__/dateValidationUtils.test.ts} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.12.
|
|
3
|
+
"version": "2.12.43",
|
|
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",
|
package/src/index.ts
CHANGED
|
@@ -243,6 +243,12 @@ export {
|
|
|
243
243
|
type UsePremiumResult,
|
|
244
244
|
} from "./presentation/hooks/usePremium";
|
|
245
245
|
|
|
246
|
+
export {
|
|
247
|
+
usePaywallOperations,
|
|
248
|
+
type UsePaywallOperationsProps,
|
|
249
|
+
type UsePaywallOperationsResult,
|
|
250
|
+
} from "./presentation/hooks/usePaywallOperations";
|
|
251
|
+
|
|
246
252
|
export {
|
|
247
253
|
useAuthSubscriptionSync,
|
|
248
254
|
type AuthSubscriptionSyncConfig,
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paywall Logic Hook
|
|
3
|
+
*
|
|
4
|
+
* Generic business logic for handling paywall interactions.
|
|
5
|
+
* Decouples logic from UI and specific App implementations.
|
|
6
|
+
* Follows "Package Driven Design" by accepting dynamic props.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useState, useCallback } 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
|
+
}
|
|
40
|
+
|
|
41
|
+
export function usePaywallOperations({
|
|
42
|
+
userId,
|
|
43
|
+
isAnonymous,
|
|
44
|
+
onPaywallClose,
|
|
45
|
+
onPurchaseSuccess,
|
|
46
|
+
onAuthRequired,
|
|
47
|
+
}: UsePaywallOperationsProps): UsePaywallOperationsResult {
|
|
48
|
+
const { t } = useLocalization();
|
|
49
|
+
const { purchasePackage, restorePurchase, closePaywall } = usePremium(userId);
|
|
50
|
+
const [pendingPackage, setPendingPackage] = useState<PurchasesPackage | null>(null);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if action requires authentication
|
|
54
|
+
* @returns true if authenticated, false if auth required
|
|
55
|
+
*/
|
|
56
|
+
const checkAuth = useCallback((): boolean => {
|
|
57
|
+
if (!userId || isAnonymous) {
|
|
58
|
+
if (__DEV__) {
|
|
59
|
+
console.log("[usePaywallOperations] User not authenticated, triggering onAuthRequired");
|
|
60
|
+
}
|
|
61
|
+
if (onAuthRequired) {
|
|
62
|
+
onAuthRequired();
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
}, [userId, isAnonymous, onAuthRequired]);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Execute purchase flow with Alerts
|
|
71
|
+
*/
|
|
72
|
+
const executePurchase = useCallback(
|
|
73
|
+
async (pkg: PurchasesPackage, onSuccess?: () => void): Promise<boolean> => {
|
|
74
|
+
// 1. Auth Check
|
|
75
|
+
if (!checkAuth()) {
|
|
76
|
+
setPendingPackage(pkg);
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 2. Purchase
|
|
81
|
+
const success = await purchasePackage(pkg);
|
|
82
|
+
|
|
83
|
+
// 3. Handle Result
|
|
84
|
+
if (success) {
|
|
85
|
+
if (onSuccess) onSuccess();
|
|
86
|
+
} else {
|
|
87
|
+
Alert.alert(
|
|
88
|
+
t("premium.purchaseError"),
|
|
89
|
+
t("premium.purchaseErrorMessage")
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return success;
|
|
93
|
+
},
|
|
94
|
+
[checkAuth, purchasePackage, t]
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Execute restore flow with Alerts
|
|
99
|
+
*/
|
|
100
|
+
const executeRestore = useCallback(
|
|
101
|
+
async (onSuccess?: () => void): Promise<boolean> => {
|
|
102
|
+
// 1. Restore
|
|
103
|
+
const success = await restorePurchase();
|
|
104
|
+
|
|
105
|
+
// 2. Alert Result
|
|
106
|
+
Alert.alert(
|
|
107
|
+
success ? t("premium.restoreSuccess") : t("premium.restoreError"),
|
|
108
|
+
success
|
|
109
|
+
? t("premium.restoreMessage")
|
|
110
|
+
: t("premium.restoreErrorMessage")
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// 3. Handle Success
|
|
114
|
+
if (success) {
|
|
115
|
+
if (onSuccess) onSuccess();
|
|
116
|
+
}
|
|
117
|
+
return success;
|
|
118
|
+
},
|
|
119
|
+
[restorePurchase, t]
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// ============================================================================
|
|
123
|
+
// Public Handlers
|
|
124
|
+
// ============================================================================
|
|
125
|
+
|
|
126
|
+
const handlePurchase = useCallback(
|
|
127
|
+
async (pkg: PurchasesPackage): Promise<boolean> => {
|
|
128
|
+
const result = await executePurchase(pkg, onPurchaseSuccess);
|
|
129
|
+
if (!result && !checkAuth()) {
|
|
130
|
+
if (onPaywallClose) onPaywallClose();
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
},
|
|
134
|
+
[executePurchase, onPurchaseSuccess, checkAuth, onPaywallClose]
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const handleRestore = useCallback(
|
|
138
|
+
async (): Promise<boolean> => executeRestore(onPurchaseSuccess),
|
|
139
|
+
[executeRestore, onPurchaseSuccess]
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const handleInAppPurchase = useCallback(
|
|
143
|
+
async (pkg: PurchasesPackage): Promise<boolean> => {
|
|
144
|
+
const result = await executePurchase(pkg, closePaywall);
|
|
145
|
+
if (!result && !checkAuth()) {
|
|
146
|
+
closePaywall();
|
|
147
|
+
}
|
|
148
|
+
return result;
|
|
149
|
+
},
|
|
150
|
+
[executePurchase, closePaywall, checkAuth]
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const handleInAppRestore = useCallback(
|
|
154
|
+
async (): Promise<boolean> => executeRestore(closePaywall),
|
|
155
|
+
[executeRestore, closePaywall]
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
pendingPackage,
|
|
160
|
+
handlePurchase,
|
|
161
|
+
handleRestore,
|
|
162
|
+
handleInAppPurchase,
|
|
163
|
+
handleInAppRestore,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -21,7 +21,7 @@ export class PackageHandler {
|
|
|
21
21
|
constructor(
|
|
22
22
|
private service: IRevenueCatService | null,
|
|
23
23
|
private entitlementId: string
|
|
24
|
-
) {}
|
|
24
|
+
) { }
|
|
25
25
|
|
|
26
26
|
setService(service: IRevenueCatService | null): void {
|
|
27
27
|
this.service = service;
|
|
@@ -36,6 +36,19 @@ export class PackageHandler {
|
|
|
36
36
|
try {
|
|
37
37
|
const offering = await this.service.fetchOfferings();
|
|
38
38
|
|
|
39
|
+
if (__DEV__) {
|
|
40
|
+
console.log('[DEBUG PackageHandler] fetchOfferings result:', {
|
|
41
|
+
hasOffering: !!offering,
|
|
42
|
+
identifier: offering?.identifier,
|
|
43
|
+
packagesCount: offering?.availablePackages?.length ?? 0,
|
|
44
|
+
packages: offering?.availablePackages?.map(p => ({
|
|
45
|
+
identifier: p.identifier,
|
|
46
|
+
productIdentifier: p.product.identifier,
|
|
47
|
+
offeringIdentifier: p.offeringIdentifier
|
|
48
|
+
}))
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
39
52
|
addPackageBreadcrumb("subscription", "Packages fetched", {
|
|
40
53
|
identifier: offering?.identifier,
|
|
41
54
|
count: offering?.availablePackages?.length ?? 0,
|
|
@@ -43,6 +56,9 @@ export class PackageHandler {
|
|
|
43
56
|
|
|
44
57
|
return offering?.availablePackages ?? [];
|
|
45
58
|
} catch (error) {
|
|
59
|
+
if (__DEV__) {
|
|
60
|
+
console.error('[DEBUG PackageHandler] fetchOfferings failed:', error);
|
|
61
|
+
}
|
|
46
62
|
trackPackageError(error instanceof Error ? error : new Error(String(error)), {
|
|
47
63
|
packageName: "subscription",
|
|
48
64
|
operation: "fetch_packages",
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
addPackageBreadcrumb,
|
|
12
12
|
} from "@umituz/react-native-sentry";
|
|
13
13
|
import { SUBSCRIPTION_QUERY_KEYS } from "./subscriptionQueryKeys";
|
|
14
|
+
import { creditsQueryKeys } from "../../../presentation/hooks/useCredits";
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Purchase a subscription package
|
|
@@ -64,6 +65,11 @@ export const usePurchasePackage = (userId: string | undefined) => {
|
|
|
64
65
|
queryClient.invalidateQueries({
|
|
65
66
|
queryKey: SUBSCRIPTION_QUERY_KEYS.packages,
|
|
66
67
|
});
|
|
68
|
+
if (userId) {
|
|
69
|
+
queryClient.invalidateQueries({
|
|
70
|
+
queryKey: creditsQueryKeys.user(userId),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
67
73
|
},
|
|
68
74
|
onError: (error) => {
|
|
69
75
|
trackPackageError(
|
|
@@ -71,7 +77,7 @@ export const usePurchasePackage = (userId: string | undefined) => {
|
|
|
71
77
|
{
|
|
72
78
|
packageName: "subscription",
|
|
73
79
|
operation: "purchase_mutation",
|
|
74
|
-
userId: userId ?? "
|
|
80
|
+
userId: userId ?? "ANONYMOUS",
|
|
75
81
|
}
|
|
76
82
|
);
|
|
77
83
|
},
|
|
@@ -33,28 +33,35 @@ export const useSubscriptionPackages = (userId: string | undefined) => {
|
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
// Initialize if needed (works for both authenticated and anonymous users)
|
|
36
|
-
|
|
37
|
-
if (
|
|
38
|
-
if (
|
|
39
|
-
|
|
36
|
+
try {
|
|
37
|
+
if (userId) {
|
|
38
|
+
if (!SubscriptionManager.isInitializedForUser(userId)) {
|
|
39
|
+
if (__DEV__) {
|
|
40
|
+
console.log('[DEBUG useSubscriptionPackages] Initializing for user:', userId);
|
|
41
|
+
}
|
|
42
|
+
await SubscriptionManager.initialize(userId);
|
|
43
|
+
} else {
|
|
44
|
+
if (__DEV__) {
|
|
45
|
+
console.log('[DEBUG useSubscriptionPackages] Already initialized for user:', userId);
|
|
46
|
+
}
|
|
40
47
|
}
|
|
41
|
-
await SubscriptionManager.initialize(userId);
|
|
42
48
|
} else {
|
|
43
|
-
if (
|
|
44
|
-
|
|
49
|
+
if (!SubscriptionManager.isInitialized()) {
|
|
50
|
+
if (__DEV__) {
|
|
51
|
+
console.log('[DEBUG useSubscriptionPackages] Initializing for ANONYMOUS user');
|
|
52
|
+
}
|
|
53
|
+
await SubscriptionManager.initialize(undefined);
|
|
54
|
+
} else {
|
|
55
|
+
if (__DEV__) {
|
|
56
|
+
console.log('[DEBUG useSubscriptionPackages] Already initialized for ANONYMOUS');
|
|
57
|
+
}
|
|
45
58
|
}
|
|
46
59
|
}
|
|
47
|
-
}
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
console.log('[DEBUG useSubscriptionPackages] Initializing for ANONYMOUS user');
|
|
51
|
-
}
|
|
52
|
-
await SubscriptionManager.initialize(undefined);
|
|
53
|
-
} else {
|
|
54
|
-
if (__DEV__) {
|
|
55
|
-
console.log('[DEBUG useSubscriptionPackages] Already initialized for ANONYMOUS');
|
|
56
|
-
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (__DEV__) {
|
|
62
|
+
console.error('[DEBUG useSubscriptionPackages] Initialization failed:', error);
|
|
57
63
|
}
|
|
64
|
+
throw error;
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
if (__DEV__) {
|
|
@@ -26,21 +26,6 @@ import type { PremiumStatusFetcher } from './types';
|
|
|
26
26
|
* @param isPremiumOrFetcher - Either boolean (sync) or PremiumStatusFetcher (async)
|
|
27
27
|
* @returns boolean (sync) or Promise<boolean> (async) - Whether user has premium subscription
|
|
28
28
|
*/
|
|
29
|
-
// Sync overload: when isPremium value is already known
|
|
30
|
-
export function getIsPremium(
|
|
31
|
-
isGuestFlag: boolean,
|
|
32
|
-
userId: string | null,
|
|
33
|
-
isPremium: boolean,
|
|
34
|
-
): boolean;
|
|
35
|
-
|
|
36
|
-
// Async overload: when fetcher is provided
|
|
37
|
-
export function getIsPremium(
|
|
38
|
-
isGuestFlag: boolean,
|
|
39
|
-
userId: string | null,
|
|
40
|
-
fetcher: PremiumStatusFetcher,
|
|
41
|
-
): Promise<boolean>;
|
|
42
|
-
|
|
43
|
-
// Implementation
|
|
44
29
|
export function getIsPremium(
|
|
45
30
|
isGuestFlag: boolean,
|
|
46
31
|
userId: string | null,
|
|
File without changes
|