@umituz/react-native-subscription 2.37.109 → 2.37.111
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/domains/credits/application/CreditsInitializer.ts +5 -5
- package/src/domains/credits/application/creditDocumentHelpers.ts +1 -1
- package/src/domains/credits/application/creditOperationUtils.ts +1 -1
- package/src/domains/credits/core/Credits.ts +1 -1
- package/src/domains/credits/core/CreditsMapper.ts +1 -1
- package/src/domains/credits/core/UserCreditsDocument.ts +1 -1
- package/src/domains/credits/infrastructure/CreditsRepository.ts +5 -4
- package/src/domains/credits/infrastructure/operations/CreditsInitializer.ts +1 -1
- package/src/domains/credits/infrastructure/operations/CreditsWriter.ts +7 -18
- package/src/domains/revenuecat/core/types/RevenueCatData.ts +4 -10
- package/src/domains/revenuecat/infrastructure/services/userSwitchHandler.ts +6 -2
- package/src/domains/subscription/application/SubscriptionInitializerTypes.ts +3 -11
- package/src/domains/subscription/application/SubscriptionSyncProcessor.ts +9 -9
- package/src/domains/subscription/application/SubscriptionSyncUtils.ts +6 -6
- package/src/domains/subscription/application/syncIdGenerators.ts +6 -6
- package/src/domains/subscription/core/SubscriptionEvents.ts +4 -7
- package/src/domains/subscription/core/SubscriptionStatus.ts +4 -4
- package/src/domains/subscription/core/types/CreditInfo.ts +10 -0
- package/src/domains/subscription/core/types/PremiumStatus.ts +21 -0
- package/src/domains/subscription/core/types/SubscriptionMetadata.ts +18 -0
- package/src/domains/subscription/core/types/index.ts +3 -0
- package/src/domains/subscription/infrastructure/handlers/PurchaseStatusResolver.ts +2 -15
- package/src/domains/subscription/infrastructure/managers/SubscriptionManager.types.ts +1 -1
- package/src/domains/subscription/infrastructure/utils/PremiumStatusSyncer.ts +6 -2
- package/src/domains/subscription/presentation/components/details/PremiumDetailsCardTypes.ts +2 -6
- package/src/domains/subscription/presentation/screens/SubscriptionDetailScreen.types.ts +3 -7
- package/src/domains/subscription/presentation/useSubscriptionStatus.types.ts +3 -14
- package/src/index.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.37.
|
|
3
|
+
"version": "2.37.111",
|
|
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",
|
|
@@ -41,14 +41,14 @@ export async function initializeCreditsTransaction(
|
|
|
41
41
|
|
|
42
42
|
// Global cross-user deduplication: prevent the same Apple/Google transaction
|
|
43
43
|
// from allocating credits under multiple Firebase UIDs.
|
|
44
|
-
if (metadata.
|
|
45
|
-
const globalRef = doc(_db, GLOBAL_TRANSACTION_COLLECTION, metadata.
|
|
44
|
+
if (metadata.storeTransactionId) {
|
|
45
|
+
const globalRef = doc(_db, GLOBAL_TRANSACTION_COLLECTION, metadata.storeTransactionId);
|
|
46
46
|
const globalDoc = await transaction.get(globalRef);
|
|
47
47
|
if (globalDoc.exists()) {
|
|
48
48
|
const globalData = globalDoc.data();
|
|
49
49
|
if (globalData?.ownerUserId && globalData.ownerUserId !== userId) {
|
|
50
50
|
console.warn(
|
|
51
|
-
`[CreditsInitializer] Transaction ${metadata.
|
|
51
|
+
`[CreditsInitializer] Transaction ${metadata.storeTransactionId} already processed by user ${globalData.ownerUserId}, skipping for ${userId}`
|
|
52
52
|
);
|
|
53
53
|
return {
|
|
54
54
|
credits: existingData.credits,
|
|
@@ -89,8 +89,8 @@ export async function initializeCreditsTransaction(
|
|
|
89
89
|
transaction.set(creditsRef, creditsData, { merge: true });
|
|
90
90
|
|
|
91
91
|
// Register transaction globally so other UIDs cannot claim the same purchase.
|
|
92
|
-
if (metadata.
|
|
93
|
-
const globalRef = doc(_db, GLOBAL_TRANSACTION_COLLECTION, metadata.
|
|
92
|
+
if (metadata.storeTransactionId) {
|
|
93
|
+
const globalRef = doc(_db, GLOBAL_TRANSACTION_COLLECTION, metadata.storeTransactionId);
|
|
94
94
|
transaction.set(globalRef, {
|
|
95
95
|
ownerUserId: userId,
|
|
96
96
|
purchaseId,
|
|
@@ -68,7 +68,7 @@ export function buildCreditsData({
|
|
|
68
68
|
...(isPurchaseOrRenewal && { lastPurchaseAt: serverTimestamp() }),
|
|
69
69
|
...(expirationTimestamp && { expirationDate: expirationTimestamp }),
|
|
70
70
|
...(metadata.willRenew !== undefined && { willRenew: metadata.willRenew }),
|
|
71
|
-
...(metadata.
|
|
71
|
+
...(metadata.storeTransactionId && { storeTransactionId: metadata.storeTransactionId }),
|
|
72
72
|
...(canceledAtTimestamp && { canceledAt: canceledAtTimestamp }),
|
|
73
73
|
...(billingIssueTimestamp && { billingIssueDetectedAt: billingIssueTimestamp }),
|
|
74
74
|
...(metadata.store && { store: metadata.store }),
|
|
@@ -19,7 +19,7 @@ export interface UserCredits {
|
|
|
19
19
|
willRenew: boolean | null;
|
|
20
20
|
productId: string | null;
|
|
21
21
|
packageType: PackageType | null;
|
|
22
|
-
|
|
22
|
+
storeTransactionId: string | null;
|
|
23
23
|
periodType: string | null;
|
|
24
24
|
credits: number;
|
|
25
25
|
creditLimit: number;
|
|
@@ -41,7 +41,7 @@ export function mapCreditsDocumentToEntity(doc: UserCreditsDocumentRead): UserCr
|
|
|
41
41
|
willRenew: doc.willRenew,
|
|
42
42
|
productId: doc.productId,
|
|
43
43
|
packageType: doc.packageType,
|
|
44
|
-
|
|
44
|
+
storeTransactionId: doc.storeTransactionId,
|
|
45
45
|
periodType,
|
|
46
46
|
credits: doc.credits,
|
|
47
47
|
creditLimit: doc.creditLimit,
|
|
@@ -42,7 +42,7 @@ export interface UserCreditsDocumentRead {
|
|
|
42
42
|
willRenew: boolean | null;
|
|
43
43
|
productId: string | null;
|
|
44
44
|
packageType: PackageType | null;
|
|
45
|
-
|
|
45
|
+
storeTransactionId: string | null;
|
|
46
46
|
store: Store | null;
|
|
47
47
|
ownershipType: OwnershipType | null;
|
|
48
48
|
periodType: string | null;
|
|
@@ -8,7 +8,8 @@ import { refundCreditsOperation } from "../application/RefundCreditsCommand";
|
|
|
8
8
|
import { PURCHASE_TYPE, type PurchaseType } from "../../subscription/core/SubscriptionConstants";
|
|
9
9
|
import { requireFirestore, buildDocRef, type CollectionConfig } from "../../../shared/infrastructure/firestore";
|
|
10
10
|
import { fetchCredits, checkHasCredits, documentExists } from "./operations/CreditsFetcher";
|
|
11
|
-
import { syncExpiredStatus, syncPremiumMetadata, createRecoveryCreditsDocument
|
|
11
|
+
import { syncExpiredStatus, syncPremiumMetadata, createRecoveryCreditsDocument } from "./operations/CreditsWriter";
|
|
12
|
+
import type { SubscriptionMetadata } from "../../subscription/core/types";
|
|
12
13
|
import { initializeCreditsWithRetry } from "./operations/CreditsInitializer";
|
|
13
14
|
import { calculateCreditLimit } from "../application/CreditLimitCalculator";
|
|
14
15
|
|
|
@@ -81,7 +82,7 @@ export class CreditsRepository extends BaseRepository {
|
|
|
81
82
|
await syncExpiredStatus(this.getRef(db, userId));
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
async syncPremiumMetadata(userId: string, metadata:
|
|
85
|
+
async syncPremiumMetadata(userId: string, metadata: SubscriptionMetadata): Promise<void> {
|
|
85
86
|
const db = requireFirestore();
|
|
86
87
|
await syncPremiumMetadata(this.getRef(db, userId), metadata);
|
|
87
88
|
}
|
|
@@ -92,7 +93,7 @@ export class CreditsRepository extends BaseRepository {
|
|
|
92
93
|
willRenew: boolean,
|
|
93
94
|
expirationDate: string | null,
|
|
94
95
|
periodType: string | null,
|
|
95
|
-
|
|
96
|
+
storeTransactionId?: string | null,
|
|
96
97
|
): Promise<boolean> {
|
|
97
98
|
const db = requireFirestore();
|
|
98
99
|
const creditLimit = calculateCreditLimit(productId, this.config);
|
|
@@ -105,7 +106,7 @@ export class CreditsRepository extends BaseRepository {
|
|
|
105
106
|
periodType,
|
|
106
107
|
db,
|
|
107
108
|
userId,
|
|
108
|
-
|
|
109
|
+
storeTransactionId,
|
|
109
110
|
);
|
|
110
111
|
}
|
|
111
112
|
}
|
|
@@ -63,7 +63,7 @@ export async function initializeCreditsWithRetry(params: InitializeCreditsParams
|
|
|
63
63
|
source,
|
|
64
64
|
expirationDate: revenueCatData.expirationDate,
|
|
65
65
|
willRenew: revenueCatData.willRenew,
|
|
66
|
-
|
|
66
|
+
storeTransactionId: revenueCatData.storeTransactionId,
|
|
67
67
|
isPremium: revenueCatData.isPremium,
|
|
68
68
|
periodType: revenueCatData.periodType,
|
|
69
69
|
unsubscribeDetectedAt: revenueCatData.unsubscribeDetectedAt,
|
|
@@ -3,6 +3,7 @@ import { runTransaction, serverTimestamp } from "@umituz/react-native-firebase";
|
|
|
3
3
|
import { doc, getDoc, setDoc } from "firebase/firestore";
|
|
4
4
|
import { SUBSCRIPTION_STATUS } from "../../../subscription/core/SubscriptionConstants";
|
|
5
5
|
import { resolveSubscriptionStatus } from "../../../subscription/core/SubscriptionStatus";
|
|
6
|
+
import type { SubscriptionMetadata } from "../../../subscription/core/types";
|
|
6
7
|
import { toTimestamp } from "../../../../shared/utils/dateConverter";
|
|
7
8
|
import { isPast } from "../../../../utils/dateUtils";
|
|
8
9
|
import { getAppVersion, validatePlatform } from "../../../../utils/appUtils";
|
|
@@ -25,22 +26,10 @@ export async function syncExpiredStatus(ref: DocumentReference): Promise<void> {
|
|
|
25
26
|
});
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
export interface PremiumMetadata {
|
|
29
|
-
isPremium: boolean;
|
|
30
|
-
willRenew: boolean;
|
|
31
|
-
expirationDate: string | null;
|
|
32
|
-
productId: string;
|
|
33
|
-
periodType: string | null;
|
|
34
|
-
unsubscribeDetectedAt: string | null;
|
|
35
|
-
billingIssueDetectedAt: string | null;
|
|
36
|
-
store: string | null;
|
|
37
|
-
ownershipType: string | null;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
29
|
// Fix: was getDoc+setDoc (non-atomic) — now uses runTransaction.
|
|
41
30
|
export async function syncPremiumMetadata(
|
|
42
31
|
ref: DocumentReference,
|
|
43
|
-
metadata:
|
|
32
|
+
metadata: SubscriptionMetadata
|
|
44
33
|
): Promise<void> {
|
|
45
34
|
await runTransaction(async (tx: Transaction) => {
|
|
46
35
|
const doc = await tx.get(ref);
|
|
@@ -78,7 +67,7 @@ export async function syncPremiumMetadata(
|
|
|
78
67
|
* This handles edge cases like test store purchases, reinstalls, or failed initializations.
|
|
79
68
|
* Returns true if a new document was created, false if one already existed.
|
|
80
69
|
*
|
|
81
|
-
* Cross-user guard: if
|
|
70
|
+
* Cross-user guard: if storeTransactionId is provided and already registered
|
|
82
71
|
* to a different user in the global processedTransactions collection, the recovery
|
|
83
72
|
* document is NOT created (the subscription belongs to another UID).
|
|
84
73
|
*/
|
|
@@ -91,22 +80,22 @@ export async function createRecoveryCreditsDocument(
|
|
|
91
80
|
periodType: string | null,
|
|
92
81
|
db?: Firestore,
|
|
93
82
|
userId?: string,
|
|
94
|
-
|
|
83
|
+
storeTransactionId?: string | null,
|
|
95
84
|
): Promise<boolean> {
|
|
96
85
|
const existingDoc = await getDoc(ref);
|
|
97
86
|
if (existingDoc.exists()) return false;
|
|
98
87
|
|
|
99
88
|
// Cross-user deduplication: if this transaction was already processed by another
|
|
100
89
|
// user, skip recovery to prevent double credit allocation across UIDs.
|
|
101
|
-
if (db && userId &&
|
|
90
|
+
if (db && userId && storeTransactionId) {
|
|
102
91
|
try {
|
|
103
|
-
const globalRef = doc(db, GLOBAL_TRANSACTION_COLLECTION,
|
|
92
|
+
const globalRef = doc(db, GLOBAL_TRANSACTION_COLLECTION, storeTransactionId);
|
|
104
93
|
const globalDoc = await getDoc(globalRef);
|
|
105
94
|
if (globalDoc.exists()) {
|
|
106
95
|
const globalData = globalDoc.data();
|
|
107
96
|
if (globalData?.ownerUserId && globalData.ownerUserId !== userId) {
|
|
108
97
|
console.warn(
|
|
109
|
-
`[CreditsWriter] Recovery skipped: transaction ${
|
|
98
|
+
`[CreditsWriter] Recovery skipped: transaction ${storeTransactionId} belongs to user ${globalData.ownerUserId}, not ${userId}`
|
|
110
99
|
);
|
|
111
100
|
return false;
|
|
112
101
|
}
|
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SubscriptionMetadata } from "../../../subscription/core/types";
|
|
2
|
+
import type { PackageType } from "./RevenueCatTypes";
|
|
2
3
|
|
|
3
|
-
export interface RevenueCatData {
|
|
4
|
-
expirationDate: string | null;
|
|
4
|
+
export interface RevenueCatData extends Omit<SubscriptionMetadata, 'willRenew' | 'productId'> {
|
|
5
5
|
willRenew: boolean | null;
|
|
6
|
-
|
|
7
|
-
isPremium: boolean;
|
|
8
|
-
periodType: string | null;
|
|
6
|
+
storeTransactionId: string | null;
|
|
9
7
|
packageType: PackageType | null;
|
|
10
|
-
unsubscribeDetectedAt: string | null;
|
|
11
|
-
billingIssueDetectedAt: string | null;
|
|
12
|
-
store: Store | null;
|
|
13
|
-
ownershipType: OwnershipType | null;
|
|
14
8
|
revenueCatUserId?: string | null;
|
|
15
9
|
}
|
|
@@ -193,10 +193,14 @@ export async function handleInitialConfiguration(
|
|
|
193
193
|
userId: normalizedUserId,
|
|
194
194
|
isPremium: true,
|
|
195
195
|
productId: premiumEntitlement.productIdentifier,
|
|
196
|
-
|
|
196
|
+
expirationDate: premiumEntitlement.expirationDate ?? null,
|
|
197
197
|
willRenew: premiumEntitlement.willRenew,
|
|
198
198
|
periodType: premiumEntitlement.periodType as PeriodType | undefined,
|
|
199
|
-
|
|
199
|
+
storeTransactionId: subscription?.storeTransactionId ?? undefined,
|
|
200
|
+
unsubscribeDetectedAt: premiumEntitlement.unsubscribeDetectedAt ?? null,
|
|
201
|
+
billingIssueDetectedAt: premiumEntitlement.billingIssueDetectedAt ?? null,
|
|
202
|
+
store: premiumEntitlement.store ?? null,
|
|
203
|
+
ownershipType: premiumEntitlement.ownershipType ?? null,
|
|
200
204
|
});
|
|
201
205
|
} else {
|
|
202
206
|
await deps.config.onPremiumStatusChanged({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CreditsConfig } from "../../credits/core/Credits";
|
|
2
2
|
import type { UserCreditsDocumentRead } from "../../credits/core/UserCreditsDocument";
|
|
3
3
|
import type { PurchaseSource, PurchaseType } from "../core/SubscriptionConstants";
|
|
4
|
-
import type {
|
|
4
|
+
import type { SubscriptionMetadata } from "../core/types";
|
|
5
5
|
|
|
6
6
|
export interface FirebaseAuthLike {
|
|
7
7
|
currentUser: { uid: string; isAnonymous: boolean } | null;
|
|
@@ -28,19 +28,11 @@ export interface SubscriptionInitConfig {
|
|
|
28
28
|
authStateTimeoutMs?: number;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export interface InitializeCreditsMetadata {
|
|
32
|
-
productId: string;
|
|
31
|
+
export interface InitializeCreditsMetadata extends Omit<SubscriptionMetadata, 'willRenew'> {
|
|
33
32
|
source: PurchaseSource;
|
|
34
33
|
type: PurchaseType;
|
|
35
|
-
expirationDate: string | null;
|
|
36
34
|
willRenew: boolean | null;
|
|
37
|
-
|
|
38
|
-
isPremium: boolean;
|
|
39
|
-
periodType: string | null;
|
|
40
|
-
unsubscribeDetectedAt: string | null;
|
|
41
|
-
billingIssueDetectedAt: string | null;
|
|
42
|
-
store: Store | null;
|
|
43
|
-
ownershipType: OwnershipType | null;
|
|
35
|
+
storeTransactionId: string | null;
|
|
44
36
|
revenueCatUserId?: string | null;
|
|
45
37
|
}
|
|
46
38
|
|
|
@@ -110,7 +110,7 @@ export class SubscriptionSyncProcessor {
|
|
|
110
110
|
revenueCatData.packageType = event.packageType ?? null;
|
|
111
111
|
const revenueCatAppUserId = await this.getRevenueCatAppUserId();
|
|
112
112
|
revenueCatData.revenueCatUserId = revenueCatAppUserId ?? event.userId;
|
|
113
|
-
const purchaseId = generatePurchaseId(revenueCatData.
|
|
113
|
+
const purchaseId = generatePurchaseId(revenueCatData.storeTransactionId, event.productId);
|
|
114
114
|
|
|
115
115
|
const creditsUserId = await this.getCreditsUserId(event.userId);
|
|
116
116
|
|
|
@@ -140,7 +140,7 @@ export class SubscriptionSyncProcessor {
|
|
|
140
140
|
revenueCatData.expirationDate = event.newExpirationDate ?? revenueCatData.expirationDate;
|
|
141
141
|
const revenueCatAppUserId = await this.getRevenueCatAppUserId();
|
|
142
142
|
revenueCatData.revenueCatUserId = revenueCatAppUserId ?? event.userId;
|
|
143
|
-
const purchaseId = generateRenewalId(revenueCatData.
|
|
143
|
+
const purchaseId = generateRenewalId(revenueCatData.storeTransactionId, event.productId, event.newExpirationDate);
|
|
144
144
|
|
|
145
145
|
const creditsUserId = await this.getCreditsUserId(event.userId);
|
|
146
146
|
|
|
@@ -220,9 +220,9 @@ export class SubscriptionSyncProcessor {
|
|
|
220
220
|
userId,
|
|
221
221
|
event.productId!,
|
|
222
222
|
event.willRenew ?? false,
|
|
223
|
-
event.
|
|
223
|
+
event.expirationDate ?? null,
|
|
224
224
|
event.periodType ?? null,
|
|
225
|
-
event.
|
|
225
|
+
event.storeTransactionId,
|
|
226
226
|
);
|
|
227
227
|
if (__DEV__ && created) {
|
|
228
228
|
console.log('[SubscriptionSyncProcessor] Recovery: created missing credits document for premium user', {
|
|
@@ -235,13 +235,13 @@ export class SubscriptionSyncProcessor {
|
|
|
235
235
|
await repo.syncPremiumMetadata(userId, {
|
|
236
236
|
isPremium: event.isPremium,
|
|
237
237
|
willRenew: event.willRenew ?? false,
|
|
238
|
-
expirationDate: event.
|
|
238
|
+
expirationDate: event.expirationDate ?? null,
|
|
239
239
|
productId: event.productId!,
|
|
240
240
|
periodType: event.periodType ?? null,
|
|
241
|
-
unsubscribeDetectedAt: null,
|
|
242
|
-
billingIssueDetectedAt: null,
|
|
243
|
-
store: null,
|
|
244
|
-
ownershipType: null,
|
|
241
|
+
unsubscribeDetectedAt: event.unsubscribeDetectedAt ?? null,
|
|
242
|
+
billingIssueDetectedAt: event.billingIssueDetectedAt ?? null,
|
|
243
|
+
store: event.store ?? null,
|
|
244
|
+
ownershipType: event.ownershipType ?? null,
|
|
245
245
|
});
|
|
246
246
|
this.emitCreditsUpdated(userId);
|
|
247
247
|
}
|
|
@@ -23,7 +23,7 @@ export const extractRevenueCatData = (customerInfo: CustomerInfo, entitlementId:
|
|
|
23
23
|
return {
|
|
24
24
|
expirationDate: null,
|
|
25
25
|
willRenew: null,
|
|
26
|
-
|
|
26
|
+
storeTransactionId: null,
|
|
27
27
|
periodType: null,
|
|
28
28
|
packageType: null,
|
|
29
29
|
isPremium: false,
|
|
@@ -39,11 +39,11 @@ export const extractRevenueCatData = (customerInfo: CustomerInfo, entitlementId:
|
|
|
39
39
|
return {
|
|
40
40
|
expirationDate: entitlement.expirationDate ?? null,
|
|
41
41
|
willRenew: entitlement.willRenew ?? null,
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
// Note:
|
|
45
|
-
//
|
|
46
|
-
|
|
42
|
+
// Current transaction ID from Apple/Google — changes with each renewal.
|
|
43
|
+
// Used as dedup key in processedTransactions collection.
|
|
44
|
+
// Note: original_store_transaction_id (stable across renewals) is only
|
|
45
|
+
// available server-side (webhooks/REST API), not in the client SDK.
|
|
46
|
+
storeTransactionId: subscription?.storeTransactionId ?? null,
|
|
47
47
|
periodType: validatePeriodType(entitlement.periodType),
|
|
48
48
|
packageType: null,
|
|
49
49
|
isPremium: true,
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
const uniqueSuffix = (): string => `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
2
2
|
|
|
3
|
-
export const generatePurchaseId = (
|
|
4
|
-
return
|
|
5
|
-
? `purchase_${
|
|
3
|
+
export const generatePurchaseId = (storeTransactionId: string | null, productId: string): string => {
|
|
4
|
+
return storeTransactionId
|
|
5
|
+
? `purchase_${storeTransactionId}`
|
|
6
6
|
: `purchase_${productId}_${uniqueSuffix()}`;
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
-
export const generateRenewalId = (
|
|
10
|
-
return
|
|
11
|
-
? `renewal_${
|
|
9
|
+
export const generateRenewalId = (storeTransactionId: string | null, productId: string, expirationDate: string): string => {
|
|
10
|
+
return storeTransactionId
|
|
11
|
+
? `renewal_${storeTransactionId}_${expirationDate}`
|
|
12
12
|
: `renewal_${productId}_${uniqueSuffix()}`;
|
|
13
13
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CustomerInfo } from "react-native-purchases";
|
|
2
|
-
import type {
|
|
2
|
+
import type { PurchaseSource } from "./SubscriptionConstants";
|
|
3
|
+
import type { SubscriptionMetadata } from "./types";
|
|
3
4
|
import type { PackageType } from "../../revenuecat/core/types/RevenueCatTypes";
|
|
4
5
|
|
|
5
6
|
export interface PurchaseCompletedEvent {
|
|
@@ -17,14 +18,10 @@ export interface RenewalDetectedEvent {
|
|
|
17
18
|
customerInfo: CustomerInfo;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
export interface PremiumStatusChangedEvent {
|
|
21
|
+
export interface PremiumStatusChangedEvent extends Partial<SubscriptionMetadata> {
|
|
21
22
|
userId: string;
|
|
22
23
|
isPremium: boolean;
|
|
23
|
-
|
|
24
|
-
expiresAt?: string;
|
|
25
|
-
willRenew?: boolean;
|
|
26
|
-
periodType?: PeriodType;
|
|
27
|
-
originalTransactionId?: string;
|
|
24
|
+
storeTransactionId?: string;
|
|
28
25
|
}
|
|
29
26
|
|
|
30
27
|
export interface PlanChangedEvent {
|
|
@@ -12,7 +12,7 @@ export type { SubscriptionStatusType };
|
|
|
12
12
|
|
|
13
13
|
export interface SubscriptionStatus {
|
|
14
14
|
isPremium: boolean;
|
|
15
|
-
|
|
15
|
+
expirationDate: string | null;
|
|
16
16
|
productId: string | null;
|
|
17
17
|
purchasedAt?: string | null;
|
|
18
18
|
customerId?: string | null;
|
|
@@ -23,7 +23,7 @@ export interface SubscriptionStatus {
|
|
|
23
23
|
|
|
24
24
|
export const createDefaultSubscriptionStatus = (): SubscriptionStatus => ({
|
|
25
25
|
isPremium: false,
|
|
26
|
-
|
|
26
|
+
expirationDate: null,
|
|
27
27
|
productId: null,
|
|
28
28
|
purchasedAt: null,
|
|
29
29
|
customerId: null,
|
|
@@ -33,8 +33,8 @@ export const createDefaultSubscriptionStatus = (): SubscriptionStatus => ({
|
|
|
33
33
|
|
|
34
34
|
export const isSubscriptionValid = (status: SubscriptionStatus | null): boolean => {
|
|
35
35
|
if (!status || !status.isPremium) return false;
|
|
36
|
-
if (!status.
|
|
37
|
-
return timezoneService.isFuture(new Date(status.
|
|
36
|
+
if (!status.expirationDate) return true;
|
|
37
|
+
return timezoneService.isFuture(new Date(status.expirationDate));
|
|
38
38
|
};
|
|
39
39
|
|
|
40
40
|
export interface StatusResolverInput {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credit usage information for display in subscription UI components.
|
|
3
|
+
* Single source of truth — used by SubscriptionDetailScreen and PremiumDetailsCard.
|
|
4
|
+
*/
|
|
5
|
+
export interface CreditInfo {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
current: number;
|
|
9
|
+
total: number;
|
|
10
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolved premium status from RevenueCat CustomerInfo.
|
|
3
|
+
* Uses Date objects (presentation-ready).
|
|
4
|
+
*
|
|
5
|
+
* Extended by SubscriptionStatusResult which adds hook state (isLoading, error, refetch).
|
|
6
|
+
*/
|
|
7
|
+
export interface PremiumStatus {
|
|
8
|
+
isPremium: boolean;
|
|
9
|
+
expirationDate: Date | null;
|
|
10
|
+
willRenew: boolean;
|
|
11
|
+
productIdentifier: string | null;
|
|
12
|
+
originalPurchaseDate: Date | null;
|
|
13
|
+
latestPurchaseDate: Date | null;
|
|
14
|
+
billingIssuesDetected: boolean;
|
|
15
|
+
isSandbox: boolean;
|
|
16
|
+
periodType: string | null;
|
|
17
|
+
packageType: string | null;
|
|
18
|
+
store: string | null;
|
|
19
|
+
gracePeriodExpiresDate: Date | null;
|
|
20
|
+
unsubscribeDetectedAt: Date | null;
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base subscription metadata — the single source of truth for subscription state fields.
|
|
3
|
+
*
|
|
4
|
+
* All subscription-related types (RevenueCatData, PremiumStatusChangedEvent,
|
|
5
|
+
* InitializeCreditsMetadata) extend or compose from this base to eliminate
|
|
6
|
+
* field duplication and ensure consistent naming.
|
|
7
|
+
*/
|
|
8
|
+
export interface SubscriptionMetadata {
|
|
9
|
+
isPremium: boolean;
|
|
10
|
+
willRenew: boolean;
|
|
11
|
+
expirationDate: string | null;
|
|
12
|
+
productId: string;
|
|
13
|
+
periodType: string | null;
|
|
14
|
+
unsubscribeDetectedAt: string | null;
|
|
15
|
+
billingIssueDetectedAt: string | null;
|
|
16
|
+
store: string | null;
|
|
17
|
+
ownershipType: string | null;
|
|
18
|
+
}
|
|
@@ -2,22 +2,9 @@ import type { CustomerInfo } from "react-native-purchases";
|
|
|
2
2
|
import { getPremiumEntitlement } from "../../../revenuecat/core/types";
|
|
3
3
|
import { toDate } from "../../../../shared/utils/dateConverter";
|
|
4
4
|
import { detectPackageType } from "../../../../utils/packageTypeDetector";
|
|
5
|
+
import type { PremiumStatus } from "../../core/types";
|
|
5
6
|
|
|
6
|
-
export
|
|
7
|
-
isPremium: boolean;
|
|
8
|
-
expirationDate: Date | null;
|
|
9
|
-
willRenew: boolean;
|
|
10
|
-
productIdentifier: string | null;
|
|
11
|
-
originalPurchaseDate: Date | null;
|
|
12
|
-
latestPurchaseDate: Date | null;
|
|
13
|
-
billingIssuesDetected: boolean;
|
|
14
|
-
isSandbox: boolean;
|
|
15
|
-
periodType: string | null;
|
|
16
|
-
packageType: string | null;
|
|
17
|
-
store: string | null;
|
|
18
|
-
gracePeriodExpiresDate: Date | null;
|
|
19
|
-
unsubscribeDetectedAt: Date | null;
|
|
20
|
-
}
|
|
7
|
+
export type { PremiumStatus };
|
|
21
8
|
|
|
22
9
|
export class PurchaseStatusResolver {
|
|
23
10
|
static resolve(customerInfo: CustomerInfo, entitlementId: string): PremiumStatus {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RevenueCatConfig } from "../../../revenuecat/core/types";
|
|
2
|
-
import type { PremiumStatus } from "
|
|
2
|
+
import type { PremiumStatus } from "../../core/types";
|
|
3
3
|
import type { RestoreResultInfo } from "../handlers/package-operations/types";
|
|
4
4
|
|
|
5
5
|
export interface SubscriptionManagerConfig {
|
|
@@ -36,10 +36,14 @@ export async function syncPremiumStatus(
|
|
|
36
36
|
userId,
|
|
37
37
|
isPremium: true,
|
|
38
38
|
productId: premiumEntitlement.productIdentifier,
|
|
39
|
-
|
|
39
|
+
expirationDate: premiumEntitlement.expirationDate ?? null,
|
|
40
40
|
willRenew: premiumEntitlement.willRenew,
|
|
41
41
|
periodType: premiumEntitlement.periodType as PeriodType | undefined,
|
|
42
|
-
|
|
42
|
+
storeTransactionId: subscription?.storeTransactionId ?? undefined,
|
|
43
|
+
unsubscribeDetectedAt: premiumEntitlement.unsubscribeDetectedAt ?? null,
|
|
44
|
+
billingIssueDetectedAt: premiumEntitlement.billingIssueDetectedAt ?? null,
|
|
45
|
+
store: premiumEntitlement.store ?? null,
|
|
46
|
+
ownershipType: premiumEntitlement.ownershipType ?? null,
|
|
43
47
|
});
|
|
44
48
|
} else {
|
|
45
49
|
await config.onPremiumStatusChanged({ userId, isPremium: false });
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import type { SubscriptionStatusType } from "./PremiumStatusBadge";
|
|
2
|
+
import type { CreditInfo } from "../../../core/types";
|
|
2
3
|
|
|
3
|
-
export
|
|
4
|
-
id: string;
|
|
5
|
-
label: string;
|
|
6
|
-
current: number;
|
|
7
|
-
total: number;
|
|
8
|
-
}
|
|
4
|
+
export type { CreditInfo };
|
|
9
5
|
|
|
10
6
|
export interface PremiumDetailsTranslations {
|
|
11
7
|
title: string;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { SubscriptionStatusType } from "../../core/SubscriptionConstants";
|
|
2
|
+
import type { CreditInfo } from "../../core/types";
|
|
3
|
+
|
|
4
|
+
export type { CreditInfo };
|
|
2
5
|
|
|
3
6
|
export interface SubscriptionDisplayFlags {
|
|
4
7
|
showHeader: boolean;
|
|
@@ -40,13 +43,6 @@ export interface UpgradePromptConfig {
|
|
|
40
43
|
onUpgrade?: () => void;
|
|
41
44
|
}
|
|
42
45
|
|
|
43
|
-
export interface CreditInfo {
|
|
44
|
-
id: string;
|
|
45
|
-
label: string;
|
|
46
|
-
current: number;
|
|
47
|
-
total: number;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
46
|
export interface SubscriptionDetailConfig {
|
|
51
47
|
display: SubscriptionDisplayFlags;
|
|
52
48
|
statusType: SubscriptionStatusType;
|
|
@@ -1,17 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
willRenew: boolean;
|
|
5
|
-
productIdentifier: string | null;
|
|
6
|
-
originalPurchaseDate: Date | null;
|
|
7
|
-
latestPurchaseDate: Date | null;
|
|
8
|
-
billingIssuesDetected: boolean;
|
|
9
|
-
isSandbox: boolean;
|
|
10
|
-
periodType: string | null;
|
|
11
|
-
packageType: string | null;
|
|
12
|
-
store: string | null;
|
|
13
|
-
gracePeriodExpiresDate: Date | null;
|
|
14
|
-
unsubscribeDetectedAt: Date | null;
|
|
1
|
+
import type { PremiumStatus } from "../core/types";
|
|
2
|
+
|
|
3
|
+
export interface SubscriptionStatusResult extends PremiumStatus {
|
|
15
4
|
isLoading: boolean;
|
|
16
5
|
error: Error | null;
|
|
17
6
|
refetch: () => void;
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Domain Layer - Constants & Types
|
|
2
2
|
export * from "./domains/subscription/core/SubscriptionConstants";
|
|
3
|
+
export type { SubscriptionMetadata, PremiumStatus, CreditInfo } from "./domains/subscription/core/types";
|
|
3
4
|
export {
|
|
4
5
|
createDefaultSubscriptionStatus,
|
|
5
6
|
isSubscriptionValid,
|
|
@@ -57,7 +58,6 @@ export type {
|
|
|
57
58
|
SubscriptionDetailTranslations,
|
|
58
59
|
SubscriptionDisplayFlags,
|
|
59
60
|
UpgradePromptConfig,
|
|
60
|
-
CreditInfo,
|
|
61
61
|
} from "./domains/subscription/presentation/screens/SubscriptionDetailScreen.types";
|
|
62
62
|
export * from "./domains/paywall/components/PaywallContainer";
|
|
63
63
|
|