@umituz/react-native-subscription 2.27.142 → 2.27.144

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 (19) hide show
  1. package/package.json +1 -1
  2. package/src/domains/subscription/application/SubscriptionSyncProcessor.ts +24 -57
  3. package/src/domains/subscription/application/statusChangeHandlers.ts +54 -0
  4. package/src/domains/subscription/application/syncConstants.ts +11 -0
  5. package/src/domains/subscription/application/syncEventEmitter.ts +5 -0
  6. package/src/domains/subscription/application/syncIdGenerators.ts +19 -0
  7. package/src/domains/subscription/core/SubscriptionDisplayConfig.ts +0 -6
  8. package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +24 -70
  9. package/src/domains/subscription/infrastructure/managers/initializationHandler.ts +14 -0
  10. package/src/domains/subscription/infrastructure/managers/managerConstants.ts +3 -0
  11. package/src/domains/subscription/infrastructure/managers/managerOperations.ts +38 -0
  12. package/src/domains/subscription/infrastructure/managers/packageHandlerFactory.ts +17 -0
  13. package/src/domains/subscription/infrastructure/managers/premiumStatusChecker.ts +17 -0
  14. package/src/domains/subscription/infrastructure/services/RevenueCatInitializer.ts +9 -35
  15. package/src/domains/subscription/infrastructure/services/RevenueCatInitializer.types.ts +9 -0
  16. package/src/domains/subscription/infrastructure/services/RevenueCatService.ts +2 -134
  17. package/src/domains/subscription/infrastructure/services/RevenueCatService.types.ts +87 -0
  18. package/src/domains/subscription/infrastructure/services/initializerConstants.ts +9 -0
  19. package/src/domains/subscription/infrastructure/services/revenueCatServiceInstance.ts +17 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.27.142",
3
+ "version": "2.27.144",
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",
@@ -1,23 +1,19 @@
1
1
  import type { CustomerInfo } from "react-native-purchases";
2
- import type { RevenueCatData } from "../core/RevenueCatData";
3
- import {
4
- type PeriodType,
5
- type PurchaseSource,
6
- PURCHASE_SOURCE,
7
- PURCHASE_TYPE
8
- } from "../core/SubscriptionConstants";
2
+ import type { PeriodType, PurchaseSource } from "../core/SubscriptionConstants";
3
+ import { PURCHASE_SOURCE, PURCHASE_TYPE } from "../core/SubscriptionConstants";
9
4
  import { getCreditsRepository } from "../../credits/infrastructure/CreditsRepositoryManager";
10
5
  import { extractRevenueCatData } from "./SubscriptionSyncUtils";
11
- import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
6
+ import { emitCreditsUpdated } from "./syncEventEmitter";
7
+ import { generatePurchaseId, generateRenewalId } from "./syncIdGenerators";
8
+ import { handleExpiredSubscription, handleFreeUserInitialization, handlePremiumStatusSync } from "./statusChangeHandlers";
9
+ import { NO_SUBSCRIPTION_PRODUCT_ID } from "./syncConstants";
12
10
 
