@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.13.17",
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 success = await purchaseMutation.mutateAsync(pkg);
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('[usePremium] Purchase failed:', 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 success = await restoreMutation.mutateAsync();
108
- return success;
107
+ const result = await restoreMutation.mutateAsync();
108
+ return result.success;
109
109
  } catch (error) {
110
110
  if (__DEV__) {
111
- console.error('[usePremium] Restore failed:', 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 '../../application/ports/IRevenueCatService';
8
- import { getPremiumEntitlement } from '../../domain/types/RevenueCatTypes';
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<boolean> {
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
- return result.success;
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 '../../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 } from '../handlers/PackageHandler';
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<boolean> {
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 '../../infrastructure/managers/SubscriptionManager';
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
- addPackageBreadcrumb("subscription", "Purchase started", {
29
- packageId: pkg.identifier,
30
- userId,
31
- });
36
+ const productId = pkg.product.identifier;
32
37
 
33
- addPackageBreadcrumb("subscription", "Purchase mutation started", {
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
- addPackageBreadcrumb("subscription", "Purchase mutation success", {
47
- packageId: pkg.identifier,
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
- queryClient.invalidateQueries({
66
- queryKey: SUBSCRIPTION_QUERY_KEYS.packages,
67
- });
68
- if (userId) {
90
+ onSuccess: (result) => {
91
+ if (result.success) {
69
92
  queryClient.invalidateQueries({
70
- queryKey: creditsQueryKeys.user(userId),
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 '../../infrastructure/managers/SubscriptionManager';
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
- addPackageBreadcrumb("subscription", "Restore mutation started", {
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
- addPackageBreadcrumb("subscription", "Restore mutation success", {
42
- userId,
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 mutation failed", {
78
+ addPackageBreadcrumb("subscription", "Restore failed - no premium found", {
46
79
  userId,
47
80
  });
48
81
  }
49
82
 
50
- return success;
83
+ return result;
51
84
  },
52
- onSuccess: () => {
53
- queryClient.invalidateQueries({
54
- queryKey: SUBSCRIPTION_QUERY_KEYS.packages,
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(