@umituz/react-native-subscription 2.14.45 → 2.14.48

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.
Files changed (33) hide show
  1. package/package.json +2 -4
  2. package/src/domains/paywall/components/PaywallHeader.tsx +1 -0
  3. package/src/domains/paywall/components/PaywallModal.styles.ts +49 -0
  4. package/src/domains/paywall/components/PaywallModal.tsx +2 -37
  5. package/src/domains/wallet/domain/entities/CreditCost.ts +3 -3
  6. package/src/domains/wallet/infrastructure/repositories/TransactionRepository.ts +11 -20
  7. package/src/domains/wallet/presentation/components/BalanceCard.tsx +4 -3
  8. package/src/domains/wallet/presentation/components/TransactionItem.tsx +4 -3
  9. package/src/domains/wallet/presentation/components/TransactionList.tsx +7 -5
  10. package/src/domains/wallet/presentation/hooks/useWallet.ts +6 -4
  11. package/src/domains/wallet/presentation/screens/WalletScreen.tsx +11 -8
  12. package/src/infrastructure/repositories/CreditsRepository.ts +7 -42
  13. package/src/infrastructure/services/CreditsInitializer.ts +1 -52
  14. package/src/infrastructure/services/SubscriptionService.ts +2 -0
  15. package/src/presentation/components/feedback/PaywallFeedbackModal.tsx +1 -0
  16. package/src/presentation/components/sections/SubscriptionSection.tsx +4 -3
  17. package/src/presentation/hooks/useCreditChecker.ts +3 -2
  18. package/src/presentation/hooks/useCredits.ts +4 -3
  19. package/src/presentation/hooks/usePremium.ts +4 -3
  20. package/src/presentation/hooks/useSubscriptionDetails.ts +3 -2
  21. package/src/presentation/hooks/useSubscriptionSettingsConfig.ts +4 -3
  22. package/src/presentation/screens/SubscriptionDetailScreen.tsx +3 -2
  23. package/src/presentation/screens/components/UpgradePrompt.tsx +4 -3
  24. package/src/revenuecat/infrastructure/services/CustomerInfoListenerManager.ts +60 -158
  25. package/src/revenuecat/infrastructure/services/OfferingsFetcher.ts +13 -29
  26. package/src/revenuecat/infrastructure/services/PurchaseHandler.ts +64 -88
  27. package/src/revenuecat/infrastructure/services/RestoreHandler.ts +32 -48
  28. package/src/revenuecat/infrastructure/services/RevenueCatInitializer.ts +90 -219
  29. package/src/revenuecat/infrastructure/services/RevenueCatService.ts +121 -126
  30. package/src/revenuecat/infrastructure/utils/InitializationCache.ts +25 -29
  31. package/src/revenuecat/infrastructure/utils/PremiumStatusSyncer.ts +52 -100
  32. package/src/revenuecat/infrastructure/utils/UserIdProvider.ts +17 -25
  33. package/src/revenuecat/presentation/hooks/usePaywallFlow.ts +9 -8
@@ -4,177 +4,79 @@
4
4
  */
5
5
 
6
6
  import Purchases, {
7
- type CustomerInfo,
8
- type CustomerInfoUpdateListener,
7
+ type CustomerInfo,
8
+ type CustomerInfoUpdateListener,
9
9
  } from "react-native-purchases";
10
- import type { RevenueCatConfig } from '../../domain/value-objects/RevenueCatConfig';
11
- import { syncPremiumStatus } from '../utils/PremiumStatusSyncer';
10
+ import type { RevenueCatConfig } from "../../domain/value-objects/RevenueCatConfig";
11
+ import { syncPremiumStatus } from "../utils/PremiumStatusSyncer";
12
12
 
