@umituz/react-native-subscription 2.27.39 → 2.27.40

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.27.39",
3
+ "version": "2.27.40",
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,12 @@
1
+ /**
2
+ * RevenueCat subscription data (Single Source of Truth)
3
+ * Used across the subscription package for storing RevenueCat data in Firestore
4
+ */
5
+ export interface RevenueCatData {
6
+ expirationDate?: string | null;
7
+ willRenew?: boolean;
8
+ originalTransactionId?: string;
9
+ isPremium?: boolean;
10
+ /** RevenueCat period type: NORMAL, INTRO, or TRIAL */
11
+ periodType?: "NORMAL" | "INTRO" | "TRIAL";
12
+ }
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * Credits Repository
3
3
  */
4
-
5
4
  declare const __DEV__: boolean;
6
5
 
7
6
  import { doc, getDoc, runTransaction, serverTimestamp, type Firestore, type Transaction } from "firebase/firestore";
@@ -11,18 +10,11 @@ import type { UserCreditsDocumentRead, PurchaseSource } from "../models/UserCred
11
10
  import { initializeCreditsTransaction, type InitializeCreditsMetadata } from "../services/CreditsInitializer";
12
11
  import { detectPackageType } from "../../utils/packageTypeDetector";
13
12
  import { getCreditAllocation } from "../../utils/creditMapper";
14
-
15
13
  import { CreditsMapper } from "../mappers/CreditsMapper";
14
+ import type { RevenueCatData } from "../../domain/types/RevenueCatData";
15
+ import { initializeFreeCredits as initializeFreeCreditsService } from "../services/FreeCreditsService";
16
16
 
17
- /** RevenueCat subscription data to save (Single Source of Truth) */
18
- export interface RevenueCatData {
19
- expirationDate?: string | null;
20
- willRenew?: boolean;
21
- originalTransactionId?: string;
22
- isPremium?: boolean;
23
- /** RevenueCat period type: NORMAL, INTRO, or TRIAL */
24
- periodType?: "NORMAL" | "INTRO" | "TRIAL";
25
- }
17
+ export type { RevenueCatData } from "../../domain/types/RevenueCatData";
26
18
 
27
19
  export class CreditsRepository extends BaseRepository {
28
20
  constructor(private config: CreditsConfig) { super(); }
@@ -58,11 +50,8 @@ export class CreditsRepository extends BaseRepository {
58
50
  }
59
51
 
60
52
  async initializeCredits(
61
- userId: string,
62
- purchaseId?: string,
63
- productId?: string,
64
- source?: PurchaseSource,
65
- revenueCatData?: RevenueCatData
53
+ userId: string, purchaseId?: string, productId?: string,
54
+ source?: PurchaseSource, revenueCatData?: RevenueCatData
66
55
  ): Promise<CreditsResult> {
67
56
  const db = getFirestore();
68
57
  if (!db) return { success: false, error: { message: "No DB", code: "INIT_ERR" } };
@@ -79,9 +68,7 @@ export class CreditsRepository extends BaseRepository {
79
68
  }
80
69
 
81
70
  const metadata: InitializeCreditsMetadata = {
82
- productId,
83
- source,
84
- // RevenueCat data for Single Source of Truth
71
+ productId, source,
85
72
  expirationDate: revenueCatData?.expirationDate,
86
73
  willRenew: revenueCatData?.willRenew,
87
74
  originalTransactionId: revenueCatData?.originalTransactionId,
@@ -89,23 +76,14 @@ export class CreditsRepository extends BaseRepository {
89
76
  periodType: revenueCatData?.periodType,
90
77
  };
91
78
 
92
- const res = await initializeCreditsTransaction(
93
- db,
94
- this.getRef(db, userId),
95
- cfg,
96
- purchaseId,
97
- metadata
98
- );
99
-
79
+ const res = await initializeCreditsTransaction(db, this.getRef(db, userId), cfg, purchaseId, metadata);
100
80
  return {
101
81
  success: true,
102
- data: CreditsMapper.toEntity({
103
- ...res,
104
- purchasedAt: undefined,
105
- lastUpdatedAt: undefined,
106
- })
82
+ data: CreditsMapper.toEntity({ ...res, purchasedAt: undefined, lastUpdatedAt: undefined })
107
83
  };
108
- } catch (e: any) { return { success: false, error: { message: e.message, code: "INIT_ERR" } }; }
84
+ } catch (e: any) {
85
+ return { success: false, error: { message: e.message, code: "INIT_ERR" } };
86
+ }
109
87
  }
