@umituz/react-native-subscription 2.22.7 → 2.22.8
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 +21 -4
- package/src/infrastructure/mappers/CreditsMapper.ts +39 -18
- package/src/infrastructure/models/UserCreditsDocument.ts +23 -8
- package/src/infrastructure/repositories/CreditsRepository.ts +17 -3
- package/src/infrastructure/services/CreditsInitializer.ts +36 -2
- package/src/infrastructure/services/SubscriptionInitializer.ts +2 -0
- package/src/presentation/hooks/useSubscriptionSettingsConfig.ts +20 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.22.
|
|
3
|
+
"version": "2.22.8",
|
|
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",
|
|
@@ -19,17 +19,34 @@ export type PurchaseSource =
|
|
|
19
19
|
|
|
20
20
|
export type PurchaseType = "initial" | "renewal" | "upgrade" | "downgrade";
|
|
21
21
|
|
|
22
|
+
export type SubscriptionStatus = "active" | "expired" | "canceled" | "free";
|
|
23
|
+
|
|
24
|
+
/** Single Source of Truth for user subscription + credits data */
|
|
22
25
|
export interface UserCredits {
|
|
26
|
+
// Core subscription
|
|
27
|
+
isPremium: boolean;
|
|
28
|
+
status: SubscriptionStatus;
|
|
29
|
+
|
|
30
|
+
// Dates
|
|
31
|
+
purchasedAt: Date | null;
|
|
32
|
+
expirationDate: Date | null;
|
|
33
|
+
lastUpdatedAt: Date | null;
|
|
34
|
+
|
|
35
|
+
// RevenueCat subscription details
|
|
36
|
+
willRenew: boolean;
|
|
37
|
+
productId?: string;
|
|
38
|
+
packageType?: "weekly" | "monthly" | "yearly" | "lifetime";
|
|
39
|
+
originalTransactionId?: string;
|
|
40
|
+
|
|
41
|
+
// Credits
|
|
23
42
|
credits: number;
|
|
24
|
-
packageType?: "weekly" | "monthly" | "yearly";
|
|
25
43
|
creditLimit?: number;
|
|
26
|
-
|
|
44
|
+
|
|
45
|
+
// Metadata
|
|
27
46
|
purchaseSource?: PurchaseSource;
|
|
28
47
|
purchaseType?: PurchaseType;
|
|
29
48
|
platform?: "ios" | "android";
|
|
30
49
|
appVersion?: string;
|
|
31
|
-
purchasedAt: Date | null;
|
|
32
|
-
lastUpdatedAt: Date | null;
|
|
33
50
|
}
|
|
34
51
|
|
|
35
52
|
export interface CreditAllocation {
|
|
@@ -1,27 +1,48 @@
|
|
|
1
|
-
import type { UserCredits } from "../../domain/entities/Credits";
|
|
1
|
+
import type { UserCredits, SubscriptionStatus } from "../../domain/entities/Credits";
|
|
2
2
|
import type { UserCreditsDocumentRead } from "../models/UserCreditsDocument";
|
|
3
3
|
|
|
4
|
+
/** Maps Firestore document to domain entity */
|
|
4
5
|
export class CreditsMapper {
|
|
5
|
-
static toEntity(
|
|
6
|
+
static toEntity(doc: UserCreditsDocumentRead): UserCredits {
|
|
7
|
+
// Determine status from document or derive from isPremium/expirationDate
|
|
8
|
+
const status = doc.status ?? CreditsMapper.deriveStatus(doc);
|
|
9
|
+
|
|
6
10
|
return {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
// Core subscription
|
|
12
|
+
isPremium: doc.isPremium ?? false,
|
|
13
|
+
status,
|
|
14
|
+
|
|
15
|
+
// Dates
|
|
16
|
+
purchasedAt: doc.purchasedAt?.toDate?.() ?? null,
|
|
17
|
+
expirationDate: doc.expirationDate?.toDate?.() ?? null,
|
|
18
|
+
lastUpdatedAt: doc.lastUpdatedAt?.toDate?.() ?? null,
|
|
19
|
+
|
|
20
|
+
// RevenueCat details
|
|
21
|
+
willRenew: doc.willRenew ?? false,
|
|
22
|
+
productId: doc.productId,
|
|
23
|
+
packageType: doc.packageType,
|
|
24
|
+
originalTransactionId: doc.originalTransactionId,
|
|
25
|
+
|
|
26
|
+
// Credits
|
|
27
|
+
credits: doc.credits,
|
|
28
|
+
creditLimit: doc.creditLimit,
|
|
29
|
+
|
|
30
|
+
// Metadata
|
|
31
|
+
purchaseSource: doc.purchaseSource,
|
|
32
|
+
purchaseType: doc.purchaseType,
|
|
33
|
+
platform: doc.platform,
|
|
34
|
+
appVersion: doc.appVersion,
|
|
17
35
|
};
|
|
18
36
|
}
|
|
19
37
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
38
|
+
/** Derive status from isPremium and expirationDate for backward compatibility */
|
|
39
|
+
private static deriveStatus(doc: UserCreditsDocumentRead): SubscriptionStatus {
|
|
40
|
+
if (!doc.isPremium && !doc.expirationDate) return "free";
|
|
41
|
+
if (doc.isPremium) return "active";
|
|
42
|
+
if (doc.expirationDate) {
|
|
43
|
+
const expDate = doc.expirationDate.toDate?.();
|
|
44
|
+
if (expDate && expDate < new Date()) return "expired";
|
|
45
|
+
}
|
|
46
|
+
return "free";
|
|
26
47
|
}
|
|
27
48
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
export interface FirestoreTimestamp {
|
|
3
2
|
toDate: () => Date;
|
|
4
3
|
}
|
|
@@ -13,9 +12,11 @@ export type PurchaseSource =
|
|
|
13
12
|
|
|
14
13
|
export type PurchaseType = "initial" | "renewal" | "upgrade" | "downgrade";
|
|
15
14
|
|
|
15
|
+
export type SubscriptionDocStatus = "active" | "expired" | "canceled" | "free";
|
|
16
|
+
|
|
16
17
|
export interface PurchaseMetadata {
|
|
17
18
|
productId: string;
|
|
18
|
-
packageType: "weekly" | "monthly" | "yearly";
|
|
19
|
+
packageType: "weekly" | "monthly" | "yearly" | "lifetime";
|
|
19
20
|
creditLimit: number;
|
|
20
21
|
source: PurchaseSource;
|
|
21
22
|
type: PurchaseType;
|
|
@@ -24,19 +25,33 @@ export interface PurchaseMetadata {
|
|
|
24
25
|
timestamp: FirestoreTimestamp;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
/** Single Source of Truth for user subscription data */
|
|
28
29
|
export interface UserCreditsDocumentRead {
|
|
30
|
+
// Core subscription status
|
|
31
|
+
isPremium?: boolean;
|
|
32
|
+
status?: SubscriptionDocStatus;
|
|
33
|
+
|
|
34
|
+
// Dates (all from RevenueCat)
|
|
35
|
+
purchasedAt?: FirestoreTimestamp;
|
|
36
|
+
expirationDate?: FirestoreTimestamp;
|
|
37
|
+
lastUpdatedAt?: FirestoreTimestamp;
|
|
38
|
+
lastPurchaseAt?: FirestoreTimestamp;
|
|
39
|
+
|
|
40
|
+
// RevenueCat subscription details
|
|
41
|
+
willRenew?: boolean;
|
|
42
|
+
productId?: string;
|
|
43
|
+
packageType?: "weekly" | "monthly" | "yearly" | "lifetime";
|
|
44
|
+
originalTransactionId?: string;
|
|
45
|
+
|
|
46
|
+
// Credits
|
|
29
47
|
credits: number;
|
|
30
|
-
packageType?: "weekly" | "monthly" | "yearly";
|
|
31
48
|
creditLimit?: number;
|
|
32
|
-
|
|
49
|
+
|
|
50
|
+
// Metadata
|
|
33
51
|
purchaseSource?: PurchaseSource;
|
|
34
52
|
purchaseType?: PurchaseType;
|
|
35
53
|
platform?: "ios" | "android";
|
|
36
54
|
appVersion?: string;
|
|
37
|
-
purchasedAt?: FirestoreTimestamp;
|
|
38
|
-
lastUpdatedAt?: FirestoreTimestamp;
|
|
39
|
-
lastPurchaseAt?: FirestoreTimestamp;
|
|
40
55
|
processedPurchases?: string[];
|
|
41
56
|
purchaseHistory?: PurchaseMetadata[];
|
|
42
57
|
}
|
|
@@ -14,12 +14,20 @@ import { getCreditAllocation } from "../../utils/creditMapper";
|
|
|
14
14
|
|
|
15
15
|
import { CreditsMapper } from "../mappers/CreditsMapper";
|
|
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
|
+
}
|
|
24
|
+
|
|
17
25
|
export class CreditsRepository extends BaseRepository {
|
|
18
26
|
constructor(private config: CreditsConfig) { super(); }
|
|
19
27
|
|
|
20
28
|
private getRef(db: Firestore, userId: string) {
|
|
21
|
-
return this.config.useUserSubcollection
|
|
22
|
-
? doc(db, "users", userId, "credits", "balance")
|
|
29
|
+
return this.config.useUserSubcollection
|
|
30
|
+
? doc(db, "users", userId, "credits", "balance")
|
|
23
31
|
: doc(db, this.config.collectionName, userId);
|
|
24
32
|
}
|
|
25
33
|
|
|
@@ -51,7 +59,8 @@ export class CreditsRepository extends BaseRepository {
|
|
|
51
59
|
userId: string,
|
|
52
60
|
purchaseId?: string,
|
|
53
61
|
productId?: string,
|
|
54
|
-
source?: PurchaseSource
|
|
62
|
+
source?: PurchaseSource,
|
|
63
|
+
revenueCatData?: RevenueCatData
|
|
55
64
|
): Promise<CreditsResult> {
|
|
56
65
|
const db = getFirestore();
|
|
57
66
|
if (!db) return { success: false, error: { message: "No DB", code: "INIT_ERR" } };
|
|
@@ -70,6 +79,11 @@ export class CreditsRepository extends BaseRepository {
|
|
|
70
79
|
const metadata: InitializeCreditsMetadata = {
|
|
71
80
|
productId,
|
|
72
81
|
source,
|
|
82
|
+
// RevenueCat data for Single Source of Truth
|
|
83
|
+
expirationDate: revenueCatData?.expirationDate,
|
|
84
|
+
willRenew: revenueCatData?.willRenew,
|
|
85
|
+
originalTransactionId: revenueCatData?.originalTransactionId,
|
|
86
|
+
isPremium: revenueCatData?.isPremium,
|
|
73
87
|
};
|
|
74
88
|
|
|
75
89
|
const res = await initializeCreditsTransaction(
|
|
@@ -3,6 +3,7 @@ import Constants from "expo-constants";
|
|
|
3
3
|
import {
|
|
4
4
|
runTransaction,
|
|
5
5
|
serverTimestamp,
|
|
6
|
+
Timestamp,
|
|
6
7
|
type Firestore,
|
|
7
8
|
type FieldValue,
|
|
8
9
|
type Transaction,
|
|
@@ -14,6 +15,7 @@ import type {
|
|
|
14
15
|
PurchaseSource,
|
|
15
16
|
PurchaseType,
|
|
16
17
|
PurchaseMetadata,
|
|
18
|
+
SubscriptionDocStatus,
|
|
17
19
|
} from "../models/UserCreditsDocument";
|
|
18
20
|
import { detectPackageType } from "../../utils/packageTypeDetector";
|
|
19
21
|
import { getCreditAllocation } from "../../utils/creditMapper";
|
|
@@ -22,10 +24,16 @@ interface InitializationResult {
|
|
|
22
24
|
credits: number;
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
/** RevenueCat data to save to Firestore (Single Source of Truth) */
|
|
25
28
|
export interface InitializeCreditsMetadata {
|
|
26
29
|
productId?: string;
|
|
27
30
|
source?: PurchaseSource;
|
|
28
31
|
type?: PurchaseType;
|
|
32
|
+
// RevenueCat subscription data
|
|
33
|
+
expirationDate?: string | null;
|
|
34
|
+
willRenew?: boolean;
|
|
35
|
+
originalTransactionId?: string;
|
|
36
|
+
isPremium?: boolean;
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
export async function initializeCreditsTransaction(
|
|
@@ -114,17 +122,41 @@ export async function initializeCreditsTransaction(
|
|
|
114
122
|
? [...(existing?.purchaseHistory || []), purchaseMetadata].slice(-10)
|
|
115
123
|
: existing?.purchaseHistory;
|
|
116
124
|
|
|
117
|
-
//
|
|
125
|
+
// Determine subscription status
|
|
126
|
+
const isPremium = metadata?.isPremium ?? true;
|
|
127
|
+
const status: SubscriptionDocStatus = isPremium ? "active" : "expired";
|
|
128
|
+
|
|
129
|
+
// Build credits data (Single Source of Truth)
|
|
118
130
|
const creditsData: Record<string, unknown> = {
|
|
131
|
+
// Core subscription
|
|
132
|
+
isPremium,
|
|
133
|
+
status,
|
|
134
|
+
|
|
135
|
+
// Credits
|
|
119
136
|
credits: newCredits,
|
|
120
137
|
creditLimit,
|
|
138
|
+
|
|
139
|
+
// Dates
|
|
121
140
|
purchasedAt,
|
|
122
141
|
lastUpdatedAt: now,
|
|
123
142
|
lastPurchaseAt: now,
|
|
143
|
+
|
|
144
|
+
// Tracking
|
|
124
145
|
processedPurchases,
|
|
125
146
|
};
|
|
126
147
|
|
|
127
|
-
//
|
|
148
|
+
// RevenueCat subscription data
|
|
149
|
+
if (metadata?.expirationDate) {
|
|
150
|
+
creditsData.expirationDate = Timestamp.fromDate(new Date(metadata.expirationDate));
|
|
151
|
+
}
|
|
152
|
+
if (metadata?.willRenew !== undefined) {
|
|
153
|
+
creditsData.willRenew = metadata.willRenew;
|
|
154
|
+
}
|
|
155
|
+
if (metadata?.originalTransactionId) {
|
|
156
|
+
creditsData.originalTransactionId = metadata.originalTransactionId;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Package info
|
|
128
160
|
if (packageType && packageType !== "unknown") {
|
|
129
161
|
creditsData.packageType = packageType;
|
|
130
162
|
}
|
|
@@ -133,6 +165,8 @@ export async function initializeCreditsTransaction(
|
|
|
133
165
|
creditsData.platform = platform;
|
|
134
166
|
creditsData.appVersion = appVersion;
|
|
135
167
|
}
|
|
168
|
+
|
|
169
|
+
// Purchase metadata
|
|
136
170
|
if (metadata?.source) {
|
|
137
171
|
creditsData.purchaseSource = metadata.source;
|
|
138
172
|
}
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Platform } from "react-native";
|
|
6
|
+
import type { CustomerInfo } from "react-native-purchases";
|
|
6
7
|
import type { CreditsConfig } from "../../domain/entities/Credits";
|
|
7
8
|
import { configureCreditsRepository, getCreditsRepository } from "../repositories/CreditsRepositoryProvider";
|
|
8
9
|
import { SubscriptionManager } from "../../revenuecat/infrastructure/managers/SubscriptionManager";
|
|
9
10
|
import { configureAuthProvider } from "../../presentation/hooks/useAuthAwarePurchase";
|
|
11
|
+
import type { RevenueCatData } from "../repositories/CreditsRepository";
|
|
10
12
|
|
|
11
13
|
export interface FirebaseAuthLike {
|
|
12
14
|
currentUser: { uid: string; isAnonymous: boolean } | null;
|
|
@@ -61,11 +61,11 @@ export const useSubscriptionSettingsConfig = (
|
|
|
61
61
|
|
|
62
62
|
// RevenueCat entitlement info - dynamically using configured entitlementId
|
|
63
63
|
const entitlementId = SubscriptionManager.getEntitlementId() || "premium";
|
|
64
|
-
const
|
|
64
|
+
const activeEntitlement = customerInfo?.entitlements.active[entitlementId];
|
|
65
|
+
const allEntitlement = customerInfo?.entitlements.all[entitlementId];
|
|
65
66
|
|
|
66
|
-
// Premium status:
|
|
67
|
-
|
|
68
|
-
const isPremium = !!premiumEntitlement || subscriptionActive;
|
|
67
|
+
// Premium status: only active entitlements count as premium
|
|
68
|
+
const isPremium = !!activeEntitlement || subscriptionActive;
|
|
69
69
|
|
|
70
70
|
const dynamicCreditLimit = useMemo(() => {
|
|
71
71
|
const config = getCreditsConfig();
|
|
@@ -76,8 +76,8 @@ export const useSubscriptionSettingsConfig = (
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
// 2. FALLBACK: RevenueCat'ten detect et
|
|
79
|
-
if (
|
|
80
|
-
const packageType = detectPackageType(
|
|
79
|
+
if (activeEntitlement?.productIdentifier) {
|
|
80
|
+
const packageType = detectPackageType(activeEntitlement.productIdentifier);
|
|
81
81
|
const allocation = getCreditAllocation(packageType, config.packageAllocations);
|
|
82
82
|
if (allocation !== null) return allocation;
|
|
83
83
|
}
|
|
@@ -92,19 +92,19 @@ export const useSubscriptionSettingsConfig = (
|
|
|
92
92
|
|
|
93
93
|
// 4. FINAL FALLBACK: Config'den al
|
|
94
94
|
return creditLimit ?? config.creditLimit;
|
|
95
|
-
}, [credits?.creditLimit, credits?.credits,
|
|
96
|
-
|
|
97
|
-
// Get expiration date
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const willRenew =
|
|
95
|
+
}, [credits?.creditLimit, credits?.credits, activeEntitlement?.productIdentifier, creditLimit]);
|
|
96
|
+
|
|
97
|
+
// Get expiration date with fallback chain (supports expired subscriptions)
|
|
98
|
+
// 1. Active entitlement (current subscription)
|
|
99
|
+
// 2. All entitlements (includes expired subscriptions)
|
|
100
|
+
// 3. latestExpirationDate from CustomerInfo
|
|
101
|
+
// 4. Status from Firestore
|
|
102
|
+
const expiresAtIso = activeEntitlement?.expirationDate
|
|
103
|
+
?? allEntitlement?.expirationDate
|
|
104
|
+
?? customerInfo?.latestExpirationDate
|
|
105
|
+
?? (statusExpirationDate ? statusExpirationDate.toISOString() : null);
|
|
106
|
+
|
|
107
|
+
const willRenew = activeEntitlement?.willRenew || false;
|
|
108
108
|
const purchasedAtIso = convertPurchasedAt(credits?.purchasedAt);
|
|
109
109
|
|
|
110
110
|
// Formatted dates
|
|
@@ -135,7 +135,7 @@ export const useSubscriptionSettingsConfig = (
|
|
|
135
135
|
showHeader: isPremium || hasCredits,
|
|
136
136
|
showCredits: hasCredits,
|
|
137
137
|
showUpgradePrompt: !isPremium && !hasCredits && !!upgradePrompt,
|
|
138
|
-
showExpirationDate: isPremium && !!expiresAtIso,
|
|
138
|
+
showExpirationDate: (isPremium || hasCredits) && !!expiresAtIso,
|
|
139
139
|
}), [isPremium, hasCredits, upgradePrompt, expiresAtIso]);
|
|
140
140
|
|
|
141
141
|
// Build config
|
|
@@ -197,7 +197,5 @@ export const useSubscriptionSettingsConfig = (
|
|
|
197
197
|
]
|
|
198
198
|
);
|
|
199
199
|
|
|
200
|
-
|
|
201
|
-
|
|
202
200
|
return config;
|
|
203
201
|
};
|