13
13
  export class CustomerInfoListenerManager {
14
- private listener: CustomerInfoUpdateListener | null = null;
15
- private currentUserId: string | null = null;
16
- private entitlementIdentifier: string;
17
-
18
- constructor(entitlementIdentifier: string) {
19
- this.entitlementIdentifier = entitlementIdentifier;
20
- }
21
-
22
- setUserId(userId: string): void {
23
- this.currentUserId = userId;
24
- }
25
-
26
- clearUserId(): void {
27
- this.currentUserId = null;
28
- }
29
-
30
- setupListener(config: RevenueCatConfig): void {
31
- this.removeListener();
32
-
33
- if (__DEV__) {
34
- console.log("[CustomerInfoListener] Setting up listener", {
35
- userId: this.currentUserId,
36
- entitlementId: this.entitlementIdentifier,
37
- });
14
+ private listener: CustomerInfoUpdateListener | null = null;
15
+ private currentUserId: string | null = null;
16
+ private entitlementIdentifier: string;
17
+
18
+ constructor(entitlementIdentifier: string) {
19
+ this.entitlementIdentifier = entitlementIdentifier;
38
20
  }
39
21
 
40
- userId: this.currentUserId,
41
- });
22
+ setUserId(userId: string): void {
23
+ this.currentUserId = userId;
24
+ }
42
25
 
43
- this.listener = async (customerInfo: CustomerInfo) => {
44
- if (__DEV__) {
45
- console.log("[CustomerInfoListener] 🔔 Listener fired!", {
46
- userId: this.currentUserId,
47
- hasActiveEntitlements: Object.keys(customerInfo.entitlements.active).length > 0,
48
- });
49
- }
26
+ clearUserId(): void {
27
+ this.currentUserId = null;
28
+ }
50
29
 
51
- if (!this.currentUserId) {
52
- if (__DEV__) {
53
- console.warn("[CustomerInfoListener] ❌ No userId, skipping");
54
- }
55
- return;
56
- }
57
-
58
- const hasPremium =
59
- !!customerInfo.entitlements.active[this.entitlementIdentifier];
60
-
61
- if (__DEV__) {
62
- console.log("[CustomerInfoListener] Customer info updated", {
63
- userId: this.currentUserId,
64
- hasPremium,
65
- entitlementIdentifier: this.entitlementIdentifier,
66
- activeEntitlements: Object.keys(customerInfo.entitlements.active),
67
- });
68
- }
69
-
70
- userId: this.currentUserId,
71
- hasPremium,
72
- entitlementIdentifier: this.entitlementIdentifier,
73
- });
74
-
75
- // Handle credit renewal for subscription renewals
76
- if (hasPremium && config.onCreditRenewal) {
77
- const premiumEntitlement =
78
- customerInfo.entitlements.active[this.entitlementIdentifier];
79
-
80
- if (premiumEntitlement && premiumEntitlement.expirationDate) {
81
- const productId = premiumEntitlement.productIdentifier;
82
- const renewalId = `renewal_${productId}_${premiumEntitlement.expirationDate}`;
83
-
84
- if (__DEV__) {
85
- console.log("[CustomerInfoListener] 💰 Processing credit renewal", {
86
- userId: this.currentUserId,
87
- productId,
88
- renewalId,
89
- expirationDate: premiumEntitlement.expirationDate,
90
- });
91
- }
92
-
93
- "subscription",
94
- "Processing credit renewal",
95
- {
96
- userId: this.currentUserId,
97
- productId,
98
- renewalId,
99
- }
100
- );
101
-
102
- try {
103
- await config.onCreditRenewal(
104
- this.currentUserId,
105
- productId,
106
- renewalId
107
- );
108
-
109
- if (__DEV__) {
110
- console.log("[CustomerInfoListener] ✅ Credit renewal completed", {
111
- userId: this.currentUserId,
112
- productId,
113
- });
114
- }
30
+ setupListener(config: RevenueCatConfig): void {
31
+ this.removeListener();
115
32
 
116
- "subscription",
117
- "Credit renewal completed",
118
- {
119
- userId: this.currentUserId,
120
- productId,
121
- }
122
- );
123
-
124
- // Notify app to invalidate credits cache
125
- if (config.onCreditsUpdated && this.currentUserId) {
126
- config.onCreditsUpdated(this.currentUserId);
127
- }
128
- } catch (error) {
129
- if (__DEV__) {
130
- console.error("[CustomerInfoListener] ❌ Credit renewal failed", {
131
- userId: this.currentUserId,
132
- productId,
133
- error: error instanceof Error ? error.message : String(error),
134
- });
33
+ this.listener = async (customerInfo: CustomerInfo) => {
34
+ if (!this.currentUserId) {
35
+ return;
135
36
  }
136
37
 
137
- "subscription",
138
- "Credit renewal failed",
139
- {
140
- userId: this.currentUserId,
141
- productId,
142
- error: error instanceof Error ? error.message : String(error),
143
- }
144
- );
145
- }
146
- } else {
147
- if (__DEV__) {
148
- console.warn("[CustomerInfoListener] ⚠️ Premium but no entitlement/expiration", {
149
- hasPremiumEntitlement: !!premiumEntitlement,
150
- hasExpirationDate: !!(premiumEntitlement?.expirationDate),
151
- });
152
- }
153
- }
154
- } else {
155
- if (__DEV__) {
156
- console.log("[CustomerInfoListener] ℹ️ Skipping credit renewal", {
157
- hasPremium,
158
- hasCallback: !!config.onCreditRenewal,
159
- });
160
- }
161
- }
38
+ const hasPremium =
39
+ !!customerInfo.entitlements.active[this.entitlementIdentifier];
40
+
41
+ if (hasPremium && config.onCreditRenewal) {
42
+ const premiumEntitlement =
43
+ customerInfo.entitlements.active[this.entitlementIdentifier];
44
+
45
+ if (premiumEntitlement && premiumEntitlement.expirationDate) {
46
+ const productId = premiumEntitlement.productIdentifier;
47
+ const renewalId = `renewal_${productId}_${premiumEntitlement.expirationDate}`;
48
+
49
+ try {
50
+ await config.onCreditRenewal(
51
+ this.currentUserId,
52
+ productId,
53
+ renewalId
54
+ );
55
+
56
+ if (config.onCreditsUpdated && this.currentUserId) {
57
+ config.onCreditsUpdated(this.currentUserId);
58
+ }
59
+ } catch {
60
+ // Silent error handling
61
+ }
62
+ }
63
+ }
162
64
 
