@umituz/react-native-subscription 2.13.17 → 2.13.19
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 +2 -0
- package/src/presentation/hooks/usePaywallOperations.ts +39 -0
- package/src/presentation/hooks/usePremium.ts +6 -6
- package/src/revenuecat/infrastructure/handlers/PackageHandler.ts +24 -6
- package/src/revenuecat/infrastructure/managers/SubscriptionManager.ts +9 -9
- package/src/revenuecat/presentation/hooks/usePurchasePackage.ts +50 -22
- package/src/revenuecat/presentation/hooks/useRestorePurchase.ts +57 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.13.
|
|
3
|
+
"version": "2.13.19",
|
|
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
|
@@ -391,6 +391,8 @@ export {
|
|
|
391
391
|
type PremiumStatus,
|
|
392
392
|
} from "./revenuecat/infrastructure/managers/SubscriptionManager";
|
|
393
393
|
|
|
394
|
+
export type { RestoreResultInfo } from "./revenuecat/infrastructure/handlers/PackageHandler";
|
|
395
|
+
|
|
394
396
|
// =============================================================================
|
|
395
397
|
// REVENUECAT - Hooks
|
|
396
398
|
// =============================================================================
|
|
@@ -36,6 +36,10 @@ export interface UsePaywallOperationsResult {
|
|
|
36
36
|
handleInAppPurchase: (pkg: PurchasesPackage) => Promise<boolean>;
|
|
37
37
|
/** Handle in-app restore (with auto-close logic) */
|
|
38
38
|
handleInAppRestore: () => Promise<boolean>;
|
|
39
|
+
/** Complete pending purchase after authentication */
|
|
40
|
+
completePendingPurchase: () => Promise<boolean>;
|
|
41
|
+
/** Clear pending package without purchasing */
|
|
42
|
+
clearPendingPackage: () => void;
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
export function usePaywallOperations({
|
|
@@ -155,11 +159,46 @@ export function usePaywallOperations({
|
|
|
155
159
|
[executeRestore, closePaywall]
|
|
156
160
|
);
|
|
157
161
|
|
|
162
|
+
const completePendingPurchase = useCallback(async (): Promise<boolean> => {
|
|
163
|
+
if (!pendingPackage) {
|
|
164
|
+
if (__DEV__) {
|
|
165
|
+
console.log("[usePaywallOperations] No pending package to complete");
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (__DEV__) {
|
|
171
|
+
console.log("[usePaywallOperations] Completing pending purchase:", pendingPackage.identifier);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const pkg = pendingPackage;
|
|
175
|
+
setPendingPackage(null);
|
|
176
|
+
|
|
177
|
+
const success = await purchasePackage(pkg);
|
|
178
|
+
|
|
179
|
+
if (success) {
|
|
180
|
+
if (onPurchaseSuccess) onPurchaseSuccess();
|
|
181
|
+
} else {
|
|
182
|
+
Alert.alert(
|
|
183
|
+
t("premium.purchaseError"),
|
|
184
|
+
t("premium.purchaseErrorMessage")
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return success;
|
|
189
|
+
}, [pendingPackage, purchasePackage, onPurchaseSuccess, t]);
|
|
190
|
+
|
|
191
|
+
const clearPendingPackage = useCallback(() => {
|
|
192
|
+
setPendingPackage(null);
|
|
193
|
+
}, []);
|
|
194
|
+
|
|
158
195
|
return {
|
|
159
196
|
pendingPackage,
|
|
160
197
|
handlePurchase,
|
|
161
198
|
handleRestore,
|
|
162
199
|
handleInAppPurchase,
|
|
163
200
|
handleInAppRestore,
|
|
201
|
+
completePendingPurchase,
|
|
202
|
+
clearPendingPackage,
|
|
164
203
|
};
|
|
165
204
|
}
|
|
@@ -89,11 +89,11 @@ export const usePremium = (userId?: string): UsePremiumResult => {
|
|
|
89
89
|
const handlePurchase = useCallback(
|
|
90
90
|
async (pkg: PurchasesPackage): Promise<boolean> => {
|
|
91
91
|
try {
|
|
92
|
-
const
|
|
93
|
-
return success;
|
|
92
|
+
const result = await purchaseMutation.mutateAsync(pkg);
|
|
93
|
+
return result.success;
|
|
94
94
|
} catch (error) {
|
|
95
95
|
if (__DEV__) {
|
|
96
|
-
console.error(
|
|
96
|
+
console.error("[usePremium] Purchase failed:", error);
|
|
97
97
|
}
|
|
98
98
|
return false;
|
|
99
99
|
}
|
|
@@ -104,11 +104,11 @@ export const usePremium = (userId?: string): UsePremiumResult => {
|
|
|
104
104
|
// Restore handler with proper error handling
|
|
105
105
|
const handleRestore = useCallback(async (): Promise<boolean> => {
|
|
106
106
|
try {
|
|
107
|
-
const
|
|
108
|
-
return success;
|
|
107
|
+
const result = await restoreMutation.mutateAsync();
|
|
108
|
+
return result.success;
|
|
109
109
|
} catch (error) {
|
|
110
110
|
if (__DEV__) {
|
|
111
|
-
console.error(
|
|
111
|
+
console.error("[usePremium] Restore failed:", error);
|
|
112
112
|
}
|
|
113
113
|
return false;
|
|
114
114
|
}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
7
|
-
import type { IRevenueCatService } from
|
|
8
|
-
import { getPremiumEntitlement } from
|
|
7
|
+
import type { IRevenueCatService } from "../../application/ports/IRevenueCatService";
|
|
8
|
+
import { getPremiumEntitlement } from "../../domain/types/RevenueCatTypes";
|
|
9
9
|
import {
|
|
10
10
|
trackPackageError,
|
|
11
11
|
addPackageBreadcrumb,
|
|
@@ -17,6 +17,11 @@ export interface PremiumStatus {
|
|
|
17
17
|
expirationDate: Date | null;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export interface RestoreResultInfo {
|
|
21
|
+
success: boolean;
|
|
22
|
+
productId: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
export class PackageHandler {
|
|
21
26
|
constructor(
|
|
22
27
|
private service: IRevenueCatService | null,
|
|
@@ -89,22 +94,35 @@ export class PackageHandler {
|
|
|
89
94
|
}
|
|
90
95
|
}
|
|
91
96
|
|
|
92
|
-
async restore(userId: string): Promise<
|
|
97
|
+
async restore(userId: string): Promise<RestoreResultInfo> {
|
|
93
98
|
if (!this.service?.isInitialized()) {
|
|
94
99
|
trackPackageWarning("subscription", "Restore attempted but not initialized", {});
|
|
95
|
-
return false;
|
|
100
|
+
return { success: false, productId: null };
|
|
96
101
|
}
|
|
97
102
|
|
|
98
103
|
try {
|
|
99
104
|
const result = await this.service.restorePurchases(userId);
|
|
100
|
-
|
|
105
|
+
|
|
106
|
+
// Extract product ID from active entitlement
|
|
107
|
+
let productId: string | null = null;
|
|
108
|
+
if (result.success && result.customerInfo) {
|
|
109
|
+
const entitlement = getPremiumEntitlement(
|
|
110
|
+
result.customerInfo,
|
|
111
|
+
this.entitlementId
|
|
112
|
+
);
|
|
113
|
+
if (entitlement) {
|
|
114
|
+
productId = entitlement.productIdentifier;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { success: result.success, productId };
|
|
101
119
|
} catch (error) {
|
|
102
120
|
trackPackageError(error instanceof Error ? error : new Error(String(error)), {
|
|
103
121
|
packageName: "subscription",
|
|
104
122
|
operation: "restore",
|
|
105
123
|
userId,
|
|
106
124
|
});
|
|
107
|
-
return false;
|
|
125
|
+
return { success: false, productId: null };
|
|
108
126
|
}
|
|
109
127
|
}
|
|
110
128
|
|
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
8
|
-
import type { RevenueCatConfig } from
|
|
9
|
-
import type { IRevenueCatService } from
|
|
10
|
-
import { initializeRevenueCatService, getRevenueCatService } from
|
|
11
|
-
import { UserIdProvider } from
|
|
12
|
-
import { InitializationCache } from
|
|
13
|
-
import { PackageHandler } from
|
|
14
|
-
import type { PremiumStatus } from
|
|
8
|
+
import type { RevenueCatConfig } from "../../domain/value-objects/RevenueCatConfig";
|
|
9
|
+
import type { IRevenueCatService } from "../../application/ports/IRevenueCatService";
|
|
10
|
+
import { initializeRevenueCatService, getRevenueCatService } from "../services/RevenueCatService";
|
|
11
|
+
import { UserIdProvider } from "../utils/UserIdProvider";
|
|
12
|
+
import { InitializationCache } from "../utils/InitializationCache";
|
|
13
|
+
import { PackageHandler } from "../handlers/PackageHandler";
|
|
14
|
+
import type { PremiumStatus, RestoreResultInfo } from "../handlers/PackageHandler";
|
|
15
15
|
import {
|
|
16
16
|
trackPackageError,
|
|
17
17
|
addPackageBreadcrumb,
|
|
@@ -138,10 +138,10 @@ class SubscriptionManagerImpl {
|
|
|
138
138
|
return this.packageHandler!.purchase(pkg, userId);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
async restore(): Promise<
|
|
141
|
+
async restore(): Promise<RestoreResultInfo> {
|
|
142
142
|
this.ensureConfigured();
|
|
143
143
|
const userId = this.initCache.getCurrentUserId();
|
|
144
|
-
if (!userId) return false;
|
|
144
|
+
if (!userId) return { success: false, productId: null };
|
|
145
145
|
return this.packageHandler!.restore(userId);
|
|
146
146
|
}
|
|
147
147
|
|
|
@@ -1,37 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Purchase Package Hook
|
|
3
3
|
* TanStack mutation for purchasing subscription packages
|
|
4
|
+
* Automatically initializes credits after successful purchase
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
7
8
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
8
|
-
import { SubscriptionManager } from
|
|
9
|
+
import { SubscriptionManager } from "../../infrastructure/managers/SubscriptionManager";
|
|
9
10
|
import {
|
|
10
11
|
trackPackageError,
|
|
11
12
|
addPackageBreadcrumb,
|
|
12
13
|
} from "@umituz/react-native-sentry";
|
|
13
14
|
import { SUBSCRIPTION_QUERY_KEYS } from "./subscriptionQueryKeys";
|
|
14
15
|
import { creditsQueryKeys } from "../../../presentation/hooks/useCredits";
|
|
16
|
+
import { getCreditsRepository } from "../../../infrastructure/repositories/CreditsRepositoryProvider";
|
|
17
|
+
|
|
18
|
+
interface PurchaseResult {
|
|
19
|
+
success: boolean;
|
|
20
|
+
productId: string;
|
|
21
|
+
}
|
|
15
22
|
|
|
16
23
|
/**
|
|
17
24
|
* Purchase a subscription package
|
|
25
|
+
* After successful purchase, automatically initializes credits
|
|
18
26
|
*/
|
|
19
27
|
export const usePurchasePackage = (userId: string | undefined) => {
|
|
20
28
|
const queryClient = useQueryClient();
|
|
21
29
|
|
|
22
30
|
return useMutation({
|
|
23
|
-
mutationFn: async (pkg: PurchasesPackage) => {
|
|
31
|
+
mutationFn: async (pkg: PurchasesPackage): Promise<PurchaseResult> => {
|
|
24
32
|
if (!userId) {
|
|
25
33
|
throw new Error("User not authenticated");
|
|
26
34
|
}
|
|
27
35
|
|
|
28
|
-
|
|
29
|
-
packageId: pkg.identifier,
|
|
30
|
-
userId,
|
|
31
|
-
});
|
|
36
|
+
const productId = pkg.product.identifier;
|
|
32
37
|
|
|
33
|
-
addPackageBreadcrumb("subscription", "Purchase
|
|
38
|
+
addPackageBreadcrumb("subscription", "Purchase started", {
|
|
34
39
|
packageId: pkg.identifier,
|
|
40
|
+
productId,
|
|
35
41
|
userId,
|
|
36
42
|
});
|
|
37
43
|
|
|
@@ -40,35 +46,57 @@ export const usePurchasePackage = (userId: string | undefined) => {
|
|
|
40
46
|
if (success) {
|
|
41
47
|
addPackageBreadcrumb("subscription", "Purchase success", {
|
|
42
48
|
packageId: pkg.identifier,
|
|
49
|
+
productId,
|
|
43
50
|
userId,
|
|
44
51
|
});
|
|
45
52
|
|
|
46
|
-
|
|
47
|
-
|
|
53
|
+
// Initialize credits immediately after purchase
|
|
54
|
+
const repository = getCreditsRepository();
|
|
55
|
+
const creditResult = await repository.initializeCredits(
|
|
48
56
|
userId,
|
|
49
|
-
|
|
57
|
+
pkg.identifier,
|
|
58
|
+
productId
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
if (creditResult.success) {
|
|
62
|
+
addPackageBreadcrumb("subscription", "Credits initialized", {
|
|
63
|
+
productId,
|
|
64
|
+
userId,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Update cache immediately for instant UI update
|
|
68
|
+
if (creditResult.data) {
|
|
69
|
+
queryClient.setQueryData(
|
|
70
|
+
creditsQueryKeys.user(userId),
|
|
71
|
+
creditResult.data
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
addPackageBreadcrumb("subscription", "Credits initialization failed", {
|
|
76
|
+
productId,
|
|
77
|
+
userId,
|
|
78
|
+
error: creditResult.error?.message,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
50
81
|
} else {
|
|
51
82
|
addPackageBreadcrumb("subscription", "Purchase cancelled", {
|
|
52
83
|
packageId: pkg.identifier,
|
|
53
84
|
userId,
|
|
54
85
|
});
|
|
55
|
-
|
|
56
|
-
addPackageBreadcrumb("subscription", "Purchase mutation failed", {
|
|
57
|
-
packageId: pkg.identifier,
|
|
58
|
-
userId,
|
|
59
|
-
});
|
|
60
86
|
}
|
|
61
87
|
|
|
62
|
-
return success;
|
|
88
|
+
return { success, productId };
|
|
63
89
|
},
|
|
64
|
-
onSuccess: () => {
|
|
65
|
-
|
|
66
|
-
queryKey: SUBSCRIPTION_QUERY_KEYS.packages,
|
|
67
|
-
});
|
|
68
|
-
if (userId) {
|
|
90
|
+
onSuccess: (result) => {
|
|
91
|
+
if (result.success) {
|
|
69
92
|
queryClient.invalidateQueries({
|
|
70
|
-
queryKey:
|
|
93
|
+
queryKey: SUBSCRIPTION_QUERY_KEYS.packages,
|
|
71
94
|
});
|
|
95
|
+
if (userId) {
|
|
96
|
+
queryClient.invalidateQueries({
|
|
97
|
+
queryKey: creditsQueryKeys.user(userId),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
72
100
|
}
|
|
73
101
|
},
|
|
74
102
|
onError: (error) => {
|
|
@@ -1,24 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Restore Purchase Hook
|
|
3
3
|
* TanStack mutation for restoring previous purchases
|
|
4
|
+
* Automatically initializes credits after successful restore
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
7
|
-
import { SubscriptionManager } from
|
|
8
|
+
import { SubscriptionManager } from "../../infrastructure/managers/SubscriptionManager";
|
|
8
9
|
import {
|
|
9
10
|
trackPackageError,
|
|
10
11
|
addPackageBreadcrumb,
|
|
11
12
|
} from "@umituz/react-native-sentry";
|
|
12
13
|
import { SUBSCRIPTION_QUERY_KEYS } from "./subscriptionQueryKeys";
|
|
14
|
+
import { creditsQueryKeys } from "../../../presentation/hooks/useCredits";
|
|
15
|
+
import { getCreditsRepository } from "../../../infrastructure/repositories/CreditsRepositoryProvider";
|
|
16
|
+
|
|
17
|
+
interface RestoreResult {
|
|
18
|
+
success: boolean;
|
|
19
|
+
productId: string | null;
|
|
20
|
+
}
|
|
13
21
|
|
|
14
22
|
/**
|
|
15
23
|
* Restore previous purchases
|
|
24
|
+
* After successful restore, automatically initializes credits
|
|
16
25
|
*/
|
|
17
26
|
export const useRestorePurchase = (userId: string | undefined) => {
|
|
18
27
|
const queryClient = useQueryClient();
|
|
19
28
|
|
|
20
29
|
return useMutation({
|
|
21
|
-
mutationFn: async () => {
|
|
30
|
+
mutationFn: async (): Promise<RestoreResult> => {
|
|
22
31
|
if (!userId) {
|
|
23
32
|
throw new Error("User not authenticated");
|
|
24
33
|
}
|
|
@@ -27,32 +36,63 @@ export const useRestorePurchase = (userId: string | undefined) => {
|
|
|
27
36
|
userId,
|
|
28
37
|
});
|
|
29
38
|
|
|
30
|
-
|
|
31
|
-
userId,
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const success = await SubscriptionManager.restore();
|
|
39
|
+
const result = await SubscriptionManager.restore();
|
|
35
40
|
|
|
36
|
-
if (success) {
|
|
41
|
+
if (result.success) {
|
|
37
42
|
addPackageBreadcrumb("subscription", "Restore success", {
|
|
38
43
|
userId,
|
|
44
|
+
productId: result.productId,
|
|
39
45
|
});
|
|
40
46
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
// Initialize credits if we have a product ID
|
|
48
|
+
if (result.productId) {
|
|
49
|
+
const repository = getCreditsRepository();
|
|
50
|
+
const creditResult = await repository.initializeCredits(
|
|
51
|
+
userId,
|
|
52
|
+
undefined,
|
|
53
|
+
result.productId
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
if (creditResult.success) {
|
|
57
|
+
addPackageBreadcrumb("subscription", "Credits initialized after restore", {
|
|
58
|
+
productId: result.productId,
|
|
59
|
+
userId,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Update cache immediately for instant UI update
|
|
63
|
+
if (creditResult.data) {
|
|
64
|
+
queryClient.setQueryData(
|
|
65
|
+
creditsQueryKeys.user(userId),
|
|
66
|
+
creditResult.data
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
addPackageBreadcrumb("subscription", "Credits initialization failed after restore", {
|
|
71
|
+
productId: result.productId,
|
|
72
|
+
userId,
|
|
73
|
+
error: creditResult.error?.message,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
44
77
|
} else {
|
|
45
|
-
addPackageBreadcrumb("subscription", "Restore
|
|
78
|
+
addPackageBreadcrumb("subscription", "Restore failed - no premium found", {
|
|
46
79
|
userId,
|
|
47
80
|
});
|
|
48
81
|
}
|
|
49
82
|
|
|
50
|
-
return
|
|
83
|
+
return result;
|
|
51
84
|
},
|
|
52
|
-
onSuccess: () => {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
85
|
+
onSuccess: (result) => {
|
|
86
|
+
if (result.success) {
|
|
87
|
+
queryClient.invalidateQueries({
|
|
88
|
+
queryKey: SUBSCRIPTION_QUERY_KEYS.packages,
|
|
89
|
+
});
|
|
90
|
+
if (userId) {
|
|
91
|
+
queryClient.invalidateQueries({
|
|
92
|
+
queryKey: creditsQueryKeys.user(userId),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
56
96
|
},
|
|
57
97
|
onError: (error) => {
|
|
58
98
|
trackPackageError(
|