@umituz/react-native-subscription 2.32.0 → 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.32.0",
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
+ }