163
- syncPremiumStatus(config, this.currentUserId, customerInfo);
164
- };
65
+ syncPremiumStatus(config, this.currentUserId, customerInfo);
66
+ };
165
67
 
166
- Purchases.addCustomerInfoUpdateListener(this.listener);
167
- }
68
+ Purchases.addCustomerInfoUpdateListener(this.listener);
69
+ }
168
70
 
169
- removeListener(): void {
170
- if (this.listener) {
171
- Purchases.removeCustomerInfoUpdateListener(this.listener);
172
- this.listener = null;
71
+ removeListener(): void {
72
+ if (this.listener) {
73
+ Purchases.removeCustomerInfoUpdateListener(this.listener);
74
+ this.listener = null;
75
+ }
173
76
  }
174
- }
175
77
 
176
- destroy(): void {
177
- this.removeListener();
178
- this.clearUserId();
179
- }
78
+ destroy(): void {
79
+ this.removeListener();
80
+ this.clearUserId();
81
+ }
180
82
  }
@@ -6,37 +6,21 @@
6
6
  import Purchases, { type PurchasesOffering } from "react-native-purchases";
7
7
 
8
8
  export interface OfferingsFetcherDeps {
9
- isInitialized: () => boolean;
10
- isUsingTestStore: () => boolean;
9
+ isInitialized: () => boolean;
10
+ isUsingTestStore: () => boolean;
11
11
  }
12
12
 