110
88
 
111
89
  async deductCredit(userId: string, cost: number = 1): Promise<DeductCreditsResult> {
@@ -133,98 +111,22 @@ export class CreditsRepository extends BaseRepository {
133
111
  return !!(res.success && res.data && res.data.credits >= cost);
134
112
  }
135
113
 
136
- /**
137
- * Initialize free credits for new users
138
- * Creates a credits document with freeCredits amount (no subscription)
139
- * Uses transaction to prevent race condition with premium init
140
- */
141
114
  async initializeFreeCredits(userId: string): Promise<CreditsResult> {
142
- const db = getFirestore();
143
- if (!db) return { success: false, error: { message: "No DB", code: "INIT_ERR" } };
144
-
145
- const freeCredits = this.config.freeCredits ?? 0;
146
- if (freeCredits <= 0) {
147
- return { success: false, error: { message: "Free credits not configured", code: "NO_FREE_CREDITS" } };
148
- }
149
-
150
- try {
151
- const ref = this.getRef(db, userId);
152
-
153
- // Use transaction to atomically check-and-set
154
- const result = await runTransaction(db, async (tx: Transaction) => {
155
- const snap = await tx.get(ref);
156
-
157
- // Don't overwrite if document already exists (premium or previous init)
158
- if (snap.exists()) {
159
- if (__DEV__) console.log("[CreditsRepository] Credits document already exists, skipping free credits init");
160
- const existing = snap.data() as UserCreditsDocumentRead;
161
- return { skipped: true, data: CreditsMapper.toEntity(existing) };
162
- }
163
-
164
- // Create new document with free credits
165
- const now = serverTimestamp();
166
-
167
- const creditsData = {
168
- // Not premium - just free credits
169
- isPremium: false,
170
- status: "free" as const,
171
-
172
- // Free credits - store initial amount for tracking
173
- credits: freeCredits,
174
- creditLimit: freeCredits,
175
- initialFreeCredits: freeCredits,
176
- isFreeCredits: true,
177
-
178
- // Dates
179
- createdAt: now,
180
- lastUpdatedAt: now,
181
- };
182
-
183
- tx.set(ref, creditsData);
184
-
185
- if (__DEV__) console.log("[CreditsRepository] Initialized free credits:", { userId: userId.slice(0, 8), credits: freeCredits });
186
-
187
- return {
188
- skipped: false,
189
- data: {
190
- isPremium: false,
191
- status: "free" as const,
192
- credits: freeCredits,
193
- creditLimit: freeCredits,
194
- purchasedAt: null,
195
- expirationDate: null,
196
- lastUpdatedAt: null,
197
- willRenew: false,
198
- }
199
- };
200
- });
201
-
202
- return { success: true, data: result.data };
203
- } catch (e: any) {
204
- if (__DEV__) console.error("[CreditsRepository] Free credits init error:", e.message);
205
- return { success: false, error: { message: e.message, code: "INIT_ERR" } };
206
- }
115
+ return initializeFreeCreditsService({ config: this.config, getRef: this.getRef.bind(this) }, userId);
207
116
  }
208
117
 
209
- /** Sync expired subscription status to Firestore (background) */
210
118
  async syncExpiredStatus(userId: string): Promise<void> {
211
119
  const db = getFirestore();
212
120
  if (!db) return;
213
-
214
121
  try {
215
122
  const ref = this.getRef(db, userId);
216
123
  const { updateDoc } = await import("firebase/firestore");
217
- await updateDoc(ref, {
218
- isPremium: false,
219
- status: "expired",
220
- lastUpdatedAt: serverTimestamp(),
221
- });
222
- if (__DEV__) console.log("[CreditsRepository] Synced expired status for:", userId.slice(0, 8));
124
+ await updateDoc(ref, { isPremium: false, status: "expired", lastUpdatedAt: serverTimestamp() });
125
+ if (__DEV__) console.log("[CreditsRepository] Synced expired status:", userId.slice(0, 8));
223
126
  } catch (e) {
224
127
  if (__DEV__) console.error("[CreditsRepository] Sync expired failed:", e);
225
128
  }
226
129
  }
227
-
228
130
  }
