@umituz/react-native-subscription 2.31.26 → 2.32.0

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.31.26",
3
+ "version": "2.32.0",
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",
@@ -0,0 +1,49 @@
1
+ import type { InitializeResult } from "../../../../shared/application/ports/IRevenueCatService";
2
+
3
+ export class ConfigurationStateManager {
4
+ private _isPurchasesConfigured = false;
5
+ private _configurationPromise: Promise<InitializeResult> | null = null;
6
+ private _resolveConfiguration: ((value: InitializeResult) => void) | null = null;
7
+
8
+ get isPurchasesConfigured(): boolean {
9
+ return this._isPurchasesConfigured;
10
+ }
11
+
12
+ get configurationPromise(): Promise<InitializeResult> | null {
13
+ return this._configurationPromise;
14
+ }
15
+
16
+ get isConfiguring(): boolean {
17
+ return this._configurationPromise !== null;
18
+ }
19
+
20
+ startConfiguration(): (value: InitializeResult) => void {
21
+ if (this._configurationPromise) {
22
+ throw new Error('Configuration already in progress');
23
+ }
24
+
25
+ this._configurationPromise = new Promise((resolve) => {
26
+ this._resolveConfiguration = resolve;
27
+ });
28
+
29
+ return (value: InitializeResult) => {
30
+ if (this._resolveConfiguration) {
31
+ this._resolveConfiguration(value);
32
+ }
33
+ };
34
+ }
35
+
36
+ completeConfiguration(success: boolean): void {
37
+ this._isPurchasesConfigured = success;
38
+ this._configurationPromise = null;
39
+ this._resolveConfiguration = null;
40
+ }
41
+
42
+ reset(): void {
43
+ this._isPurchasesConfigured = false;
44
+ this._configurationPromise = null;
45
+ this._resolveConfiguration = null;
46
+ }
47
+ }
48
+
49
+ export const configState = new ConfigurationStateManager();
@@ -1,133 +1,25 @@
1
- import Purchases, { type CustomerInfo, type PurchasesOfferings } from "react-native-purchases";
2
1
  import type { InitializeResult } from "../../../../shared/application/ports/IRevenueCatService";
3
2
  import { resolveApiKey } from "../utils/ApiKeyResolver";
4
3
  import type { InitializerDeps } from "./RevenueCatInitializer.types";
5
4
  import { FAILED_INITIALIZATION_RESULT, CONFIGURATION_RETRY_DELAY_MS, MAX_INIT_RETRIES } from "./initializerConstants";
5
+ import { configState } from "./ConfigurationStateManager";
6
+ import { handleUserSwitch, handleInitialConfiguration, fetchCurrentUserData } from "./userSwitchHandler";
6
7
 
7
8
  export type { InitializerDeps } from "./RevenueCatInitializer.types";
8
9
 
