@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.12.41",
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",
@@ -48,7 +48,7 @@ export const useInitializeSubscription = (userId: string | undefined) => {
48
48
  {
49
49
  packageName: "subscription",
50
50
  operation: "initialize_mutation",
51
- userId: userId ?? "NO_USER",
51
+ userId: userId ?? "ANONYMOUS",
52
52
  }
53
53
  );
54
54
  },
@@ -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 ?? "NO_USER",
80
+ userId: userId ?? "ANONYMOUS",
75
81
  }
76
82
  );
77
83
  },
@@ -60,7 +60,7 @@ export const useRestorePurchase = (userId: string | undefined) => {
60
60
  {
61
61
  packageName: "subscription",
62
62
  operation: "restore_mutation",
63
- userId: userId ?? "NO_USER",
63
+ userId: userId ?? "ANONYMOUS",
64
64
  }
65
65
  );
66
66
  },
@@ -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
- if (userId) {
37
- if (!SubscriptionManager.isInitializedForUser(userId)) {
38
- if (__DEV__) {
39
- console.log('[DEBUG useSubscriptionPackages] Initializing for user:', userId);
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 (__DEV__) {
44
- console.log('[DEBUG useSubscriptionPackages] Already initialized for user:', userId);
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
- } else {
48
- if (!SubscriptionManager.isInitialized()) {
49
- if (__DEV__) {
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,