@umituz/react-native-subscription 2.31.26 → 2.32.1

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.1",
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,20 +1,15 @@
1
- import { getDoc, setDoc } from "firebase/firestore";
2
- import { BaseRepository, serverTimestamp, type Firestore, type DocumentReference } from "@umituz/react-native-firebase";
1
+ import type { Firestore, DocumentReference } from "@umituz/react-native-firebase";
2
+ import { BaseRepository } from "@umituz/react-native-firebase";
3
3
  import type { CreditsConfig, CreditsResult, DeductCreditsResult } from "../core/Credits";
4
- import type { UserCreditsDocumentRead, PurchaseSource } from "../core/UserCreditsDocument";
5
- import { initializeCreditsTransaction } from "../application/CreditsInitializer";
6
- import { mapCreditsDocumentToEntity } from "../core/CreditsMapper";
4
+ import type { PurchaseSource } from "../core/UserCreditsDocument";
7
5
  import type { RevenueCatData } from "../../revenuecat/core/types";
8
6
  import { deductCreditsOperation } from "../application/DeductCreditsCommand";
9
- import { calculateCreditLimit } from "../application/CreditLimitCalculator";
10
7
  import { PURCHASE_TYPE, type PurchaseType } from "../../subscription/core/SubscriptionConstants";
11
8
  import { requireFirestore, buildDocRef, type CollectionConfig } from "../../../shared/infrastructure/firestore";
12
- import { SUBSCRIPTION_STATUS } from "../../subscription/core/SubscriptionConstants";
9
+ import { fetchCredits, checkHasCredits } from "./operations/CreditsFetcher";
10
+ import { syncExpiredStatus } from "./operations/CreditsWriter";
11
+ import { initializeCreditsWithRetry } from "./operations/CreditsInitializer";
13
12
 