13
11
  export class SubscriptionSyncProcessor {
14
12
  constructor(private entitlementId: string) {}
15
13
 
16
14
  async processPurchase(userId: string, productId: string, customerInfo: CustomerInfo, source?: PurchaseSource) {
17
15
  const revenueCatData = extractRevenueCatData(customerInfo, this.entitlementId);
18
- const purchaseId = revenueCatData.originalTransactionId
19
- ? `purchase_${revenueCatData.originalTransactionId}`
20
- : `purchase_${productId}_${Date.now()}`;
16
+ const purchaseId = generatePurchaseId(revenueCatData.originalTransactionId, productId);
21
17
 
22
18
  await getCreditsRepository().initializeCredits(
23
19
  userId,
@@ -28,15 +24,13 @@ export class SubscriptionSyncProcessor {
28
24
  PURCHASE_TYPE.INITIAL
29
25
  );
30
26
 
31
- subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
27
+ emitCreditsUpdated(userId);
32
28
  }
33
29
 
34
30
  async processRenewal(userId: string, productId: string, newExpirationDate: string, customerInfo: CustomerInfo) {
35
31
  const revenueCatData = extractRevenueCatData(customerInfo, this.entitlementId);
36
32
  revenueCatData.expirationDate = newExpirationDate || revenueCatData.expirationDate;
37
- const purchaseId = revenueCatData.originalTransactionId
38
- ? `renewal_${revenueCatData.originalTransactionId}_${newExpirationDate}`
39
- : `renewal_${productId}_${Date.now()}`;
33
+ const purchaseId = generateRenewalId(revenueCatData.originalTransactionId, productId, newExpirationDate);
40
34
 
41
35
  await getCreditsRepository().initializeCredits(
42
36
  userId,
@@ -47,19 +41,19 @@ export class SubscriptionSyncProcessor {
47
41
  PURCHASE_TYPE.RENEWAL
48
42
  );
49
43
 
50
- subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
44
+ emitCreditsUpdated(userId);
51
45
  }
52
46
 
53
47
  async processStatusChange(
54
- userId: string,
55
- isPremium: boolean,
48
+ userId: string,
49
+ isPremium: boolean,
56
50
  productId?: string,
57
- expiresAt?: string,
58
- willRenew?: boolean,
51
+ expiresAt?: string,
52
+ willRenew?: boolean,
59
53
  periodType?: PeriodType
60
54
  ) {
61
55
  const repository = getCreditsRepository();
62
-
56
+
63
57
  if (!isPremium && !productId) {
64
58
  const currentCredits = await repository.getCredits(userId);
65
59
  if (currentCredits.success && currentCredits.data?.isPremium) {
@@ -68,49 +62,22 @@ export class SubscriptionSyncProcessor {
68
62
  }
69
63
 
70
64
  if (!isPremium && productId) {
71
- await repository.syncExpiredStatus(userId);
72
- subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
65
+ await handleExpiredSubscription(userId);
73
66
  return;
74
67
  }
75
68
 
76
69
  if (!isPremium && !productId) {
77
- const stableSyncId = `init_sync_${userId}`;
78
- await repository.initializeCredits(
79
- userId,
80
- stableSyncId,
81
- 'no_subscription',
82
- PURCHASE_SOURCE.SETTINGS,
83
- {
84
- isPremium: false,
85
- expirationDate: null,
86
- willRenew: false,
87
- periodType: null,
88
- originalTransactionId: null
89
- },
90
- PURCHASE_TYPE.INITIAL
91
- );
92
- subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
70
+ await handleFreeUserInitialization(userId);
93
71
  return;
94
72
  }
95
73
 
96
- const revenueCatData: RevenueCatData = {
97
- expirationDate: expiresAt ?? null,
98
- willRenew: willRenew ?? false,
99
- isPremium,
100
- periodType: periodType ?? null,
101
- originalTransactionId: null
102
- };
103
-
104
- const statusSyncId = `status_sync_${userId}_${isPremium ? 'premium' : 'free'}`;
105
- await repository.initializeCredits(
106
- userId,
107
- statusSyncId,
108
- productId ?? 'no_subscription',
109
- PURCHASE_SOURCE.SETTINGS,
110
- revenueCatData,
111
- PURCHASE_TYPE.INITIAL
74
+ await handlePremiumStatusSync(
75
+ userId,
76
+ isPremium,
77
+ productId ?? NO_SUBSCRIPTION_PRODUCT_ID,
78
+ expiresAt ?? null,
79
+ willRenew ?? false,
80
+ periodType ?? null
112
81
  );
113
-
114
- subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
115
82
  }
116
83
  }
@@ -0,0 +1,54 @@
1
+ import type { RevenueCatData } from "../core/RevenueCatData";
2
+ import type { PeriodType } from "../core/SubscriptionConstants";
3
+ import { PURCHASE_SOURCE, PURCHASE_TYPE } from "../core/SubscriptionConstants";
4
+ import { getCreditsRepository } from "../../credits/infrastructure/CreditsRepositoryManager";
5
+ import { emitCreditsUpdated } from "./syncEventEmitter";
6
+ import { generateInitSyncId, generateStatusSyncId } from "./syncIdGenerators";
7
+ import { NO_SUBSCRIPTION_PRODUCT_ID, DEFAULT_FREE_USER_DATA } from "./syncConstants";
8
+
9
+ export const handleExpiredSubscription = async (userId: string): Promise<void> => {
10
+ await getCreditsRepository().syncExpiredStatus(userId);
11
+ emitCreditsUpdated(userId);
12
+ };
13
+
14
+ export const handleFreeUserInitialization = async (userId: string): Promise<void> => {
15
+ const stableSyncId = generateInitSyncId(userId);
16
+ await getCreditsRepository().initializeCredits(
17
+ userId,
18
+ stableSyncId,
19
+ NO_SUBSCRIPTION_PRODUCT_ID,
20
+ PURCHASE_SOURCE.SETTINGS,
21
+ DEFAULT_FREE_USER_DATA,
22
+ PURCHASE_TYPE.INITIAL
23
+ );
24
+ emitCreditsUpdated(userId);
25
+ };
26
+
27
+ export const handlePremiumStatusSync = async (
28
+ userId: string,
29
+ isPremium: boolean,
30
+ productId: string,
31
+ expiresAt: string | null,
32
+ willRenew: boolean,
33
+ periodType: PeriodType | null
34
+ ): Promise<void> => {
35
+ const revenueCatData: RevenueCatData = {
36
+ expirationDate: expiresAt,
37
+ willRenew,
38
+ isPremium,
39
+ periodType,
40
+ originalTransactionId: null
41
+ };
42
+
43
+ const statusSyncId = generateStatusSyncId(userId, isPremium);
44
+ await getCreditsRepository().initializeCredits(
45
+ userId,
46
+ statusSyncId,
47
+ productId,
48
+ PURCHASE_SOURCE.SETTINGS,
49
+ revenueCatData,
50
+ PURCHASE_TYPE.INITIAL
51
+ );
52
+
53
+ emitCreditsUpdated(userId);
54
+ };
@@ -0,0 +1,11 @@
1
+ import type { RevenueCatData } from "../core/RevenueCatData";
2
+
3
+ export const NO_SUBSCRIPTION_PRODUCT_ID = 'no_subscription';
4
+
5
+ export const DEFAULT_FREE_USER_DATA: RevenueCatData = {
6
+ isPremium: false,
7
+ expirationDate: null,
8
+ willRenew: false,
9
+ periodType: null,
10
+ originalTransactionId: null,
11
+ };
@@ -0,0 +1,5 @@
1
+ import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
2
+
3
+ export const emitCreditsUpdated = (userId: string): void => {
4
+ subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
5
+ };
@@ -0,0 +1,19 @@
1
+ export const generatePurchaseId = (originalTransactionId: string | null, productId: string): string => {
2
+ return originalTransactionId
3
+ ? `purchase_${originalTransactionId}`
4
+ : `purchase_${productId}_${Date.now()}`;
5
+ };
6
+
7
+ export const generateRenewalId = (originalTransactionId: string | null, productId: string, expirationDate: string): string => {
8
+ return originalTransactionId
9
+ ? `renewal_${originalTransactionId}_${expirationDate}`
10
+ : `renewal_${productId}_${Date.now()}`;
11
+ };
12
+
13
+ export const generateInitSyncId = (userId: string): string => {
14
+ return `init_sync_${userId}`;
15
+ };
16
+
17
+ export const generateStatusSyncId = (userId: string, isPremium: boolean): string => {
18
+ return `status_sync_${userId}_${isPremium ? 'premium' : 'free'}`;
19
+ };
@@ -1,7 +1 @@
1
1
  export const EXPIRATION_WARNING_DAYS = 7;
2
-
3
- export const REVENUE_CAT_IGNORED_LOG_MESSAGES = [
4
- 'Purchase was cancelled',
5
- 'AppTransaction',
6
- "Couldn't find previous transactions",
7
- ] as const;
@@ -1,21 +1,13 @@
1
1
  import type { PurchasesPackage } from "react-native-purchases";
2
2
  import type { IRevenueCatService } from "../../../../shared/application/ports/IRevenueCatService";
3
- import { initializeRevenueCatService, getRevenueCatService } from "../services/RevenueCatService";
4
- import { PackageHandler } from "../handlers/PackageHandler";
3
+ import type { PackageHandler } from "../handlers/PackageHandler";
5
4
  import { SubscriptionInternalState } from "./SubscriptionInternalState";
6
- import {
7
- ensureConfigured,
8
- getCurrentUserIdOrThrow,
9
- getOrCreateService,
10
- ensureServiceAvailable,
11
- } from "./subscriptionManagerUtils";
12
-
13
- import type {
14
- SubscriptionManagerConfig,
15
- PremiumStatus,
16
- RestoreResultInfo
17
- } from "./SubscriptionManager.types";
18
-
5
+ import { ensureConfigured, ensureServiceAvailable } from "./subscriptionManagerUtils";
6
+ import type { SubscriptionManagerConfig, PremiumStatus, RestoreResultInfo } from "./SubscriptionManager.types";
7
+ import { createPackageHandler } from "./packageHandlerFactory";
8
+ import { checkPremiumStatusFromService } from "./premiumStatusChecker";
9
+ import { getPackagesOperation, purchasePackageOperation, restoreOperation } from "./managerOperations";
10
+ import { performServiceInitialization } from "./initializationHandler";
19
11
 
20
12
  class SubscriptionManagerImpl {
21
13
  private managerConfig: SubscriptionManagerConfig | null = null;
@@ -29,17 +21,8 @@ class SubscriptionManagerImpl {
29
21
  }
30
22
 
31
23
  private ensurePackageHandlerInitialized(): void {
32
- if (this.packageHandler) {
33
- return;
34
- }
35
-
36
- ensureServiceAvailable(this.serviceInstance);
37
- ensureConfigured(this.managerConfig);
38
-
39
- this.packageHandler = new PackageHandler(
40
- this.serviceInstance!,
41
- this.managerConfig!.config.entitlementIdentifier
42
- );
24
+ if (this.packageHandler) return;
25
+ this.packageHandler = createPackageHandler(this.serviceInstance, this.managerConfig);
43
26
  }
44
27
 
45
28
  async initialize(userId?: string): Promise<boolean> {
@@ -52,66 +35,41 @@ class SubscriptionManagerImpl {
52
35
  return existingPromise;
53
36
  }
54
37
 
55
- const promise = (async () => {
56
- await initializeRevenueCatService(this.managerConfig!.config);
57
- this.serviceInstance = getRevenueCatService();
58
-
59
- ensureServiceAvailable(this.serviceInstance);
60
- this.ensurePackageHandlerInitialized();
61
-
62
- const result = await this.serviceInstance!.initialize(actualUserId);
63
- return result.success;
64
- })();
65
-
38
+ const promise = this.performInitialization(actualUserId);
66
39
  this.state.initCache.setPromise(promise, actualUserId);
67
40
  return promise;
68
41
  }
69
42
 
70
- isInitializedForUser(userId: string): boolean {
71
- if (!this.serviceInstance?.isInitialized()) {
72
- return false;
73
- }
74
-
75
- return this.state.initCache.getCurrentUserId() === userId;
43
+ private async performInitialization(userId: string): Promise<boolean> {
44
+ const { service, success } = await performServiceInitialization(this.managerConfig!.config, userId);
45
+ this.serviceInstance = service;
46
+ this.ensurePackageHandlerInitialized();
47
+ return success;
76
48
  }
77
49
 
50
+ isInitializedForUser = (userId: string): boolean =>
51
+ this.serviceInstance?.isInitialized() && this.state.initCache.getCurrentUserId() === userId;
52
+
78
53
  async getPackages(): Promise<PurchasesPackage[]> {
79
- ensureConfigured(this.managerConfig);
80
- this.serviceInstance = getOrCreateService(this.serviceInstance);
81
54
  this.ensurePackageHandlerInitialized();
82
-
83
- return this.packageHandler!.fetchPackages();
55
+ return getPackagesOperation(this.managerConfig, this.serviceInstance, this.packageHandler!);
84
56
  }
85
57
 
86
58
  async purchasePackage(pkg: PurchasesPackage): Promise<boolean> {
87
- ensureConfigured(this.managerConfig);
88
- const userId = getCurrentUserIdOrThrow(this.state);
89
59
  this.ensurePackageHandlerInitialized();
90
-
91
- return this.packageHandler!.purchase(pkg, userId);
60
+ return purchasePackageOperation(pkg, this.managerConfig, this.state, this.packageHandler!);
92
61
  }
93
62
 
94
63
  async restore(): Promise<RestoreResultInfo> {
95
- ensureConfigured(this.managerConfig);
96
- const userId = getCurrentUserIdOrThrow(this.state);
97
64
  this.ensurePackageHandlerInitialized();
98
-
99
- return this.packageHandler!.restore(userId);
65
+ return restoreOperation(this.managerConfig, this.state, this.packageHandler!);
100
66
  }
101
67
 
102
68
  async checkPremiumStatus(): Promise<PremiumStatus> {
103
69
  ensureConfigured(this.managerConfig);
104
- getCurrentUserIdOrThrow(this.state);
105
70
  ensureServiceAvailable(this.serviceInstance);
106
-
107
- const customerInfo = await this.serviceInstance!.getCustomerInfo();
108
-
109
- if (!customerInfo) {
110
- throw new Error("Customer info not available");
111
- }
112
-
113
71
  this.ensurePackageHandlerInitialized();
114
- return this.packageHandler!.checkPremiumStatusFromInfo(customerInfo);
72
+ return checkPremiumStatusFromService(this.serviceInstance!, this.packageHandler!);
115
73
  }
116
74
 
117
75
  async reset(): Promise<void> {
@@ -121,13 +79,9 @@ class SubscriptionManagerImpl {
121
79
  this.packageHandler = null;
122
80
  }
123
81
 
124
- isConfigured(): boolean {
125
- return this.managerConfig !== null;
126
- }
82
+ isConfigured = (): boolean => this.managerConfig !== null;
127
83
 
128
- isInitialized(): boolean {
129
- return this.serviceInstance?.isInitialized() ?? false;
130
- }
84
+ isInitialized = (): boolean => this.serviceInstance?.isInitialized() ?? false;
131
85
 
132
86
  getEntitlementId(): string {
133
87
  ensureConfigured(this.managerConfig);
@@ -0,0 +1,14 @@
1
+ import type { IRevenueCatService } from "../../../../shared/application/ports/IRevenueCatService";
2
+ import { initializeRevenueCatService, getRevenueCatService } from "../services/RevenueCatService";
3
+ import { ensureServiceAvailable } from "./subscriptionManagerUtils";
4
+ import type { RevenueCatConfig } from "../../core/RevenueCatConfig";
5
+
6
+ export const performServiceInitialization = async (config: RevenueCatConfig, userId: string): Promise<{ service: IRevenueCatService; success: boolean }> => {
7
+ await initializeRevenueCatService(config);
8
+ const service = getRevenueCatService();
9
+
10
+ ensureServiceAvailable(service);
11
+
12
+ const result = await service!.initialize(userId);
13
+ return { service: service!, success: result.success };
14
+ };
@@ -0,0 +1,3 @@
1
+ export const ERROR_MESSAGES = {
2
+ CUSTOMER_INFO_NOT_AVAILABLE: "Customer info not available",
3
+ } as const;
@@ -0,0 +1,38 @@
1
+ import type { PurchasesPackage } from "react-native-purchases";
2
+ import type { IRevenueCatService } from "../../../../shared/application/ports/IRevenueCatService";
3
+ import type { PackageHandler } from "../handlers/PackageHandler";
4
+ import type { RestoreResultInfo } from "./SubscriptionManager.types";
5
+ import { SubscriptionInternalState } from "./SubscriptionInternalState";
6
+ import { ensureConfigured, getCurrentUserIdOrThrow, getOrCreateService } from "./subscriptionManagerUtils";
7
+ import type { SubscriptionManagerConfig } from "./SubscriptionManager.types";
8
+
9
+ export const getPackagesOperation = async (
10
+ managerConfig: SubscriptionManagerConfig | null,
11
+ serviceInstance: IRevenueCatService | null,
12
+ packageHandler: PackageHandler
13
+ ): Promise<PurchasesPackage[]> => {
14
+ ensureConfigured(managerConfig);
15
+ getOrCreateService(serviceInstance);
16
+ return packageHandler.fetchPackages();
17
+ };
18
+
19
+ export const purchasePackageOperation = async (
20
+ pkg: PurchasesPackage,
21
+ managerConfig: SubscriptionManagerConfig | null,
22
+ state: SubscriptionInternalState,
23
+ packageHandler: PackageHandler
24
+ ): Promise<boolean> => {
25
+ ensureConfigured(managerConfig);
26
+ const userId = getCurrentUserIdOrThrow(state);
27
+ return packageHandler.purchase(pkg, userId);
28
+ };
29
+
30
+ export const restoreOperation = async (
31
+ managerConfig: SubscriptionManagerConfig | null,
32
+ state: SubscriptionInternalState,
33
+ packageHandler: PackageHandler
34
+ ): Promise<RestoreResultInfo> => {
35
+ ensureConfigured(managerConfig);
36
+ const userId = getCurrentUserIdOrThrow(state);
37
+ return packageHandler.restore(userId);
38
+ };
@@ -0,0 +1,17 @@
1
+ import type { IRevenueCatService } from "../../../../shared/application/ports/IRevenueCatService";
2
+ import { PackageHandler } from "../handlers/PackageHandler";
3
+ import { ensureServiceAvailable, ensureConfigured } from "./subscriptionManagerUtils";
4
+ import type { SubscriptionManagerConfig } from "./SubscriptionManager.types";
5
+
6
+ export const createPackageHandler = (
7
+ service: IRevenueCatService | null,
8
+ config: SubscriptionManagerConfig | null
9
+ ): PackageHandler => {
10
+ ensureServiceAvailable(service);
11
+ ensureConfigured(config);
12
+
13
+ return new PackageHandler(
14
+ service!,
15
+ config!.config.entitlementIdentifier
16
+ );
17
+ };
@@ -0,0 +1,17 @@
1
+ import type { IRevenueCatService } from "../../../../shared/application/ports/IRevenueCatService";
2
+ import type { PackageHandler } from "../handlers/PackageHandler";
3
+ import type { PremiumStatus } from "./SubscriptionManager.types";
4
+ import { ERROR_MESSAGES } from "./managerConstants";
5
+
6
+ export const checkPremiumStatusFromService = async (
7
+ service: IRevenueCatService,
8
+ packageHandler: PackageHandler
9
+ ): Promise<PremiumStatus> => {
10
+ const customerInfo = await service.getCustomerInfo();
11
+
12
+ if (!customerInfo) {
13
+ throw new Error(ERROR_MESSAGES.CUSTOMER_INFO_NOT_AVAILABLE);
14
+ }
15
+
16
+ return packageHandler.checkPremiumStatusFromInfo(customerInfo);
17
+ };
@@ -1,41 +1,17 @@
1
1
  import Purchases, { type CustomerInfo, type PurchasesOfferings } from "react-native-purchases";
2
2
  import type { InitializeResult } from "../../../../shared/application/ports/IRevenueCatService";
3
- import type { RevenueCatConfig } from "../../core/RevenueCatConfig";
4
3
  import { resolveApiKey } from "../utils/ApiKeyResolver";
4
+ import type { InitializerDeps } from "./RevenueCatInitializer.types";
5
+ import { FAILED_INITIALIZATION_RESULT, CONFIGURATION_RETRY_DELAY_MS } from "./initializerConstants";
5
6
 
6
- export interface InitializerDeps {
7
- config: RevenueCatConfig;
8
- isInitialized: () => boolean;
9
- getCurrentUserId: () => string | null;
10
- setInitialized: (value: boolean) => void;
11
- setCurrentUserId: (userId: string) => void;
12
- }
7
+ export type { InitializerDeps } from "./RevenueCatInitializer.types";
13
8
 
14
- // State management to prevent race conditions
15
9
  const configurationState = {
16
10
  isPurchasesConfigured: false,
17
- isLogHandlerConfigured: false,
18
11
  configurationInProgress: false,
19
12
  configurationPromise: null as Promise<InitializeResult> | null,
20
13
  };
21
14
 
22
-
23
- // Simple lock mechanism to prevent concurrent configurations (implementation deferred)
24
-
25
- function configureLogHandler(): void {
26
- if (configurationState.isLogHandlerConfigured) return;
27
- if (typeof Purchases.setLogHandler !== 'function') return;
28
- try {
29
- Purchases.setLogHandler((_logLevel, message) => {
30
- const ignoreMessages = ['Purchase was cancelled', 'AppTransaction', "Couldn't find previous transactions"];
31
- if (ignoreMessages.some(m => message.includes(m))) return;
32
- });
33
- configurationState.isLogHandlerConfigured = true;
34
- } catch {
35
- // Failing to set log handler should not block initialization
36
- }
37
- }
38
-
39
15
  function buildSuccessResult(deps: InitializerDeps, customerInfo: CustomerInfo, offerings: PurchasesOfferings): InitializeResult {
40
16
  const isPremium = !!customerInfo.entitlements.active[deps.config.entitlementIdentifier];
41
17
  return { success: true, offering: offerings.current, isPremium };
@@ -54,7 +30,7 @@ export async function initializeSDK(
54
30
  ]);
55
31
  return buildSuccessResult(deps, customerInfo, offerings);
56
32
  } catch {
57
- return { success: false, offering: null, isPremium: false };
33
+ return FAILED_INITIALIZATION_RESULT;
58
34
  }
59
35
  }
60
36
 
@@ -73,7 +49,7 @@ export async function initializeSDK(
73
49
  const offerings = await Purchases.getOfferings();
74
50
  return buildSuccessResult(deps, customerInfo, offerings);
75
51
  } catch {
76
- return { success: false, offering: null, isPremium: false };
52
+ return FAILED_INITIALIZATION_RESULT;
77
53
  }
78
54
  }
79
55
 
@@ -82,13 +58,13 @@ export async function initializeSDK(
82
58
  await configurationState.configurationPromise;
83
59
  return initializeSDK(deps, userId, apiKey);
84
60
  }
85
- await new Promise(resolve => setTimeout(resolve, 100));
61
+ await new Promise(resolve => setTimeout(resolve, CONFIGURATION_RETRY_DELAY_MS));
86
62
  return initializeSDK(deps, userId, apiKey);
87
63
  }
88
64
 
89
65
  const key = apiKey || resolveApiKey(deps.config);
90
66
  if (!key) {
91
- return { success: false, offering: null, isPremium: false };
67
+ return FAILED_INITIALIZATION_RESULT;
92
68
  }
93
69
 
94
70
  let resolveInProgress: (value: InitializeResult) => void;
@@ -98,7 +74,6 @@ export async function initializeSDK(
98
74
 
99
75
  configurationState.configurationInProgress = true;
100
76
  try {
101
- configureLogHandler();
102
77
  await Purchases.configure({ apiKey: key, appUserID: userId });
103
78
  configurationState.isPurchasesConfigured = true;
104
79
  deps.setInitialized(true);
@@ -113,9 +88,8 @@ export async function initializeSDK(
113
88
  resolveInProgress!(result);
114
89
  return result;
115
90
  } catch {
116
- const errorResult = { success: false, offering: null, isPremium: false };
117
- resolveInProgress!(errorResult);
118
- return errorResult;
91
+ resolveInProgress!(FAILED_INITIALIZATION_RESULT);
92
+ return FAILED_INITIALIZATION_RESULT;
119
93
  } finally {
120
94
  configurationState.configurationInProgress = false;
121
95
  configurationState.configurationPromise = null;
@@ -0,0 +1,9 @@
1
+ import type { RevenueCatConfig } from "../../core/RevenueCatConfig";
2
+
3
+ export interface InitializerDeps {
4
+ config: RevenueCatConfig;
5
+ isInitialized: () => boolean;
6
+ getCurrentUserId: () => string | null;
7
+ setInitialized: (value: boolean) => void;
8
+ setCurrentUserId: (userId: string) => void;
9
+ }
@@ -1,134 +1,2 @@
1
- /**
2
- * RevenueCat Service Implementation
3
- * Main service class for RevenueCat operations
4
- */
5
-
6
- import Purchases from "react-native-purchases";
7
- import type {
8
- PurchasesOffering,
9
- PurchasesPackage,
10
- CustomerInfo,
11
- } from "react-native-purchases";
12
- import type {
13
- IRevenueCatService,
14
- InitializeResult,
15
- PurchaseResult,
16
- RestoreResult,
17
- } from "../../../../shared/application/ports/IRevenueCatService";
18
- import type { RevenueCatConfig } from "../../core/RevenueCatConfig";
19
- import { resolveApiKey } from "../utils/ApiKeyResolver";
20
- import { initializeSDK } from "./RevenueCatInitializer";
21
- import { fetchOfferings } from "./OfferingsFetcher";
22
- import { handlePurchase } from "./PurchaseHandler";
23
- import { handleRestore } from "./RestoreHandler";
24
- import { CustomerInfoListenerManager } from "./CustomerInfoListenerManager";
25
- import { ServiceStateManager } from "./ServiceStateManager";
26
-
27
- export class RevenueCatService implements IRevenueCatService {
28
- private stateManager: ServiceStateManager;
29
- private listenerManager: CustomerInfoListenerManager;
30
-
31
- constructor(config: RevenueCatConfig) {
32
- this.stateManager = new ServiceStateManager(config);
33
- this.listenerManager = new CustomerInfoListenerManager();
34
- }
35
-
36
- getRevenueCatKey(): string | null {
37
- return resolveApiKey(this.stateManager.getConfig());
38
- }
39
-
40
- isInitialized(): boolean {
41
- return this.stateManager.isInitialized();
42
- }
43
-
44
- getCurrentUserId(): string | null {
45
- return this.stateManager.getCurrentUserId();
46
- }
47
-
48
- private getSDKParams() {
49
- return {
50
- config: this.stateManager.getConfig(),
51
- isInitialized: () => this.isInitialized(),
52
- getCurrentUserId: () => this.stateManager.getCurrentUserId(),
53
- setInitialized: (value: boolean) => this.stateManager.setInitialized(value),
54
- setCurrentUserId: (id: string | null) => this.stateManager.setCurrentUserId(id),
55
- };
56
- }
57
-
58
- async initialize(userId: string, apiKey?: string): Promise<InitializeResult> {
59
- if (this.isInitialized() && this.getCurrentUserId() === userId) {
60
- const customerInfo = await Purchases.getCustomerInfo();
61
- const isPremium = !!customerInfo.entitlements.active[this.stateManager.getConfig().entitlementIdentifier];
62
- return {
63
- success: true,
64
- offering: await this.fetchOfferings(),
65
- isPremium,
66
- };
67
- }
68
-
69
- const result = await initializeSDK(this.getSDKParams(), userId, apiKey);
70
-
71
- if (result.success) {
72
- this.listenerManager.setUserId(userId);
73
- this.listenerManager.setupListener(this.stateManager.getConfig());
74
- }
75
-
76
- return result;
77
- }
78
-
79
- async fetchOfferings(): Promise<PurchasesOffering | null> {
80
- return fetchOfferings(this.getSDKParams());
81
- }
82
-
83
- async purchasePackage(
84
- pkg: PurchasesPackage,
85
- userId: string
86
- ): Promise<PurchaseResult> {
87
- return handlePurchase(this.getSDKParams(), pkg, userId);
88
- }
89
-
90
- async restorePurchases(userId: string): Promise<RestoreResult> {
91
- return handleRestore(this.getSDKParams(), userId);
92
- }
93
-
94
- async getCustomerInfo(): Promise<CustomerInfo | null> {
95
- if (!this.isInitialized()) {
96
- return null;
97
- }
98
- return Purchases.getCustomerInfo();
99
- }
100
-
101
- async reset(): Promise<void> {
102
- if (!this.isInitialized()) {
103
- return;
104
- }
105
-
106
- this.listenerManager.destroy();
107
-
108
- try {
109
- await Purchases.logOut();
110
- this.stateManager.setInitialized(false);
111
- } catch {
112
- // Silently fail during logout to allow cleanup to complete
113
- }
114
- }
115
- }
116
-
117
- let revenueCatServiceInstance: RevenueCatService | null = null;
118
-
119
- export function initializeRevenueCatService(
120
- config: RevenueCatConfig
121
- ): RevenueCatService {
122
- if (!revenueCatServiceInstance) {
123
- revenueCatServiceInstance = new RevenueCatService(config);
124
- }
125
- return revenueCatServiceInstance;
126
- }
127
-
128
- export function getRevenueCatService(): RevenueCatService | null {
129
- return revenueCatServiceInstance;
130
- }
131
-
132
- export function resetRevenueCatService(): void {
133
- revenueCatServiceInstance = null;
134
- }
1
+ export { RevenueCatService } from "./RevenueCatService.types";
2
+ export { initializeRevenueCatService, getRevenueCatService, resetRevenueCatService } from "./revenueCatServiceInstance";
@@ -0,0 +1,87 @@
1
+ import Purchases from "react-native-purchases";
2
+ import type { PurchasesOffering, PurchasesPackage, CustomerInfo } from "react-native-purchases";
3
+ import type { IRevenueCatService, InitializeResult, PurchaseResult, RestoreResult } from "../../../../shared/application/ports/IRevenueCatService";
4
+ import type { RevenueCatConfig } from "../../core/RevenueCatConfig";
5
+ import { resolveApiKey } from "../utils/ApiKeyResolver";
6
+ import { initializeSDK } from "./RevenueCatInitializer";
7
+ import { fetchOfferings } from "./OfferingsFetcher";
8
+ import { handlePurchase } from "./PurchaseHandler";
9
+ import { handleRestore } from "./RestoreHandler";
10
+ import { CustomerInfoListenerManager } from "./CustomerInfoListenerManager";
11
+ import { ServiceStateManager } from "./ServiceStateManager";
12
+
13
+ export class RevenueCatService implements IRevenueCatService {
14
+ private stateManager: ServiceStateManager;
15
+ private listenerManager: CustomerInfoListenerManager;
16
+
17
+ constructor(config: RevenueCatConfig) {
18
+ this.stateManager = new ServiceStateManager(config);
19
+ this.listenerManager = new CustomerInfoListenerManager();
20
+ }
21
+
22
+ getRevenueCatKey = (): string | null => resolveApiKey(this.stateManager.getConfig());
23
+
24
+ isInitialized = (): boolean => this.stateManager.isInitialized();
25
+
26
+ getCurrentUserId = (): string | null => this.stateManager.getCurrentUserId();
27
+
28
+ private getSDKParams() {
29
+ return {
30
+ config: this.stateManager.getConfig(),
31
+ isInitialized: () => this.isInitialized(),
32
+ getCurrentUserId: () => this.stateManager.getCurrentUserId(),
33
+ setInitialized: (value: boolean) => this.stateManager.setInitialized(value),
34
+ setCurrentUserId: (id: string | null) => this.stateManager.setCurrentUserId(id),
35
+ };
36
+ }
37
+
38
+ async initialize(userId: string, apiKey?: string): Promise<InitializeResult> {
39
+ if (this.isInitialized() && this.getCurrentUserId() === userId) {
40
+ const customerInfo = await Purchases.getCustomerInfo();
41
+ const isPremium = !!customerInfo.entitlements.active[this.stateManager.getConfig().entitlementIdentifier];
42
+ return {
43
+ success: true,
44
+ offering: await this.fetchOfferings(),
45
+ isPremium,
46
+ };
47
+ }
48
+
49
+ const result = await initializeSDK(this.getSDKParams(), userId, apiKey);
50
+
51
+ if (result.success) {
52
+ this.listenerManager.setUserId(userId);
53
+ this.listenerManager.setupListener(this.stateManager.getConfig());
54
+ }
55
+
56
+ return result;
57
+ }
58
+
59
+ async fetchOfferings(): Promise<PurchasesOffering | null> {
60
+ return fetchOfferings(this.getSDKParams());
61
+ }
62
+
63
+ async purchasePackage(pkg: PurchasesPackage, userId: string): Promise<PurchaseResult> {
64
+ return handlePurchase(this.getSDKParams(), pkg, userId);
65
+ }
66
+
67
+ async restorePurchases(userId: string): Promise<RestoreResult> {
68
+ return handleRestore(this.getSDKParams(), userId);
69
+ }
70
+
71
+ async getCustomerInfo(): Promise<CustomerInfo | null> {
72
+ if (!this.isInitialized()) return null;
73
+ return Purchases.getCustomerInfo();
74
+ }
75
+
76
+ async reset(): Promise<void> {
77
+ if (!this.isInitialized()) return;
78
+
79
+ this.listenerManager.destroy();
80
+
81
+ try {
82
+ await Purchases.logOut();
83
+ this.stateManager.setInitialized(false);
84
+ } catch {
85
+ }
86
+ }
87
+ }
@@ -0,0 +1,9 @@
1
+ import type { InitializeResult } from "../../../../shared/application/ports/IRevenueCatService";
2
+
3
+ export const FAILED_INITIALIZATION_RESULT: InitializeResult = {
4
+ success: false,
5
+ offering: null,
6
+ isPremium: false,
7
+ };
8
+
9
+ export const CONFIGURATION_RETRY_DELAY_MS = 100;
@@ -0,0 +1,17 @@
1
+ import { RevenueCatService } from "./RevenueCatService.types";
2
+ import type { RevenueCatConfig } from "../../core/RevenueCatConfig";
3
+
4
+ let revenueCatServiceInstance: RevenueCatService | null = null;
5
+
6
+ export const initializeRevenueCatService = (config: RevenueCatConfig): RevenueCatService => {
7
+ if (!revenueCatServiceInstance) {
8
+ revenueCatServiceInstance = new RevenueCatService(config);
9
+ }
10
+ return revenueCatServiceInstance;
11
+ };
12
+
13
+ export const getRevenueCatService = (): RevenueCatService | null => revenueCatServiceInstance;
14
+
15
+ export const resetRevenueCatService = (): void => {
16
+ revenueCatServiceInstance = null;
17
+ };