229
131
 
230
132
  export const createCreditsRepository = (c: CreditsConfig) => new CreditsRepository(c);
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Free Credits Service
3
+ * Handles initialization of free credits for new users
4
+ */
5
+ declare const __DEV__: boolean;
6
+
7
+ import { runTransaction, serverTimestamp, type Firestore, type DocumentReference, type Transaction } from "firebase/firestore";
8
+ import { getFirestore } from "@umituz/react-native-firebase";
9
+ import type { CreditsConfig, CreditsResult, UserCredits } from "../../domain/entities/Credits";
10
+ import type { UserCreditsDocumentRead } from "../models/UserCreditsDocument";
11
+ import { CreditsMapper } from "../mappers/CreditsMapper";
12
+
13
+ export interface FreeCreditsServiceConfig {
14
+ config: CreditsConfig;
15
+ getRef: (db: Firestore, userId: string) => DocumentReference;
16
+ }
17
+
18
+ /**
19
+ * Initialize free credits for new users
20
+ * Creates a credits document with freeCredits amount (no subscription)
21
+ * Uses transaction to prevent race condition with premium init
22
+ */
23
+ export async function initializeFreeCredits(
24
+ deps: FreeCreditsServiceConfig,
25
+ userId: string
26
+ ): Promise<CreditsResult> {
27
+ const db = getFirestore();
28
+ if (!db) return { success: false, error: { message: "No DB", code: "INIT_ERR" } };
29
+
30
+ const freeCredits = deps.config.freeCredits ?? 0;
31
+ if (freeCredits <= 0) {
32
+ return { success: false, error: { message: "Free credits not configured", code: "NO_FREE_CREDITS" } };
33
+ }
34
+
35
+ try {
36
+ const ref = deps.getRef(db, userId);
37
+
38
+ const result = await runTransaction(db, async (tx: Transaction) => {
39
+ const snap = await tx.get(ref);
40
+
41
+ // Don't overwrite if document already exists (premium or previous init)
42
+ if (snap.exists()) {
43
+ if (__DEV__) console.log("[FreeCreditsService] Credits document exists, skipping");
44
+ const existing = snap.data() as UserCreditsDocumentRead;
45
+ return { skipped: true, data: CreditsMapper.toEntity(existing) };
46
+ }
47
+
48
+ // Create new document with free credits
49
+ const now = serverTimestamp();
50
+ const creditsData = {
51
+ isPremium: false,
52
+ status: "free" as const,
53
+ credits: freeCredits,
54
+ creditLimit: freeCredits,
55
+ initialFreeCredits: freeCredits,
56
+ isFreeCredits: true,
57
+ createdAt: now,
58
+ lastUpdatedAt: now,
59
+ };
60
+
61
+ tx.set(ref, creditsData);
62
+
63
+ if (__DEV__) console.log("[FreeCreditsService] Initialized:", { userId: userId.slice(0, 8), credits: freeCredits });
64
+
65
+ const entity: UserCredits = {
66
+ isPremium: false,
67
+ status: "free",
68
+ credits: freeCredits,
69
+ creditLimit: freeCredits,
70
+ purchasedAt: null,
71
+ expirationDate: null,
72
+ lastUpdatedAt: null,
73
+ willRenew: false,
74
+ };
75
+ return { skipped: false, data: entity };
76
+ });
77
+
78
+ return { success: true, data: result.data };
79
+ } catch (e: any) {
80
+ if (__DEV__) console.error("[FreeCreditsService] Init error:", e.message);
81
+ return { success: false, error: { message: e.message, code: "INIT_ERR" } };
82
+ }
83
+ }
@@ -1,38 +1,17 @@
1
1
  /**
2
2
  * Subscription Initializer
3
3
  */
4
-
5
4
  declare const __DEV__: boolean;
6
5
 
7
6
  import { Platform } from "react-native";
8
7
  import type { CustomerInfo } from "react-native-purchases";
9
- import type { CreditsConfig } from "../../domain/entities/Credits";
10
8
  import { configureCreditsRepository, getCreditsRepository } from "../repositories/CreditsRepositoryProvider";