9
- /**
10
- * Thread-safe configuration state manager
11
- * Prevents race conditions during concurrent initialization attempts
12
- */
13
- class ConfigurationStateManager {
14
- private _isPurchasesConfigured = false;
15
- private _configurationPromise: Promise<InitializeResult> | null = null;
16
- private _resolveConfiguration: ((value: InitializeResult) => void) | null = null;
17
-
18
- get isPurchasesConfigured(): boolean {
19
- return this._isPurchasesConfigured;
20
- }
21
-
22
- get configurationPromise(): Promise<InitializeResult> | null {
23
- return this._configurationPromise;
24
- }
25
-
26
- get isConfiguring(): boolean {
27
- return this._configurationPromise !== null;
28
- }
29
-
30
- /**
31
- * Starts a new configuration process
32
- * @throws Error if configuration is already in progress
33
- */
34
- startConfiguration(): (value: InitializeResult) => void {
35
- if (this._configurationPromise) {
36
- throw new Error('Configuration already in progress');
37
- }
38
-
39
- this._configurationPromise = new Promise((resolve) => {
40
- this._resolveConfiguration = resolve;
41
- });
42
-
43
- return (value: InitializeResult) => {
44
- if (this._resolveConfiguration) {
45
- this._resolveConfiguration(value);
46
- }
47
- };
48
- }
49
-
50
- /**
51
- * Completes the configuration process
52
- */
53
- completeConfiguration(success: boolean): void {
54
- this._isPurchasesConfigured = success;
55
- this._configurationPromise = null;
56
- this._resolveConfiguration = null;
57
- }
58
-
59
- /**
60
- * Resets the configuration state
61
- */
62
- reset(): void {
63
- this._isPurchasesConfigured = false;
64
- this._configurationPromise = null;
65
- this._resolveConfiguration = null;
66
- }
67
- }
68
-
69
- const configState = new ConfigurationStateManager();
70
-
71
- function buildSuccessResult(deps: InitializerDeps, customerInfo: CustomerInfo, offerings: PurchasesOfferings): InitializeResult {
72
- const isPremium = !!customerInfo.entitlements.active[deps.config.entitlementIdentifier];
73
- return { success: true, offering: offerings.current, isPremium };
74
- }
75
-
76
10
  export async function initializeSDK(
77
11
  deps: InitializerDeps,
78
12
  userId: string,
79
13
  apiKey?: string
80
14
  ): Promise<InitializeResult> {
81
15
  if (deps.isInitialized() && deps.getCurrentUserId() === userId) {
82
- try {
83
- const [customerInfo, offerings] = await Promise.all([
84
- Purchases.getCustomerInfo(),
85
- Purchases.getOfferings(),
86
- ]);
87
- return buildSuccessResult(deps, customerInfo, offerings);
88
- } catch (error) {
89
- console.error('[RevenueCatInitializer] Failed to fetch customer info/offerings for initialized user', {
90
- userId,
91
- error
92
- });
93
- return FAILED_INITIALIZATION_RESULT;
94
- }
16
+ return fetchCurrentUserData(deps);
95
17
  }
96
18
 
97
19
  if (configState.isPurchasesConfigured) {
98
- try {
99
- const currentAppUserId = await Purchases.getAppUserID();
100
- let customerInfo;
101
-
102
- // Handle user switching
103
- if (currentAppUserId !== userId) {
104
- if (userId) {
105
- // Switch to authenticated user
106
- const result = await Purchases.logIn(userId);
107
- customerInfo = result.customerInfo;
108
- } else {
109
- // User logged out - switch to anonymous
110
- customerInfo = await Purchases.logOut();
111
- }
112
- } else {
113
- customerInfo = await Purchases.getCustomerInfo();
114
- }
115
-
116
- deps.setInitialized(true);
117
- deps.setCurrentUserId(userId ?? null);
118
- const offerings = await Purchases.getOfferings();
119
- return buildSuccessResult(deps, customerInfo, offerings);
120
- } catch (error) {
121
- console.error('[RevenueCatInitializer] Failed during user switch or fetch', {
122
- userId,
123
- currentAppUserId: await Purchases.getAppUserID().catch(() => 'unknown'),
124
- error
125
- });
126
- return FAILED_INITIALIZATION_RESULT;
127
- }
20
+ return handleUserSwitch(deps, userId);
128
21
  }
129
22
 
130
- // Wait for ongoing configuration with retry limit
131
23
  if (configState.isConfiguring) {
132
24
  let retryCount = 0;
133
25
  while (configState.isConfiguring && retryCount < MAX_INIT_RETRIES) {
@@ -144,7 +36,6 @@ export async function initializeSDK(
144
36
  return FAILED_INITIALIZATION_RESULT;
145
37
  }
146
38
 
147
- // Configuration completed, try again
148
39
  return initializeSDK(deps, userId, apiKey);
149
40
  }
150
41
 
@@ -161,36 +52,12 @@ export async function initializeSDK(
161
52
  userId,
162
53
  error
163
54
  });
164
- // Configuration already in progress, wait and retry
165
55
  await new Promise(resolve => setTimeout(resolve, CONFIGURATION_RETRY_DELAY_MS));
166
56
  return initializeSDK(deps, userId, apiKey);
167
57
  }
168
58
 
169
- try {
170
- // Configure with null appUserID for anonymous users (generates RevenueCat anonymous ID)
171
- // For authenticated users, use their userId
172
- await Purchases.configure({ apiKey: key, appUserID: userId ?? null });
173
- deps.setInitialized(true);
174
- deps.setCurrentUserId(userId ?? null);
175
-
176
- const [customerInfo, offerings] = await Promise.all([
177
- Purchases.getCustomerInfo(),
178
- Purchases.getOfferings(),
179
- ]);
180
-
181
- const result = buildSuccessResult(deps, customerInfo, offerings);
182
- configState.completeConfiguration(true);
183
- resolveConfig(result);
184
- return result;
185
- } catch (error) {
186
- console.error('[RevenueCatInitializer] SDK configuration failed', {
187
- userId,
188
- apiKey: apiKey ? 'provided' : 'from config',
189
- error
190
- });
191
- configState.completeConfiguration(false);
192
- resolveConfig(FAILED_INITIALIZATION_RESULT);
193
- return FAILED_INITIALIZATION_RESULT;
194
- }
59
+ const result = await handleInitialConfiguration(deps, userId, key);
60
+ configState.completeConfiguration(result.success);
61
+ resolveConfig(result);
62
+ return result;
195
63
  }
