@umituz/react-native-subscription 2.26.7 → 2.26.9
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 +1 -1
- package/src/domain/entities/Credits.ts +4 -0
- package/src/domain/entities/SubscriptionStatus.ts +1 -0
- package/src/infrastructure/repositories/CreditsRepository.ts +66 -0
- package/src/presentation/components/details/PremiumStatusBadge.tsx +5 -0
- package/src/presentation/hooks/useCredits.ts +49 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.26.
|
|
3
|
+
"version": "2.26.9",
|
|
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",
|
|
@@ -74,6 +74,10 @@ export interface CreditsConfig {
|
|
|
74
74
|
creditPackageAmounts?: Record<string, number>;
|
|
75
75
|
/** Credit allocations for different subscription types (weekly, monthly, yearly) */
|
|
76
76
|
packageAllocations?: PackageAllocationMap;
|
|
77
|
+
/** Free credits given to new users on registration (default: 0) */
|
|
78
|
+
freeCredits?: number;
|
|
79
|
+
/** Whether to auto-initialize free credits when user has no credits document (default: true if freeCredits > 0) */
|
|
80
|
+
autoInitializeFreeCredits?: boolean;
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
export interface CreditsResult<T = UserCredits> {
|
|
@@ -133,6 +133,72 @@ export class CreditsRepository extends BaseRepository {
|
|
|
133
133
|
return !!(res.success && res.data && res.data.credits >= cost);
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Initialize free credits for new users
|
|
138
|
+
* Creates a credits document with freeCredits amount (no subscription)
|
|
139
|
+
*/
|
|
140
|
+
async initializeFreeCredits(userId: string): Promise<CreditsResult> {
|
|
141
|
+
const db = getFirestore();
|
|
142
|
+
if (!db) return { success: false, error: { message: "No DB", code: "INIT_ERR" } };
|
|
143
|
+
|
|
144
|
+
const freeCredits = this.config.freeCredits ?? 0;
|
|
145
|
+
if (freeCredits <= 0) {
|
|
146
|
+
return { success: false, error: { message: "Free credits not configured", code: "NO_FREE_CREDITS" } };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const ref = this.getRef(db, userId);
|
|
151
|
+
const snap = await getDoc(ref);
|
|
152
|
+
|
|
153
|
+
// Don't overwrite if document already exists
|
|
154
|
+
if (snap.exists()) {
|
|
155
|
+
if (__DEV__) console.log("[CreditsRepository] Credits document already exists, skipping free credits init");
|
|
156
|
+
const existing = snap.data() as UserCreditsDocumentRead;
|
|
157
|
+
return { success: true, data: CreditsMapper.toEntity(existing) };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Create new document with free credits
|
|
161
|
+
const { setDoc } = await import("firebase/firestore");
|
|
162
|
+
const now = serverTimestamp();
|
|
163
|
+
|
|
164
|
+
const creditsData = {
|
|
165
|
+
// Not premium - just free credits
|
|
166
|
+
isPremium: false,
|
|
167
|
+
status: "free" as const,
|
|
168
|
+
|
|
169
|
+
// Free credits
|
|
170
|
+
credits: freeCredits,
|
|
171
|
+
creditLimit: freeCredits,
|
|
172
|
+
isFreeCredits: true,
|
|
173
|
+
|
|
174
|
+
// Dates
|
|
175
|
+
createdAt: now,
|
|
176
|
+
lastUpdatedAt: now,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
await setDoc(ref, creditsData);
|
|
180
|
+
|
|
181
|
+
if (__DEV__) console.log("[CreditsRepository] Initialized free credits:", { userId: userId.slice(0, 8), credits: freeCredits });
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
success: true,
|
|
185
|
+
data: {
|
|
186
|
+
isPremium: false,
|
|
187
|
+
status: "free",
|
|
188
|
+
credits: freeCredits,
|
|
189
|
+
creditLimit: freeCredits,
|
|
190
|
+
purchasedAt: null,
|
|
191
|
+
expirationDate: null,
|
|
192
|
+
lastUpdatedAt: null,
|
|
193
|
+
willRenew: false,
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
} catch (e: any) {
|
|
197
|
+
if (__DEV__) console.error("[CreditsRepository] Free credits init error:", e.message);
|
|
198
|
+
return { success: false, error: { message: e.message, code: "INIT_ERR" } };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
136
202
|
/** Sync expired subscription status to Firestore (background) */
|
|
137
203
|
async syncExpiredStatus(userId: string): Promise<void> {
|
|
138
204
|
const db = getFirestore();
|
|
@@ -20,6 +20,8 @@ export interface PremiumStatusBadgeProps {
|
|
|
20
20
|
trialLabel?: string;
|
|
21
21
|
/** Label for trial_canceled status (defaults to canceledLabel if not provided) */
|
|
22
22
|
trialCanceledLabel?: string;
|
|
23
|
+
/** Label for free credits status (defaults to noneLabel if not provided) */
|
|
24
|
+
freeLabel?: string;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export const PremiumStatusBadge: React.FC<PremiumStatusBadgeProps> = ({
|
|
@@ -30,6 +32,7 @@ export const PremiumStatusBadge: React.FC<PremiumStatusBadgeProps> = ({
|
|
|
30
32
|
canceledLabel,
|
|
31
33
|
trialLabel,
|
|
32
34
|
trialCanceledLabel,
|
|
35
|
+
freeLabel,
|
|
33
36
|
}) => {
|
|
34
37
|
const tokens = useAppDesignTokens();
|
|
35
38
|
|
|
@@ -40,6 +43,7 @@ export const PremiumStatusBadge: React.FC<PremiumStatusBadgeProps> = ({
|
|
|
40
43
|
expired: expiredLabel,
|
|
41
44
|
none: noneLabel,
|
|
42
45
|
canceled: canceledLabel,
|
|
46
|
+
free: freeLabel ?? noneLabel,
|
|
43
47
|
};
|
|
44
48
|
|
|
45
49
|
const backgroundColor = useMemo(() => {
|
|
@@ -50,6 +54,7 @@ export const PremiumStatusBadge: React.FC<PremiumStatusBadgeProps> = ({
|
|
|
50
54
|
expired: tokens.colors.error,
|
|
51
55
|
none: tokens.colors.textTertiary,
|
|
52
56
|
canceled: tokens.colors.warning,
|
|
57
|
+
free: tokens.colors.info ?? tokens.colors.primary, // Blue for free credits
|
|
53
58
|
};
|
|
54
59
|
return colors[status];
|
|
55
60
|
}, [status, tokens.colors]);
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* TanStack Query hook for fetching user credits.
|
|
5
5
|
* Generic and reusable - uses config from module-level provider.
|
|
6
|
+
* Auto-initializes free credits for new users if configured.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import { useQuery } from "@umituz/react-native-design-system";
|
|
9
|
-
import { useCallback, useMemo } from "react";
|
|
10
|
+
import { useCallback, useMemo, useRef, useEffect } from "react";
|
|
10
11
|
import type { UserCredits } from "../../domain/entities/Credits";
|
|
11
12
|
import {
|
|
12
13
|
getCreditsRepository,
|
|
@@ -65,7 +66,10 @@ export const useCredits = ({
|
|
|
65
66
|
|
|
66
67
|
const queryEnabled = enabled && !!userId && isConfigured;
|
|
67
68
|
|
|
68
|
-
|
|
69
|
+
// Track if free credits initialization has been attempted
|
|
70
|
+
const freeCreditsInitAttemptedRef = useRef<string | null>(null);
|
|
71
|
+
|
|
72
|
+
const { data, isLoading, error, refetch, isFetched } = useQuery({
|
|
69
73
|
queryKey: creditsQueryKeys.user(userId ?? ""),
|
|
70
74
|
queryFn: async () => {
|
|
71
75
|
if (!userId || !isConfigured) {
|
|
@@ -98,6 +102,49 @@ export const useCredits = ({
|
|
|
98
102
|
|
|
99
103
|
const credits = data ?? null;
|
|
100
104
|
|
|
105
|
+
// Auto-initialize free credits for new users
|
|
106
|
+
const freeCredits = config.freeCredits ?? 0;
|
|
107
|
+
const autoInit = config.autoInitializeFreeCredits !== false && freeCredits > 0;
|
|
108
|
+
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
// Only run if:
|
|
111
|
+
// 1. Query has completed (isFetched)
|
|
112
|
+
// 2. User is authenticated
|
|
113
|
+
// 3. No credits data exists
|
|
114
|
+
// 4. Free credits configured
|
|
115
|
+
// 5. Auto-init enabled
|
|
116
|
+
// 6. Haven't already attempted for this user
|
|
117
|
+
if (
|
|
118
|
+
isFetched &&
|
|
119
|
+
userId &&
|
|
120
|
+
isConfigured &&
|
|
121
|
+
!credits &&
|
|
122
|
+
autoInit &&
|
|
123
|
+
freeCreditsInitAttemptedRef.current !== userId
|
|
124
|
+
) {
|
|
125
|
+
freeCreditsInitAttemptedRef.current = userId;
|
|
126
|
+
|
|
127
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
128
|
+
console.log("[useCredits] Auto-initializing free credits for new user:", userId.slice(0, 8));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const repository = getCreditsRepository();
|
|
132
|
+
repository.initializeFreeCredits(userId).then((result) => {
|
|
133
|
+
if (result.success) {
|
|
134
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
135
|
+
console.log("[useCredits] Free credits initialized:", result.data?.credits);
|
|
136
|
+
}
|
|
137
|
+
// Refetch to get the new credits
|
|
138
|
+
refetch();
|
|
139
|
+
} else {
|
|
140
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
141
|
+
console.warn("[useCredits] Free credits init failed:", result.error?.message);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}, [isFetched, userId, isConfigured, credits, autoInit, refetch]);
|
|
147
|
+
|
|
101
148
|
// Memoize derived values to prevent unnecessary re-renders
|
|
102
149
|
const derivedValues = useMemo(() => {
|
|
103
150
|
const has = (credits?.credits ?? 0) > 0;
|