11
9
  import { SubscriptionManager } from "../../revenuecat/infrastructure/managers/SubscriptionManager";
12
10
  import { configureAuthProvider } from "../../presentation/hooks/useAuthAwarePurchase";
13
- import type { RevenueCatData } from "../repositories/CreditsRepository";
14
-
15
- export interface FirebaseAuthLike {
16
- currentUser: { uid: string; isAnonymous: boolean } | null;
17
- onAuthStateChanged: (callback: (user: { uid: string; isAnonymous: boolean } | null) => void) => () => void;
18
- }
19
-
20
- export interface CreditPackageConfig { identifierPattern?: string; amounts?: Record<string, number>; }
21
-
22
- export interface SubscriptionInitConfig {
23
- apiKey?: string;
24
- apiKeyIos?: string;
25
- apiKeyAndroid?: string;
26
- entitlementId: string;
27
- credits: CreditsConfig;
28
- getAnonymousUserId: () => Promise<string>;
29
- getFirebaseAuth: () => FirebaseAuthLike | null;
30
- showAuthModal: () => void;
31
- onCreditsUpdated?: (userId: string) => void;
32
- creditPackages?: CreditPackageConfig;
33
- timeoutMs?: number;
34
- authStateTimeoutMs?: number;
35
- }
11
+ import type { RevenueCatData } from "../../domain/types/RevenueCatData";
12
+ import type { SubscriptionInitConfig, FirebaseAuthLike } from "./SubscriptionInitializerTypes";
13
+
14
+ export type { FirebaseAuthLike, CreditPackageConfig, SubscriptionInitConfig } from "./SubscriptionInitializerTypes";
36
15
 
37
16
  const waitForAuthState = async (getAuth: () => FirebaseAuthLike | null, timeoutMs: number): Promise<string | undefined> => {
38
17
  const auth = getAuth();
@@ -44,6 +23,19 @@ const waitForAuthState = async (getAuth: () => FirebaseAuthLike | null, timeoutM
44
23
  });
45
24
  };
46
25
 