13
13
  export async function fetchOfferings(
14
- deps: OfferingsFetcherDeps
14
+ deps: OfferingsFetcherDeps
15
15
  ): Promise<PurchasesOffering | null> {
16
- isInitialized: deps.isInitialized(),
17
- });
18
-
19
- if (!deps.isInitialized()) {
20
- return null;
21
- }
22
-
23
- try {
24
- const offerings = await Purchases.getOfferings();
25
-
26
- const packagesCount = offerings.current?.availablePackages?.length ?? 0;
27
-
28
- hasCurrent: !!offerings.current,
29
- packagesCount,
30
- });
31
-
32
- return offerings.current;
33
- } catch (error) {
34
- error instanceof Error ? error : new Error(String(error)),
35
- {
36
- packageName: "subscription",
37
- operation: "fetch_offerings",
38
- }
39
- );
40
- return null;
41
- }
16
+ if (!deps.isInitialized()) {
17
+ return null;
18
+ }
19
+
20
+ try {
21
+ const offerings = await Purchases.getOfferings();
22
+ return offerings.current;
23
+ } catch {
24
+ return null;
25
+ }
42
26
  }
@@ -4,112 +4,88 @@
4
4
  */
5
5
 
6
6
  import Purchases, { type PurchasesPackage } from "react-native-purchases";
7
- import type { PurchaseResult } from '../../application/ports/IRevenueCatService';
8
- RevenueCatPurchaseError,
9
- RevenueCatInitializationError,
10
- } from '../../domain/errors/RevenueCatError';
11
- import type { RevenueCatConfig } from '../../domain/value-objects/RevenueCatConfig';
12
- isUserCancelledError,
13
- getErrorMessage,
14
- } from '../../domain/types/RevenueCatTypes';
15
- syncPremiumStatus,
16
- notifyPurchaseCompleted,
17
- } from '../utils/PremiumStatusSyncer';
7
+ import type { PurchaseResult } from "../../application/ports/IRevenueCatService";
8
+ import {
9
+ RevenueCatPurchaseError,
10
+ RevenueCatInitializationError,
11
+ } from "../../domain/errors/RevenueCatError";
12
+ import type { RevenueCatConfig } from "../../domain/value-objects/RevenueCatConfig";
13
+ import {
14
+ isUserCancelledError,
15
+ getErrorMessage,
16
+ } from "../../domain/types/RevenueCatTypes";
17
+ import {
18
+ syncPremiumStatus,
19
+ notifyPurchaseCompleted,
20
+ } from "../utils/PremiumStatusSyncer";
18
21
 
19
22
  export interface PurchaseHandlerDeps {
20
- config: RevenueCatConfig;
21
- isInitialized: () => boolean;
22
- isUsingTestStore: () => boolean;
23
+ config: RevenueCatConfig;
24
+ isInitialized: () => boolean;
25
+ isUsingTestStore: () => boolean;
23
26
  }
24
27
 
25
28
  function isConsumableProduct(
26
- pkg: PurchasesPackage,
27
- consumableIds: string[]
29
+ pkg: PurchasesPackage,
30
+ consumableIds: string[]
28
31
  ): boolean {
29
- if (consumableIds.length === 0) return false;
30
- const identifier = pkg.product.identifier.toLowerCase();
31
- return consumableIds.some((id) => identifier.includes(id.toLowerCase()));
32
+ if (consumableIds.length === 0) return false;
33
+ const identifier = pkg.product.identifier.toLowerCase();
34
+ return consumableIds.some((id) => identifier.includes(id.toLowerCase()));
32
35
  }
33
36
 
34
37
  /**
35
38
  * Handle package purchase - supports both subscriptions and consumables
36
39
  */
37
40
  export async function handlePurchase(
38
- deps: PurchaseHandlerDeps,
39
- pkg: PurchasesPackage,
40
- userId: string
41
+ deps: PurchaseHandlerDeps,
42
+ pkg: PurchasesPackage,
43
+ userId: string
41
44
  ): Promise<PurchaseResult> {
42
- productId: pkg.product.identifier,
43
- userId,
44
- });
45
-
46
- if (!deps.isInitialized()) {
47
- const error = new RevenueCatInitializationError();
48
- packageName: "subscription",
49
- operation: "purchase",
50
- userId,
51
- productId: pkg.product.identifier,
52
- });
53
- throw error;
54
- }
45
+ if (!deps.isInitialized()) {
46
+ throw new RevenueCatInitializationError();
47
+ }
55
48
 
