@umituz/react-native-subscription 2.9.5 → 2.9.7
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.9.
|
|
3
|
+
"version": "2.9.7",
|
|
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",
|
|
@@ -67,4 +67,4 @@
|
|
|
67
67
|
"README.md",
|
|
68
68
|
"LICENSE"
|
|
69
69
|
]
|
|
70
|
-
}
|
|
70
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -250,6 +250,12 @@ export {
|
|
|
250
250
|
type CreditChecker,
|
|
251
251
|
} from "./utils/creditChecker";
|
|
252
252
|
|
|
253
|
+
export {
|
|
254
|
+
createAICreditHelpers,
|
|
255
|
+
type AICreditHelpersConfig,
|
|
256
|
+
type AICreditHelpers,
|
|
257
|
+
} from "./utils/aiCreditHelpers";
|
|
258
|
+
|
|
253
259
|
// =============================================================================
|
|
254
260
|
// REVENUECAT - Errors
|
|
255
261
|
// =============================================================================
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
export interface FirestoreTimestamp {
|
|
3
|
+
toDate: () => Date;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// Document structure when READING from Firestore
|
|
7
|
+
export interface UserCreditsDocumentRead {
|
|
8
|
+
textCredits: number;
|
|
9
|
+
imageCredits: number;
|
|
10
|
+
purchasedAt?: FirestoreTimestamp;
|
|
11
|
+
lastUpdatedAt?: FirestoreTimestamp;
|
|
12
|
+
lastPurchaseAt?: FirestoreTimestamp;
|
|
13
|
+
processedPurchases?: string[];
|
|
14
|
+
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
+
|
|
1
2
|
/**
|
|
2
3
|
* Credits Repository
|
|
3
4
|
*
|
|
4
5
|
* Firestore operations for user credits management.
|
|
5
6
|
* Extends BaseRepository from @umituz/react-native-firebase.
|
|
6
|
-
*
|
|
7
|
-
* Generic and reusable - accepts config from main app.
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
9
|
import {
|
|
@@ -12,8 +11,8 @@ import {
|
|
|
12
11
|
getDoc,
|
|
13
12
|
runTransaction,
|
|
14
13
|
serverTimestamp,
|
|
15
|
-
type FieldValue,
|
|
16
14
|
type Firestore,
|
|
15
|
+
type Transaction,
|
|
17
16
|
} from "firebase/firestore";
|
|
18
17
|
import { BaseRepository, getFirestore } from "@umituz/react-native-firebase";
|
|
19
18
|
import type {
|
|
@@ -22,20 +21,8 @@ import type {
|
|
|
22
21
|
CreditsResult,
|
|
23
22
|
DeductCreditsResult,
|
|
24
23
|
} from "../../domain/entities/Credits";
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
toDate: () => Date;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Document structure when READING from Firestore
|
|
31
|
-
interface UserCreditsDocumentRead {
|
|
32
|
-
textCredits: number;
|
|
33
|
-
imageCredits: number;
|
|
34
|
-
purchasedAt?: FirestoreTimestamp;
|
|
35
|
-
lastUpdatedAt?: FirestoreTimestamp;
|
|
36
|
-
lastPurchaseAt?: FirestoreTimestamp;
|
|
37
|
-
processedPurchases?: string[];
|
|
38
|
-
}
|
|
24
|
+
import type { UserCreditsDocumentRead } from "../models/UserCreditsDocument";
|
|
25
|
+
import { initializeCreditsTransaction } from "../services/CreditsInitializer";
|
|
39
26
|
|
|
40
27
|
export class CreditsRepository extends BaseRepository {
|
|
41
28
|
private config: CreditsConfig;
|
|
@@ -83,8 +70,7 @@ export class CreditsRepository extends BaseRepository {
|
|
|
83
70
|
return {
|
|
84
71
|
success: false,
|
|
85
72
|
error: {
|
|
86
|
-
message:
|
|
87
|
-
error instanceof Error ? error.message : "Failed to get credits",
|
|
73
|
+
message: error instanceof Error ? error.message : "Failed to get credits",
|
|
88
74
|
code: "FETCH_FAILED",
|
|
89
75
|
},
|
|
90
76
|
};
|
|
@@ -105,53 +91,12 @@ export class CreditsRepository extends BaseRepository {
|
|
|
105
91
|
|
|
106
92
|
try {
|
|
107
93
|
const creditsRef = this.getCreditsDocRef(db, userId);
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
let newImageCredits = this.config.imageCreditLimit;
|
|
115
|
-
let purchasedAt = now;
|
|
116
|
-
let processedPurchases: string[] = [];
|
|
117
|
-
|
|
118
|
-
if (creditsDoc.exists()) {
|
|
119
|
-
const existing = creditsDoc.data() as UserCreditsDocumentRead;
|
|
120
|
-
processedPurchases = existing.processedPurchases || [];
|
|
121
|
-
|
|
122
|
-
if (purchaseId && processedPurchases.includes(purchaseId)) {
|
|
123
|
-
return {
|
|
124
|
-
textCredits: existing.textCredits,
|
|
125
|
-
imageCredits: existing.imageCredits,
|
|
126
|
-
alreadyProcessed: true,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
newTextCredits =
|
|
131
|
-
(existing.textCredits || 0) + this.config.textCreditLimit;
|
|
132
|
-
newImageCredits =
|
|
133
|
-
(existing.imageCredits || 0) + this.config.imageCreditLimit;
|
|
134
|
-
// Keep existing purchasedAt if available, otherwise use server timestamp
|
|
135
|
-
if (existing.purchasedAt) {
|
|
136
|
-
purchasedAt = existing.purchasedAt as unknown as FieldValue;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (purchaseId) {
|
|
141
|
-
processedPurchases = [...processedPurchases, purchaseId].slice(-10);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
transaction.set(creditsRef, {
|
|
145
|
-
textCredits: newTextCredits,
|
|
146
|
-
imageCredits: newImageCredits,
|
|
147
|
-
purchasedAt,
|
|
148
|
-
lastUpdatedAt: now,
|
|
149
|
-
lastPurchaseAt: now,
|
|
150
|
-
processedPurchases,
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
return { textCredits: newTextCredits, imageCredits: newImageCredits };
|
|
154
|
-
});
|
|
94
|
+
const result = await initializeCreditsTransaction(
|
|
95
|
+
db,
|
|
96
|
+
creditsRef,
|
|
97
|
+
this.config,
|
|
98
|
+
purchaseId
|
|
99
|
+
);
|
|
155
100
|
|
|
156
101
|
return {
|
|
157
102
|
success: true,
|
|
@@ -166,10 +111,7 @@ export class CreditsRepository extends BaseRepository {
|
|
|
166
111
|
return {
|
|
167
112
|
success: false,
|
|
168
113
|
error: {
|
|
169
|
-
message:
|
|
170
|
-
error instanceof Error
|
|
171
|
-
? error.message
|
|
172
|
-
: "Failed to initialize credits",
|
|
114
|
+
message: error instanceof Error ? error.message : "Failed to initialize credits",
|
|
173
115
|
code: "INIT_FAILED",
|
|
174
116
|
},
|
|
175
117
|
};
|
|
@@ -192,62 +134,35 @@ export class CreditsRepository extends BaseRepository {
|
|
|
192
134
|
const creditsRef = this.getCreditsDocRef(db, userId);
|
|
193
135
|
const fieldName = creditType === "text" ? "textCredits" : "imageCredits";
|
|
194
136
|
|
|
195
|
-
const newCredits = await runTransaction(db, async (transaction) => {
|
|
137
|
+
const newCredits = await runTransaction(db, async (transaction: Transaction) => {
|
|
196
138
|
const creditsDoc = await transaction.get(creditsRef);
|
|
197
|
-
|
|
198
|
-
if (!creditsDoc.exists()) {
|
|
199
|
-
throw new Error("NO_CREDITS");
|
|
200
|
-
}
|
|
139
|
+
if (!creditsDoc.exists()) throw new Error("NO_CREDITS");
|
|
201
140
|
|
|
202
141
|
const currentCredits = creditsDoc.data()[fieldName] as number;
|
|
203
|
-
|
|
204
|
-
if (currentCredits <= 0) {
|
|
205
|
-
throw new Error("CREDITS_EXHAUSTED");
|
|
206
|
-
}
|
|
142
|
+
if (currentCredits <= 0) throw new Error("CREDITS_EXHAUSTED");
|
|
207
143
|
|
|
208
144
|
const updatedCredits = currentCredits - 1;
|
|
209
145
|
transaction.update(creditsRef, {
|
|
210
146
|
[fieldName]: updatedCredits,
|
|
211
147
|
lastUpdatedAt: serverTimestamp(),
|
|
212
148
|
});
|
|
213
|
-
|
|
214
149
|
return updatedCredits;
|
|
215
150
|
});
|
|
216
151
|
|
|
217
152
|
return { success: true, remainingCredits: newCredits };
|
|
218
153
|
} catch (error) {
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (errorMessage === "NO_CREDITS") {
|
|
223
|
-
return {
|
|
224
|
-
success: false,
|
|
225
|
-
error: { message: "No credits found", code: "NO_CREDITS" },
|
|
226
|
-
};
|
|
227
|
-
}
|
|
154
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
155
|
+
const code = msg === "NO_CREDITS" || msg === "CREDITS_EXHAUSTED" ? msg : "DEDUCT_FAILED";
|
|
156
|
+
const message = msg === "NO_CREDITS" ? "No credits found" : msg === "CREDITS_EXHAUSTED" ? "Credits exhausted" : msg;
|
|
228
157
|
|
|
229
|
-
|
|
230
|
-
return {
|
|
231
|
-
success: false,
|
|
232
|
-
error: { message: "Credits exhausted", code: "CREDITS_EXHAUSTED" },
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return {
|
|
237
|
-
success: false,
|
|
238
|
-
error: { message: errorMessage, code: "DEDUCT_FAILED" },
|
|
239
|
-
};
|
|
158
|
+
return { success: false, error: { message, code } };
|
|
240
159
|
}
|
|
241
160
|
}
|
|
242
161
|
|
|
243
162
|
async hasCredits(userId: string, creditType: CreditType): Promise<boolean> {
|
|
244
163
|
const result = await this.getCredits(userId);
|
|
245
|
-
if (!result.success || !result.data)
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const credits =
|
|
250
|
-
creditType === "text" ? result.data.textCredits : result.data.imageCredits;
|
|
164
|
+
if (!result.success || !result.data) return false;
|
|
165
|
+
const credits = creditType === "text" ? result.data.textCredits : result.data.imageCredits;
|
|
251
166
|
return credits > 0;
|
|
252
167
|
}
|
|
253
168
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
|
|
2
|
+
import {
|
|
3
|
+
runTransaction,
|
|
4
|
+
serverTimestamp,
|
|
5
|
+
type Firestore,
|
|
6
|
+
type FieldValue,
|
|
7
|
+
type Transaction,
|
|
8
|
+
type DocumentReference,
|
|
9
|
+
} from "firebase/firestore";
|
|
10
|
+
import type { CreditsConfig } from "../../domain/entities/Credits";
|
|
11
|
+
import type { UserCreditsDocumentRead } from "../models/UserCreditsDocument";
|
|
12
|
+
|
|
13
|
+
interface InitializationResult {
|
|
14
|
+
textCredits: number;
|
|
15
|
+
imageCredits: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function initializeCreditsTransaction(
|
|
19
|
+
db: Firestore,
|
|
20
|
+
creditsRef: DocumentReference,
|
|
21
|
+
config: CreditsConfig,
|
|
22
|
+
purchaseId?: string
|
|
23
|
+
): Promise<InitializationResult> {
|
|
24
|
+
return runTransaction(db, async (transaction: Transaction) => {
|
|
25
|
+
const creditsDoc = await transaction.get(creditsRef);
|
|
26
|
+
const now = serverTimestamp();
|
|
27
|
+
|
|
28
|
+
let newTextCredits = config.textCreditLimit;
|
|
29
|
+
let newImageCredits = config.imageCreditLimit;
|
|
30
|
+
let purchasedAt = now;
|
|
31
|
+
let processedPurchases: string[] = [];
|
|
32
|
+
|
|
33
|
+
if (creditsDoc.exists()) {
|
|
34
|
+
const existing = creditsDoc.data() as UserCreditsDocumentRead;
|
|
35
|
+
processedPurchases = existing.processedPurchases || [];
|
|
36
|
+
|
|
37
|
+
if (purchaseId && processedPurchases.includes(purchaseId)) {
|
|
38
|
+
return {
|
|
39
|
+
textCredits: existing.textCredits,
|
|
40
|
+
imageCredits: existing.imageCredits,
|
|
41
|
+
alreadyProcessed: true,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
newTextCredits = (existing.textCredits || 0) + config.textCreditLimit;
|
|
46
|
+
newImageCredits = (existing.imageCredits || 0) + config.imageCreditLimit;
|
|
47
|
+
if (existing.purchasedAt) {
|
|
48
|
+
purchasedAt = existing.purchasedAt as unknown as FieldValue;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (purchaseId) {
|
|
53
|
+
processedPurchases = [...processedPurchases, purchaseId].slice(-10);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
transaction.set(creditsRef, {
|
|
57
|
+
textCredits: newTextCredits,
|
|
58
|
+
imageCredits: newImageCredits,
|
|
59
|
+
purchasedAt,
|
|
60
|
+
lastUpdatedAt: now,
|
|
61
|
+
lastPurchaseAt: now,
|
|
62
|
+
processedPurchases,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return { textCredits: newTextCredits, imageCredits: newImageCredits };
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Credit Helpers
|
|
3
|
+
*
|
|
4
|
+
* Common patterns for AI generation apps to handle credits.
|
|
5
|
+
* Provides ready-to-use functions for credit checking and deduction.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { createAICreditHelpers } from '@umituz/react-native-subscription';
|
|
9
|
+
*
|
|
10
|
+
* const helpers = createAICreditHelpers({
|
|
11
|
+
* repository,
|
|
12
|
+
* imageGenerationTypes: ['future_image', 'santa_transform'],
|
|
13
|
+
* onCreditDeducted: (userId) => invalidateCache(userId)
|
|
14
|
+
* });
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { CreditType } from "../domain/entities/Credits";
|
|
18
|
+
import type { CreditsRepository } from "../infrastructure/repositories/CreditsRepository";
|
|
19
|
+
import { createCreditChecker } from "./creditChecker";
|
|
20
|
+
|
|
21
|
+
export interface AICreditHelpersConfig {
|
|
22
|
+
/**
|
|
23
|
+
* Credits repository instance
|
|
24
|
+
*/
|
|
25
|
+
repository: CreditsRepository;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* List of operation types that should use "image" credits.
|
|
29
|
+
* All other types will use "text" credits.
|
|
30
|
+
* @example ['future_image', 'santa_transform', 'photo_generation']
|
|
31
|
+
*/
|
|
32
|
+
imageGenerationTypes: string[];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Optional callback called after successful credit deduction.
|
|
36
|
+
* Use this to invalidate TanStack Query cache or trigger UI updates.
|
|
37
|
+
*/
|
|
38
|
+
onCreditDeducted?: (userId: string, creditType: CreditType) => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface AICreditHelpers {
|
|
42
|
+
/**
|
|
43
|
+
* Check if user has credits for a specific generation type
|
|
44
|
+
* @param userId - User ID
|
|
45
|
+
* @param generationType - Type of generation (e.g., 'future_image', 'text_summary')
|
|
46
|
+
* @returns boolean indicating if credits are available
|
|
47
|
+
*/
|
|
48
|
+
checkCreditsForGeneration: (
|
|
49
|
+
userId: string | undefined,
|
|
50
|
+
generationType: string
|
|
51
|
+
) => Promise<boolean>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Deduct credits after successful generation
|
|
55
|
+
* @param userId - User ID
|
|
56
|
+
* @param generationType - Type of generation that was performed
|
|
57
|
+
*/
|
|
58
|
+
deductCreditsForGeneration: (
|
|
59
|
+
userId: string | undefined,
|
|
60
|
+
generationType: string
|
|
61
|
+
) => Promise<void>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get credit type for a generation type (useful for UI display)
|
|
65
|
+
* @param generationType - Type of generation
|
|
66
|
+
* @returns "image" or "text"
|
|
67
|
+
*/
|
|
68
|
+
getCreditType: (generationType: string) => CreditType;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Creates AI-specific credit helper functions
|
|
73
|
+
*/
|
|
74
|
+
export function createAICreditHelpers(
|
|
75
|
+
config: AICreditHelpersConfig
|
|
76
|
+
): AICreditHelpers {
|
|
77
|
+
const { repository, imageGenerationTypes, onCreditDeducted } = config;
|
|
78
|
+
|
|
79
|
+
// Map generation type to credit type
|
|
80
|
+
const getCreditType = (generationType: string): CreditType => {
|
|
81
|
+
return imageGenerationTypes.includes(generationType) ? "image" : "text";
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Create credit checker with the mapping
|
|
85
|
+
const checker = createCreditChecker({
|
|
86
|
+
repository,
|
|
87
|
+
getCreditType,
|
|
88
|
+
onCreditDeducted,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Check if credits are available for generation
|
|
92
|
+
const checkCreditsForGeneration = async (
|
|
93
|
+
userId: string | undefined,
|
|
94
|
+
generationType: string
|
|
95
|
+
): Promise<boolean> => {
|
|
96
|
+
const result = await checker.checkCreditsAvailable(userId, generationType);
|
|
97
|
+
return result.success;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Deduct credits after successful generation
|
|
101
|
+
const deductCreditsForGeneration = async (
|
|
102
|
+
userId: string | undefined,
|
|
103
|
+
generationType: string
|
|
104
|
+
): Promise<void> => {
|
|
105
|
+
const creditType = getCreditType(generationType);
|
|
106
|
+
await checker.deductCreditsAfterSuccess(userId, creditType);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
checkCreditsForGeneration,
|
|
111
|
+
deductCreditsForGeneration,
|
|
112
|
+
getCreditType,
|
|
113
|
+
};
|
|
114
|
+
}
|