26
+ /** Extract RevenueCat data from CustomerInfo (Single Source of Truth) */
27
+ const extractRevenueCatData = (customerInfo: CustomerInfo, entitlementId: string): RevenueCatData => {
28
+ const entitlement = customerInfo.entitlements.active[entitlementId]
29
+ ?? customerInfo.entitlements.all[entitlementId];
30
+ return {
31
+ expirationDate: entitlement?.expirationDate ?? customerInfo.latestExpirationDate ?? null,
32
+ willRenew: entitlement?.willRenew ?? false,
33
+ originalTransactionId: entitlement?.originalPurchaseDate ?? undefined,
34
+ isPremium: Object.keys(customerInfo.entitlements.active).length > 0,
35
+ periodType: entitlement?.periodType as "NORMAL" | "INTRO" | "TRIAL" | undefined,
36
+ };
37
+ };
38
+
47
39
  export const initializeSubscription = async (config: SubscriptionInitConfig): Promise<void> => {
48
40
  const {
49
41
  apiKey, apiKeyIos, apiKeyAndroid, entitlementId, credits,
@@ -56,148 +48,63 @@ export const initializeSubscription = async (config: SubscriptionInitConfig): Pr
56
48
 
57
49
  configureCreditsRepository({ ...credits, creditPackageAmounts: creditPackages?.amounts });
58
50
 
59
- /** Extract RevenueCat data from CustomerInfo (Single Source of Truth) */
60
- const extractRevenueCatData = (customerInfo: CustomerInfo, _productId: string): RevenueCatData => {
61
- const entitlement = customerInfo.entitlements.active[entitlementId]
62
- ?? customerInfo.entitlements.all[entitlementId];
63
-
64
- return {
65
- expirationDate: entitlement?.expirationDate ?? customerInfo.latestExpirationDate ?? null,
66
- willRenew: entitlement?.willRenew ?? false,
67
- originalTransactionId: entitlement?.originalPurchaseDate ?? undefined,
68
- isPremium: Object.keys(customerInfo.entitlements.active).length > 0,
69
- periodType: entitlement?.periodType as "NORMAL" | "INTRO" | "TRIAL" | undefined,
70
- };
71
- };
72
-
73
51
  const onPurchase = async (userId: string, productId: string, customerInfo: CustomerInfo, source?: string) => {
74
- if (__DEV__) {
75
- console.log('[SubscriptionInitializer] onPurchase called:', { userId, productId, source });
76
- }
52
+ if (__DEV__) console.log('[SubscriptionInitializer] onPurchase:', { userId, productId, source });
77
53
  try {
78
- const revenueCatData = extractRevenueCatData(customerInfo, productId);
79
- const result = await getCreditsRepository().initializeCredits(
80
- userId,
81
- `purchase_${productId}_${Date.now()}`,
82
- productId,
83
- source as any,
84
- revenueCatData
85
- );
86
- if (__DEV__) {
87
- console.log('[SubscriptionInitializer] Credits initialized:', result);
88
- }
54
+ const revenueCatData = extractRevenueCatData(customerInfo, entitlementId);
55
+ await getCreditsRepository().initializeCredits(userId, `purchase_${productId}_${Date.now()}`, productId, source as any, revenueCatData);
89
56
  onCreditsUpdated?.(userId);
90
57
  } catch (error) {
91
- if (__DEV__) {
92
- console.error('[SubscriptionInitializer] Credits init failed:', error);
93
- }
58
+ if (__DEV__) console.error('[SubscriptionInitializer] Credits init failed:', error);
94
59
  }
95
60
  };
96
61
 
97
62
  const onRenewal = async (userId: string, productId: string, newExpirationDate: string, customerInfo: CustomerInfo) => {
98
- if (__DEV__) {
99
- console.log('[SubscriptionInitializer] onRenewal called:', { userId, productId });
100
- }
63
+ if (__DEV__) console.log('[SubscriptionInitializer] onRenewal:', { userId, productId });
101
64
  try {
102
- const revenueCatData = extractRevenueCatData(customerInfo, productId);
103
- // Update expiration date from renewal
65
+ const revenueCatData = extractRevenueCatData(customerInfo, entitlementId);
104
66
  revenueCatData.expirationDate = newExpirationDate || revenueCatData.expirationDate;
105
- const result = await getCreditsRepository().initializeCredits(
106
- userId,
107
- `renewal_${productId}_${Date.now()}`,
108
- productId,
109
- "renewal" as any,
110
- revenueCatData
111
- );
112
- if (__DEV__) {
113
- console.log('[SubscriptionInitializer] Credits reset on renewal:', result);
114
- }
67
+ await getCreditsRepository().initializeCredits(userId, `renewal_${productId}_${Date.now()}`, productId, "renewal" as any, revenueCatData);
115
68
  onCreditsUpdated?.(userId);
116
69
  } catch (error) {
117
- if (__DEV__) {
118
- console.error('[SubscriptionInitializer] Renewal credits init failed:', error);
119
- }
70
+ if (__DEV__) console.error('[SubscriptionInitializer] Renewal credits init failed:', error);
120
71
  }
121
72
  };
122
73
 
123
- /** Sync premium status changes (including cancellation) to Firestore */
124
74
  const onPremiumStatusChanged = async (
125
- userId: string,
126
- isPremium: boolean,
127
- productId?: string,
128
- expiresAt?: string,
129
- willRenew?: boolean,
130
- periodType?: "NORMAL" | "INTRO" | "TRIAL"
75
+ userId: string, isPremium: boolean, productId?: string,
76
+ expiresAt?: string, willRenew?: boolean, periodType?: "NORMAL" | "INTRO" | "TRIAL"
131
77
  ) => {
132
- if (__DEV__) {
133
- console.log('[SubscriptionInitializer] onPremiumStatusChanged:', { userId, isPremium, productId, willRenew, periodType });
134
- }
78
+ if (__DEV__) console.log('[SubscriptionInitializer] onPremiumStatusChanged:', { userId, isPremium, productId, willRenew, periodType });
135
79
  try {
136
80
  // If not premium and no productId, this is a free user - don't overwrite free credits
137
81
  if (!isPremium && !productId) {
138
- if (__DEV__) {
139
- console.log('[SubscriptionInitializer] Free user detected, preserving free credits');
140
- }
82
+ if (__DEV__) console.log('[SubscriptionInitializer] Free user detected, preserving free credits');
141
83
  return;
142
84
  }
143
85
 
144
86
  // If premium became false, check if actually expired or just canceled
145
87
  if (!isPremium && productId) {
146
- // Check if subscription is truly expired (expiration date in the past)
147
88
  const isActuallyExpired = !expiresAt || new Date(expiresAt) < new Date();
148
-
149
89
  if (isActuallyExpired) {
150
- // Subscription truly expired - zero out credits
151
90
  await getCreditsRepository().syncExpiredStatus(userId);
152
- if (__DEV__) {
153
- console.log('[SubscriptionInitializer] Subscription expired, synced status');
154
- }
91
+ if (__DEV__) console.log('[SubscriptionInitializer] Subscription expired, synced status');
155
92
  } else {
156
- // Subscription canceled but not expired - preserve credits until expiration
157
- if (__DEV__) {
158
- console.log('[SubscriptionInitializer] Subscription canceled but not expired, preserving credits until:', expiresAt);
159
- }
160
- // Update willRenew to false but keep credits
161
- const revenueCatData: RevenueCatData = {
162
- expirationDate: expiresAt,
163
- willRenew: false, // Canceled
164
- isPremium: true, // Still has access until expiration
165
- periodType,
166
- };
167
- await getCreditsRepository().initializeCredits(
168
- userId,
169
- `status_sync_canceled_${Date.now()}`,
170
- productId,
171
- "settings" as any,
172
- revenueCatData
173
- );
93
+ if (__DEV__) console.log('[SubscriptionInitializer] Canceled but not expired, preserving until:', expiresAt);
94
+ const revenueCatData: RevenueCatData = { expirationDate: expiresAt, willRenew: false, isPremium: true, periodType };
95
+ await getCreditsRepository().initializeCredits(userId, `status_sync_canceled_${Date.now()}`, productId, "settings" as any, revenueCatData);
174
96
  }
175
97
  onCreditsUpdated?.(userId);
176
98
  return;
177
99
  }
178
100
 
179
101
  // Premium user - initialize credits with subscription data
180
- const revenueCatData: RevenueCatData = {
181
- expirationDate: expiresAt ?? null,
182
- willRenew: willRenew ?? false,
183
- isPremium,
184
- periodType,
185
- };
186
- await getCreditsRepository().initializeCredits(
187
- userId,
188
- `status_sync_${Date.now()}`,
189
- productId,
190
- "settings" as any,
191
- revenueCatData
192
- );
193
- if (__DEV__) {
194
- console.log('[SubscriptionInitializer] Premium status synced to Firestore');
195
- }
102
+ const revenueCatData: RevenueCatData = { expirationDate: expiresAt ?? null, willRenew: willRenew ?? false, isPremium, periodType };
103
+ await getCreditsRepository().initializeCredits(userId, `status_sync_${Date.now()}`, productId, "settings" as any, revenueCatData);
104
+ if (__DEV__) console.log('[SubscriptionInitializer] Premium status synced to Firestore');
196
105
  onCreditsUpdated?.(userId);
197
106
  } catch (error) {
198
- if (__DEV__) {
199
- console.error('[SubscriptionInitializer] Premium status sync failed:', error);
200
- }
107
+ if (__DEV__) console.error('[SubscriptionInitializer] Premium status sync failed:', error);
201
108
  }
202
109
  };
203
110
 
@@ -216,22 +123,16 @@ export const initializeSubscription = async (config: SubscriptionInitConfig): Pr
216
123
  });