196
-
@@ -0,0 +1,94 @@
1
+ import Purchases, { type CustomerInfo } from "react-native-purchases";
2
+ import type { InitializeResult } from "../../../../shared/application/ports/IRevenueCatService";
3
+ import type { InitializerDeps } from "./RevenueCatInitializer.types";
4
+ import { FAILED_INITIALIZATION_RESULT } from "./initializerConstants";
5
+
6
+ function buildSuccessResult(deps: InitializerDeps, customerInfo: CustomerInfo, offerings: any): InitializeResult {
7
+ const isPremium = !!customerInfo.entitlements.active[deps.config.entitlementIdentifier];
8
+ return { success: true, offering: offerings.current, isPremium };
9
+ }
10
+
11
+ function normalizeUserId(userId: string): string | null {
12
+ return (userId && userId.length > 0) ? userId : null;
13
+ }
14
+
15
+ function isAnonymousId(userId: string): boolean {
16
+ return userId.startsWith('$RCAnonymous');
17
+ }
18
+
19
+ export async function handleUserSwitch(
20
+ deps: InitializerDeps,
21
+ userId: string
22
+ ): Promise<InitializeResult> {
23
+ try {
24
+ const currentAppUserId = await Purchases.getAppUserID();
25
+ let customerInfo;
26
+
27
+ const normalizedUserId = normalizeUserId(userId);
28
+ const normalizedCurrentUserId = isAnonymousId(currentAppUserId) ? null : currentAppUserId;
29
+
30
+ if (normalizedCurrentUserId !== normalizedUserId) {
31
+ if (normalizedUserId) {
32
+ const result = await Purchases.logIn(normalizedUserId);
33
+ customerInfo = result.customerInfo;
34
+ } else {
35
+ customerInfo = await Purchases.getCustomerInfo();
36
+ }
37
+ } else {
38
+ customerInfo = await Purchases.getCustomerInfo();
39
+ }
40
+
41
+ deps.setInitialized(true);
42
+ deps.setCurrentUserId(normalizedUserId);
43
+ const offerings = await Purchases.getOfferings();
44
+ return buildSuccessResult(deps, customerInfo, offerings);
45
+ } catch (error) {
46
+ console.error('[UserSwitchHandler] Failed during user switch or fetch', {
47
+ userId,
48
+ currentAppUserId: await Purchases.getAppUserID().catch(() => 'unknown'),
49
+ error
50
+ });
51
+ return FAILED_INITIALIZATION_RESULT;
52
+ }
53
+ }
54
+
55
+ export async function handleInitialConfiguration(
56
+ deps: InitializerDeps,
57
+ userId: string,
58
+ apiKey: string
59
+ ): Promise<InitializeResult> {
60
+ try {
61
+ const normalizedUserId = normalizeUserId(userId);
62
+ await Purchases.configure({ apiKey, appUserID: normalizedUserId });
63
+ deps.setInitialized(true);
64
+ deps.setCurrentUserId(normalizedUserId);
65
+
66
+ const [customerInfo, offerings] = await Promise.all([
67
+ Purchases.getCustomerInfo(),
68
+ Purchases.getOfferings(),
69
+ ]);
70
+
71
+ return buildSuccessResult(deps, customerInfo, offerings);
72
+ } catch (error) {
73
+ console.error('[UserSwitchHandler] SDK configuration failed', {
74
+ userId,
75
+ error
76
+ });
77
+ return FAILED_INITIALIZATION_RESULT;
78
+ }
79
+ }
80
+
81
+ export async function fetchCurrentUserData(deps: InitializerDeps): Promise<InitializeResult> {
82
+ try {
83
+ const [customerInfo, offerings] = await Promise.all([
84
+ Purchases.getCustomerInfo(),
85
+ Purchases.getOfferings(),
86
+ ]);
87
+ return buildSuccessResult(deps, customerInfo, offerings);
88
+ } catch (error) {
89
+ console.error('[UserSwitchHandler] Failed to fetch customer info/offerings for initialized user', {
90
+ error
91
+ });
92
+ return FAILED_INITIALIZATION_RESULT;
93
+ }
94
+ }
@@ -34,21 +34,30 @@ class SubscriptionManagerImpl {
34
34
  async initialize(userId?: string): Promise<boolean> {
35
35
  this.ensureConfigured();
36
36
 
37
- const actualUserId = userId ?? (await this.managerConfig.getAnonymousUserId()) ?? '';
38
- const { shouldInit, existingPromise } = this.state.initCache.tryAcquireInitialization(actualUserId);
37
+ let actualUserId: string | null = null;
38
+
39
+ if (userId && userId.length > 0) {
40
+ actualUserId = userId;
41
+ } else {
42
+ const anonymousId = await this.managerConfig.getAnonymousUserId();
43
+ actualUserId = (anonymousId && anonymousId.length > 0) ? anonymousId : null;
44
+ }
45
+
46
+ const cacheKey = actualUserId ?? '__anonymous__';
47
+ const { shouldInit, existingPromise } = this.state.initCache.tryAcquireInitialization(cacheKey);
39
48
 
40
49
  if (!shouldInit && existingPromise) {
41
50
  return existingPromise;
42
51
  }
43
52
 
44
53
  const promise = this.performInitialization(actualUserId);
45
- this.state.initCache.setPromise(promise, actualUserId);
54
+ this.state.initCache.setPromise(promise, cacheKey);
46
55
  return promise;
47
56
  }
48
57
 
49
- private async performInitialization(userId: string): Promise<boolean> {
58
+ private async performInitialization(userId: string | null): Promise<boolean> {
50
59
  this.ensureConfigured();
51
- const { service, success } = await performServiceInitialization(this.managerConfig.config, userId);
60
+ const { service, success } = await performServiceInitialization(this.managerConfig.config, userId ?? '');
52
61
  this.serviceInstance = service ?? null;
53
62
  this.ensurePackageHandlerInitialized();
54
63
  return success;