56
- const consumableIds = deps.config.consumableProductIdentifiers || [];
57
- const isConsumable = isConsumableProduct(pkg, consumableIds);
49
+ const consumableIds = deps.config.consumableProductIdentifiers || [];
50
+ const isConsumable = isConsumableProduct(pkg, consumableIds);
58
51
 
59
- try {
60
- const purchaseResult = await Purchases.purchasePackage(pkg);
61
- const customerInfo = purchaseResult.customerInfo;
52
+ try {
53
+ const purchaseResult = await Purchases.purchasePackage(pkg);
54
+ const customerInfo = purchaseResult.customerInfo;
62
55
 
63
- if (isConsumable) {
64
- return {
65
- success: true,
66
- isPremium: false,
67
- customerInfo,
68
- isConsumable: true,
69
- productId: pkg.product.identifier,
70
- };
71
- }
56
+ if (isConsumable) {
57
+ return {
58
+ success: true,
59
+ isPremium: false,
60
+ customerInfo,
61
+ isConsumable: true,
62
+ productId: pkg.product.identifier,
63
+ };
64
+ }
72
65
 
73
- const entitlementIdentifier = deps.config.entitlementIdentifier;
74
- const isPremium = !!customerInfo.entitlements.active[entitlementIdentifier];
66
+ const entitlementIdentifier = deps.config.entitlementIdentifier;
67
+ const isPremium = !!customerInfo.entitlements.active[entitlementIdentifier];
75
68
 
76
- if (isPremium) {
77
- await syncPremiumStatus(deps.config, userId, customerInfo);
78
- await notifyPurchaseCompleted(
79
- deps.config,
80
- userId,
81
- pkg.product.identifier,
82
- customerInfo
83
- );
84
- return { success: true, isPremium: true, customerInfo };
85
- }
69
+ if (isPremium) {
70
+ await syncPremiumStatus(deps.config, userId, customerInfo);
71
+ await notifyPurchaseCompleted(
72
+ deps.config,
73
+ userId,
74
+ pkg.product.identifier,
75
+ customerInfo
76
+ );
77
+ return { success: true, isPremium: true, customerInfo };
78
+ }
86
79
 
87
- const entitlementError = new RevenueCatPurchaseError(
88
- "Purchase completed but premium entitlement not active",
89
- pkg.product.identifier
90
- );
91
- packageName: "subscription",
92
- operation: "purchase",
93
- userId,
94
- productId: pkg.product.identifier,
95
- reason: "entitlement_not_active",
96
- });
97
- throw entitlementError;
98
- } catch (error) {
99
- if (isUserCancelledError(error)) {
100
- productId: pkg.product.identifier,
101
- userId,
102
- });
103
- return { success: false, isPremium: false };
80
+ throw new RevenueCatPurchaseError(
81
+ "Purchase completed but premium entitlement not active",
82
+ pkg.product.identifier
83
+ );
84
+ } catch (error) {
85
+ if (isUserCancelledError(error)) {
86
+ return { success: false, isPremium: false };
87
+ }
88
+ const errorMessage = getErrorMessage(error, "Purchase failed");
89
+ throw new RevenueCatPurchaseError(errorMessage, pkg.product.identifier);
104
90
  }
105
- const errorMessage = getErrorMessage(error, "Purchase failed");
106
- const purchaseError = new RevenueCatPurchaseError(errorMessage, pkg.product.identifier);
107
- packageName: "subscription",
108
- operation: "purchase",
109
- userId,
110
- productId: pkg.product.identifier,
111
- originalError: error instanceof Error ? error.message : String(error),
112
- });
113
- throw purchaseError;
114
- }
115
91
  }
@@ -4,65 +4,49 @@
4
4
  */
5
5
 