14
- /**
15
- * Credits Repository
16
- * Provides domain-specific database operations for credits system.
17
- */
18
13
  export class CreditsRepository extends BaseRepository {
19
14
  constructor(private config: CreditsConfig) {
20
15
  super(config.collectionName);
@@ -34,14 +29,7 @@ export class CreditsRepository extends BaseRepository {
34
29
 
35
30
  async getCredits(userId: string): Promise<CreditsResult> {
36
31
  const db = requireFirestore();
37
- const snap = await getDoc(this.getRef(db, userId));
38
-
39
- if (!snap.exists()) {
40
- return { success: true, data: null, error: null };
41
- }
42
-
43
- const entity = mapCreditsDocumentToEntity(snap.data() as UserCreditsDocumentRead);
44
- return { success: true, data: entity, error: null };
32
+ return fetchCredits(this.getRef(db, userId));
45
33
  }
46
34
 
47
35
  async initializeCredits(
@@ -53,101 +41,32 @@ export class CreditsRepository extends BaseRepository {
53
41
  type: PurchaseType = PURCHASE_TYPE.INITIAL
54
42
  ): Promise<CreditsResult> {
55
43
  const db = requireFirestore();
56
- const creditLimit = calculateCreditLimit(productId, this.config);
57
- const cfg = { ...this.config, creditLimit };
58
-
59
- const maxRetries = 3;
60
- let lastError: any;
61
-
62
- for (let attempt = 0; attempt < maxRetries; attempt++) {
63
- try {
64
- const result = await initializeCreditsTransaction(
65
- db,
66
- this.getRef(db, userId),
67
- cfg,
68
- purchaseId,
69
- {
70
- productId,
71
- source,
72
- expirationDate: revenueCatData.expirationDate,
73
- willRenew: revenueCatData.willRenew,
74
- originalTransactionId: revenueCatData.originalTransactionId,
75
- isPremium: revenueCatData.isPremium,
76
- periodType: revenueCatData.periodType,
77
- unsubscribeDetectedAt: revenueCatData.unsubscribeDetectedAt,
78
- billingIssueDetectedAt: revenueCatData.billingIssueDetectedAt,
79
- store: revenueCatData.store,
80
- ownershipType: revenueCatData.ownershipType,
81
- type,
82
- }
83
- );
84
-
85
- return {
86
- success: true,
87
- data: result.finalData ? mapCreditsDocumentToEntity(result.finalData) : null,
88
- error: null,
89
- };
90
- } catch (error: any) {
91
- lastError = error;
92
-
93
- const isTransientError =
94
- error?.code === 'already-exists' ||
95
- error?.code === 'DEADLINE_EXCEEDED' ||
96
- error?.code === 'UNAVAILABLE' ||
97
- error?.code === 'RESOURCE_EXHAUSTED' ||
98
- error?.message?.includes('already-exists') ||
99
- error?.message?.includes('timeout') ||
100
- error?.message?.includes('unavailable');
101
-
102
- if (isTransientError && attempt < maxRetries - 1) {
103
- await new Promise(resolve => setTimeout(resolve, 100 * (attempt + 1)));
104
- continue;
105
- }
106
- break;
107
- }
108
- }
109
-
110
- const errorMessage = lastError instanceof Error
111
- ? lastError.message
112
- : typeof lastError === 'string'
113
- ? lastError
114
- : 'Unknown error during credit initialization';
115
-
116
- const errorCode = lastError?.code ?? 'UNKNOWN_ERROR';
117
-
118
- return {
119
- success: false,
120
- data: null,
121
- error: {
122
- message: errorMessage,
123
- code: errorCode,
124
- },
125
- };
44
+ return initializeCreditsWithRetry({
45
+ db,
46
+ ref: this.getRef(db, userId),
47
+ config: this.config,
48
+ userId,
49
+ purchaseId,
50
+ productId,
51
+ source,
52
+ revenueCatData,
53
+ type,
54
+ });
126
55
  }
127
56
 
128
- /**
129
- * Deducts credits using atomic transaction logic.
130
- */
131
57
  async deductCredit(userId: string, cost: number): Promise<DeductCreditsResult> {
132
58
  const db = requireFirestore();
133
59
  return deductCreditsOperation(db, this.getRef(db, userId), cost, userId);
134
60
  }
135
61
 
136
62
  async hasCredits(userId: string, cost: number): Promise<boolean> {
137
- const result = await this.getCredits(userId);
138
- if (!result.success || !result.data) return false;
139
- return result.data.credits >= cost;
63
+ const db = requireFirestore();
64
+ return checkHasCredits(this.getRef(db, userId), cost);
140
65
  }
141
66
 
142
67
  async syncExpiredStatus(userId: string): Promise<void> {
143
68
  const db = requireFirestore();
144
- const ref = this.getRef(db, userId);
145
- await setDoc(ref, {
146
- isPremium: false,
147
- status: SUBSCRIPTION_STATUS.EXPIRED,
148
- willRenew: false,
149
- expirationDate: serverTimestamp(),
150
- }, { merge: true });
69
+ await syncExpiredStatus(this.getRef(db, userId));
151
70
  }
152
71
  }
153
72
 
@@ -0,0 +1,23 @@
1
+ import { getDoc } from "firebase/firestore";
2
+ import type { Firestore, DocumentReference } from "@umituz/react-native-firebase";
3
+ import type { CreditsResult } from "../../core/Credits";
4
+ import type { UserCreditsDocumentRead } from "../../core/UserCreditsDocument";
5
+ import { mapCreditsDocumentToEntity } from "../../core/CreditsMapper";
6
+ import { requireFirestore } from "../../../../shared/infrastructure/firestore";
7
+
8
+ export async function fetchCredits(ref: DocumentReference): Promise<CreditsResult> {
9
+ const snap = await getDoc(ref);
10
+
11
+ if (!snap.exists()) {
12
+ return { success: true, data: null, error: null };
13
+ }
14
+
15
+ const entity = mapCreditsDocumentToEntity(snap.data() as UserCreditsDocumentRead);
16
+ return { success: true, data: entity, error: null };
17
+ }
18
+
19
+ export async function checkHasCredits(ref: DocumentReference, cost: number): Promise<boolean> {
20
+ const result = await fetchCredits(ref);
21
+ if (!result.success || !result.data) return false;
22
+ return result.data.credits >= cost;
23
+ }
@@ -0,0 +1,98 @@
1
+ import type { Firestore, DocumentReference } from "@umituz/react-native-firebase";
2
+ import type { CreditsConfig, CreditsResult } from "../../core/Credits";
3
+ import type { UserCreditsDocumentRead, PurchaseSource } from "../../core/UserCreditsDocument";
4
+ import { initializeCreditsTransaction } from "../../application/CreditsInitializer";
5
+ import { mapCreditsDocumentToEntity } from "../../core/CreditsMapper";
6
+ import type { RevenueCatData } from "../../../revenuecat/core/types";
7
+ import { calculateCreditLimit } from "../../application/CreditLimitCalculator";
8
+ import { PURCHASE_TYPE, type PurchaseType } from "../../../subscription/core/SubscriptionConstants";
9
+
10
+ interface InitializeCreditsParams {
11
+ db: Firestore;
12
+ ref: DocumentReference;
13
+ config: CreditsConfig;
14
+ userId: string;
15
+ purchaseId: string;
16
+ productId: string;
17
+ source: PurchaseSource;
18
+ revenueCatData: RevenueCatData;
19
+ type?: PurchaseType;
20
+ }
21
+
22
+ function isTransientError(error: any): boolean {
23
+ return (
24
+ error?.code === 'already-exists' ||
25
+ error?.code === 'DEADLINE_EXCEEDED' ||
26
+ error?.code === 'UNAVAILABLE' ||
27
+ error?.code === 'RESOURCE_EXHAUSTED' ||
28
+ error?.message?.includes('already-exists') ||
29
+ error?.message?.includes('timeout') ||
30
+ error?.message?.includes('unavailable')
31
+ );
32
+ }
33
+
34
+ export async function initializeCreditsWithRetry(params: InitializeCreditsParams): Promise<CreditsResult> {
35
+ const { db, ref, config, purchaseId, productId, source, revenueCatData, type = PURCHASE_TYPE.INITIAL } = params;
36
+
37
+ const creditLimit = calculateCreditLimit(productId, config);
38
+ const cfg = { ...config, creditLimit };
39
+
40
+ const maxRetries = 3;
41
+ let lastError: any;
42
+
43
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
44
+ try {
45
+ const result = await initializeCreditsTransaction(
46
+ db,
47
+ ref,
48
+ cfg,
49
+ purchaseId,
50
+ {
51
+ productId,
52
+ source,
53
+ expirationDate: revenueCatData.expirationDate,
54
+ willRenew: revenueCatData.willRenew,
55
+ originalTransactionId: revenueCatData.originalTransactionId,
56
+ isPremium: revenueCatData.isPremium,
57
+ periodType: revenueCatData.periodType,
58
+ unsubscribeDetectedAt: revenueCatData.unsubscribeDetectedAt,
59
+ billingIssueDetectedAt: revenueCatData.billingIssueDetectedAt,
60
+ store: revenueCatData.store,
61
+ ownershipType: revenueCatData.ownershipType,
62
+ type,
63
+ }
64
+ );
65
+
66
+ return {
67
+ success: true,
68
+ data: result.finalData ? mapCreditsDocumentToEntity(result.finalData) : null,
69
+ error: null,
70
+ };
71
+ } catch (error: any) {
72
+ lastError = error;
73
+
74
+ if (isTransientError(error) && attempt < maxRetries - 1) {
75
+ await new Promise(resolve => setTimeout(resolve, 100 * (attempt + 1)));
76
+ continue;
77
+ }
78
+ break;
79
+ }
80
+ }
81
+
82
+ const errorMessage = lastError instanceof Error
83
+ ? lastError.message
84
+ : typeof lastError === 'string'
85
+ ? lastError
86
+ : 'Unknown error during credit initialization';
87
+
88
+ const errorCode = lastError?.code ?? 'UNKNOWN_ERROR';
89
+
90
+ return {
91
+ success: false,
92
+ data: null,
93
+ error: {
94
+ message: errorMessage,
95
+ code: errorCode,
96
+ },
97
+ };
98
+ }
@@ -0,0 +1,13 @@
1
+ import { setDoc } from "firebase/firestore";
2
+ import type { DocumentReference } from "@umituz/react-native-firebase";
3
+ import { serverTimestamp } from "@umituz/react-native-firebase";
4
+ import { SUBSCRIPTION_STATUS } from "../../../subscription/core/SubscriptionConstants";
5
+
6
+ export async function syncExpiredStatus(ref: DocumentReference): Promise<void> {
7
+ await setDoc(ref, {
8
+ isPremium: false,
9
+ status: SUBSCRIPTION_STATUS.EXPIRED,
10
+ willRenew: false,
11
+ expirationDate: serverTimestamp(),
12
+ }, { merge: true });
13
+ }
@@ -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;