217
124
 
218
125
  const userId = await waitForAuthState(getFirebaseAuth, authStateTimeoutMs);
219
-
220
- // Initialize subscription without blocking - let it complete in background
126
+
221
127
  try {
222
128
  if (timeoutMs > 0) {
223
- const timeout = new Promise<boolean>((_, reject) =>
224
- setTimeout(() => reject(new Error("Timeout")), timeoutMs)
225
- );
129
+ const timeout = new Promise<boolean>((_, reject) => setTimeout(() => reject(new Error("Timeout")), timeoutMs));
226
130
  await Promise.race([SubscriptionManager.initialize(userId), timeout]);
227
131
  } else {
228
132
  await SubscriptionManager.initialize(userId);
229
133
  }
230
134
  } catch (error) {
231
- // Log error but don't throw - subscription will continue in background
232
- if (__DEV__) {
233
- console.warn('[SubscriptionInitializer] Initialize timeout/error (non-critical):', error);
234
- }
135
+ if (__DEV__) console.warn('[SubscriptionInitializer] Initialize timeout/error (non-critical):', error);
235
136
  }
236
137
 
237
138
  configureAuthProvider({
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Subscription Initializer Types
3
+ */
4
+
5
+ import type { CreditsConfig } from "../../domain/entities/Credits";
6
+
7
+ export interface FirebaseAuthLike {
8
+ currentUser: { uid: string; isAnonymous: boolean } | null;
9
+ onAuthStateChanged: (callback: (user: { uid: string; isAnonymous: boolean } | null) => void) => () => void;
10
+ }
11
+
12
+ export interface CreditPackageConfig {
13
+ identifierPattern?: string;
14
+ amounts?: Record<string, number>;
15
+ }
16
+
17
+ export interface SubscriptionInitConfig {
18
+ apiKey?: string;
19
+ apiKeyIos?: string;
20
+ apiKeyAndroid?: string;
21
+ entitlementId: string;
22
+ credits: CreditsConfig;
23
+ getAnonymousUserId: () => Promise<string>;
24
+ getFirebaseAuth: () => FirebaseAuthLike | null;
25
+ showAuthModal: () => void;
26
+ onCreditsUpdated?: (userId: string) => void;
27
+ creditPackages?: CreditPackageConfig;
28
+ timeoutMs?: number;
29
+ authStateTimeoutMs?: number;
30
+ }
@@ -1,63 +1,24 @@
1
1
  /**
2
- * Trial Service
3
- * Handles device-based trial tracking to prevent abuse
4
- * Uses persistent device ID that survives app reinstalls
2
+ * Trial Service - Device-based trial tracking to prevent abuse
5
3
  */
6
-
7
4
  declare const __DEV__: boolean;
8
5
 
9
- import {
10
- doc,
11
- getDoc,
12
- setDoc,
13
- serverTimestamp,
14
- arrayUnion,
15
- } from "firebase/firestore";
6
+ import { doc, getDoc, setDoc, serverTimestamp, arrayUnion } from "firebase/firestore";
16
7
  import { getFirestore } from "@umituz/react-native-firebase";
17
8
  import { PersistentDeviceIdService } from "@umituz/react-native-design-system";
9
+ import type { TrialEligibilityResult } from "./TrialTypes";
18
10
 
19
- const DEVICE_TRIALS_COLLECTION = "device_trials";
11
+ export { TRIAL_CONFIG, type DeviceTrialRecord, type TrialEligibilityResult } from "./TrialTypes";
20
12
 
21
- /** Trial constants */
22
- export const TRIAL_CONFIG = {
23
- DURATION_DAYS: 3,
24
- CREDITS: 5,
25
- } as const;
26
-
27
- /** Device trial record in Firestore */
28
- export interface DeviceTrialRecord {
29
- deviceId: string;
30
- hasUsedTrial: boolean;
31
- trialInProgress?: boolean;
32
- trialStartedAt?: Date;
33
- trialEndedAt?: Date;
34
- trialConvertedAt?: Date;
35
- lastUserId?: string;
36
- userIds: string[];
37
- createdAt: Date;
38
- updatedAt: Date;
39
- }
40
-
41
- /** Trial eligibility result */
42
- export interface TrialEligibilityResult {
43
- eligible: boolean;
44
- reason?: "already_used" | "device_not_found" | "error";
45
- deviceId?: string;
46
- }
13
+ const DEVICE_TRIALS_COLLECTION = "device_trials";
47
14
 
48
- /**
49
- * Get persistent device ID
50
- */
15
+ /** Get persistent device ID */
51
16
  export async function getDeviceId(): Promise<string> {
52
17
  return PersistentDeviceIdService.getDeviceId();
53
18
  }
54
19
 
55
- /**
56
- * Check if device is eligible for trial
57
- */
58
- export async function checkTrialEligibility(
59
- deviceId?: string
60
- ): Promise<TrialEligibilityResult> {
20
+ /** Check if device is eligible for trial */
21
+ export async function checkTrialEligibility(deviceId?: string): Promise<TrialEligibilityResult> {
61
22
  try {
62
23
  const effectiveDeviceId = deviceId || await getDeviceId();
63
24
  const db = getFirestore();
@@ -110,13 +71,8 @@ export async function checkTrialEligibility(
110
71
  }
111
72
  }
112
73
 
113
- /**
114
- * Record trial start for a device
115
- */
116
- export async function recordTrialStart(
117
- userId: string,
118
- deviceId?: string
119
- ): Promise<boolean> {
74
+ /** Record trial start for a device */
75
+ export async function recordTrialStart(userId: string, deviceId?: string): Promise<boolean> {
120
76
  try {
121
77
  const effectiveDeviceId = deviceId || await getDeviceId();
122
78
  const db = getFirestore();
@@ -134,8 +90,6 @@ export async function recordTrialStart(
134
90
  trialRef,
135
91
  {
136
92
  deviceId: effectiveDeviceId,
137
- // Don't set hasUsedTrial here - only set when trial ends or converts
138
- // This allows retry if user cancels before trial period ends
139
93
  trialInProgress: true,
140
94
  trialStartedAt: serverTimestamp(),
141
95
  lastUserId: userId,
@@ -174,23 +128,18 @@ export async function recordTrialStart(
174
128
  /**
175
129
  * Record trial end (cancelled or expired)
176
130
  */
177
- export async function recordTrialEnd(
178
- deviceId?: string
179
- ): Promise<boolean> {
131
+ export async function recordTrialEnd(deviceId?: string): Promise<boolean> {
180
132
  try {
181
133
  const effectiveDeviceId = deviceId || await getDeviceId();
182
134
  const db = getFirestore();
183
135
 
184
- if (!db) {
185
- return false;
186
- }
136
+ if (!db) return false;
187
137
 
188
138
  const trialRef = doc(db, DEVICE_TRIALS_COLLECTION, effectiveDeviceId);
189
139
 
190
140
  await setDoc(
191
141
  trialRef,
192
142
  {
193
- // Mark trial as used when it ends (prevents retry)
194
143
  hasUsedTrial: true,
195
144
  trialInProgress: false,
196
145
  trialEndedAt: serverTimestamp(),
@@ -215,23 +164,18 @@ export async function recordTrialEnd(
215
164
  /**
216
165
  * Record trial conversion to paid subscription
217
166
  */
218
- export async function recordTrialConversion(
219
- deviceId?: string
220
- ): Promise<boolean> {
167
+ export async function recordTrialConversion(deviceId?: string): Promise<boolean> {
221
168
  try {
222
169
  const effectiveDeviceId = deviceId || await getDeviceId();
223
170
  const db = getFirestore();
224
171
 
225
- if (!db) {
226
- return false;
227
- }
172
+ if (!db) return false;
228
173
 
229
174
  const trialRef = doc(db, DEVICE_TRIALS_COLLECTION, effectiveDeviceId);
230
175
 
231
176
  await setDoc(
232
177
  trialRef,
233
178
  {
234
- // Mark trial as used after conversion (prevents retry)
235
179
  hasUsedTrial: true,
236
180
  trialInProgress: false,
237
181
  trialConvertedAt: serverTimestamp(),
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Trial Types and Constants
3
+ * Device-based trial tracking types
4
+ */
5
+
6
+ /** Trial constants */
7
+ export const TRIAL_CONFIG = {
8
+ DURATION_DAYS: 3,
9
+ CREDITS: 5,
10
+ } as const;
11
+
12
+ /** Device trial record in Firestore */
13
+ export interface DeviceTrialRecord {
14
+ deviceId: string;
15
+ hasUsedTrial: boolean;
16
+ trialInProgress?: boolean;
17
+ trialStartedAt?: Date;
18
+ trialEndedAt?: Date;
19
+ trialConvertedAt?: Date;
20
+ lastUserId?: string;
21
+ userIds: string[];
22
+ createdAt: Date;
23
+ updatedAt: Date;
24
+ }
25
+
26
+ /** Trial eligibility result */
27
+ export interface TrialEligibilityResult {
28
+ eligible: boolean;
29
+ reason?: "already_used" | "device_not_found" | "error";
30
+ deviceId?: string;
31
+ }