6
6
  import Purchases from "react-native-purchases";
7
- import type { RestoreResult } from '../../application/ports/IRevenueCatService';
8
- RevenueCatRestoreError,
9
- RevenueCatInitializationError,
10
- } from '../../domain/errors/RevenueCatError';
11
- import type { RevenueCatConfig } from '../../domain/value-objects/RevenueCatConfig';
12
- import { getErrorMessage } from '../../domain/types/RevenueCatTypes';
13
- syncPremiumStatus,
14
- notifyRestoreCompleted,
15
- } from '../utils/PremiumStatusSyncer';
7
+ import type { RestoreResult } from "../../application/ports/IRevenueCatService";
8
+ import {
9
+ RevenueCatRestoreError,
10
+ RevenueCatInitializationError,
11
+ } from "../../domain/errors/RevenueCatError";
12
+ import type { RevenueCatConfig } from "../../domain/value-objects/RevenueCatConfig";
13
+ import { getErrorMessage } from "../../domain/types/RevenueCatTypes";
14
+ import {
15
+ syncPremiumStatus,
16
+ notifyRestoreCompleted,
17
+ } from "../utils/PremiumStatusSyncer";
16
18
 
17
19
  export interface RestoreHandlerDeps {
18
- config: RevenueCatConfig;
19
- isInitialized: () => boolean;
20
- isUsingTestStore: () => boolean;
20
+ config: RevenueCatConfig;
21
+ isInitialized: () => boolean;
22
+ isUsingTestStore: () => boolean;
21
23
  }
22
24
 
23
25
  /**
24
26
  * Handle restore purchases
25
27
  */
26
28
  export async function handleRestore(
27
- deps: RestoreHandlerDeps,
28
- userId: string
29
+ deps: RestoreHandlerDeps,
30
+ userId: string
29
31
  ): Promise<RestoreResult> {
32
+ if (!deps.isInitialized()) {
33
+ throw new RevenueCatInitializationError();
34
+ }
30
35
 
31
- if (!deps.isInitialized()) {
32
- const error = new RevenueCatInitializationError();
33
- packageName: "subscription",
34
- operation: "restore",
35
- userId,
36
- });
37
- throw error;
38
- }
39
-
40
- try {
41
- const customerInfo = await Purchases.restorePurchases();
42
- const entitlementIdentifier = deps.config.entitlementIdentifier;
43
- const isPremium = !!customerInfo.entitlements.active[entitlementIdentifier];
36
+ try {
37
+ const customerInfo = await Purchases.restorePurchases();
38
+ const entitlementIdentifier = deps.config.entitlementIdentifier;
39
+ const isPremium = !!customerInfo.entitlements.active[entitlementIdentifier];
44
40
 
45
- if (isPremium) {
46
- await syncPremiumStatus(deps.config, userId, customerInfo);
47
- userId,
48
- entitlementId: entitlementIdentifier,
49
- });
50
- } else {
51
- userId,
52
- });
53
- }
41
+ if (isPremium) {
42
+ await syncPremiumStatus(deps.config, userId, customerInfo);
43
+ }
54
44
 
55
- await notifyRestoreCompleted(deps.config, userId, isPremium, customerInfo);
45
+ await notifyRestoreCompleted(deps.config, userId, isPremium, customerInfo);
56
46
 
57
- return { success: isPremium, isPremium, customerInfo };
58
- } catch (error) {
59
- const errorMessage = getErrorMessage(error, "Restore failed");
60
- const restoreError = new RevenueCatRestoreError(errorMessage);
61
- packageName: "subscription",
62
- operation: "restore",
63
- userId,
64
- originalError: error instanceof Error ? error.message : String(error),
65
- });
66
- throw restoreError;
67
- }
47
+ return { success: isPremium, isPremium, customerInfo };
48
+ } catch (error) {
49
+ const errorMessage = getErrorMessage(error, "Restore failed");
50
+ throw new RevenueCatRestoreError(errorMessage);
51
+ }
68
52
  }