@umituz/react-native-subscription 2.22.7 → 2.22.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 +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 +25 -4
- package/src/presentation/hooks/useSubscriptionSettingsConfig.ts +63 -138
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.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",
|
|
@@ -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;
|
|
@@ -39,16 +41,31 @@ export const initializeSubscription = async (config: SubscriptionInitConfig): Pr
|
|
|
39
41
|
|
|
40
42
|
configureCreditsRepository({ ...credits, creditPackageAmounts: creditPackages?.amounts });
|
|
41
43
|
|
|
42
|
-
|
|
44
|
+
/** Extract RevenueCat data from CustomerInfo (Single Source of Truth) */
|
|
45
|
+
const extractRevenueCatData = (customerInfo: CustomerInfo, _productId: string): RevenueCatData => {
|
|
46
|
+
const entitlement = customerInfo.entitlements.active[entitlementId]
|
|
47
|
+
?? customerInfo.entitlements.all[entitlementId];
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
expirationDate: entitlement?.expirationDate ?? customerInfo.latestExpirationDate ?? null,
|
|
51
|
+
willRenew: entitlement?.willRenew ?? false,
|
|
52
|
+
originalTransactionId: entitlement?.originalPurchaseDate ?? undefined,
|
|
53
|
+
isPremium: Object.keys(customerInfo.entitlements.active).length > 0,
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const onPurchase = async (userId: string, productId: string, customerInfo: CustomerInfo, source?: string) => {
|
|
43
58
|
if (__DEV__) {
|
|
44
59
|
console.log('[SubscriptionInitializer] onPurchase called:', { userId, productId, source });
|
|
45
60
|
}
|
|
46
61
|
try {
|
|
62
|
+
const revenueCatData = extractRevenueCatData(customerInfo, productId);
|
|
47
63
|
const result = await getCreditsRepository().initializeCredits(
|
|
48
64
|
userId,
|
|
49
65
|
`purchase_${productId}_${Date.now()}`,
|
|
50
66
|
productId,
|
|
51
|
-
source as any
|
|
67
|
+
source as any,
|
|
68
|
+
revenueCatData
|
|
52
69
|
);
|
|
53
70
|
if (__DEV__) {
|
|
54
71
|
console.log('[SubscriptionInitializer] Credits initialized:', result);
|
|
@@ -61,16 +78,20 @@ export const initializeSubscription = async (config: SubscriptionInitConfig): Pr
|
|
|
61
78
|
}
|
|
62
79
|
};
|
|
63
80
|
|
|
64
|
-
const onRenewal = async (userId: string, productId: string,
|
|
81
|
+
const onRenewal = async (userId: string, productId: string, newExpirationDate: string, customerInfo: CustomerInfo) => {
|
|
65
82
|
if (__DEV__) {
|
|
66
83
|
console.log('[SubscriptionInitializer] onRenewal called:', { userId, productId });
|
|
67
84
|
}
|
|
68
85
|
try {
|
|
86
|
+
const revenueCatData = extractRevenueCatData(customerInfo, productId);
|
|
87
|
+
// Update expiration date from renewal
|
|
88
|
+
revenueCatData.expirationDate = newExpirationDate || revenueCatData.expirationDate;
|
|
69
89
|
const result = await getCreditsRepository().initializeCredits(
|
|
70
90
|
userId,
|
|
71
91
|
`renewal_${productId}_${Date.now()}`,
|
|
72
92
|
productId,
|
|
73
|
-
"renewal" as any
|
|
93
|
+
"renewal" as any,
|
|
94
|
+
revenueCatData
|
|
74
95
|
);
|
|
75
96
|
if (__DEV__) {
|
|
76
97
|
console.log('[SubscriptionInitializer] Credits reset on renewal:', result);
|
|
@@ -1,21 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useSubscriptionSettingsConfig Hook
|
|
3
3
|
* Returns ready-to-use config for settings screens
|
|
4
|
-
*
|
|
4
|
+
* Single Source of Truth: Firestore (credits document)
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { useMemo, useCallback } from "react";
|
|
8
8
|
import { useCredits } from "./useCredits";
|
|
9
|
-
import { useSubscriptionStatus } from "./useSubscriptionStatus";
|
|
10
|
-
import { useCustomerInfo } from "../../revenuecat/presentation/hooks/useCustomerInfo";
|
|
11
9
|
import { usePaywallVisibility } from "./usePaywallVisibility";
|
|
12
10
|
import { calculateDaysRemaining } from "../../domain/entities/SubscriptionStatus";
|
|
13
|
-
import {
|
|
14
|
-
import { formatDate, convertPurchasedAt } from "../utils/subscriptionDateUtils";
|
|
11
|
+
import { formatDate } from "../utils/subscriptionDateUtils";
|
|
15
12
|
import { useCreditsArray, getSubscriptionStatusType } from "./useSubscriptionSettingsConfig.utils";
|
|
16
13
|
import { getCreditsConfig } from "../../infrastructure/repositories/CreditsRepositoryProvider";
|
|
17
|
-
import { detectPackageType } from "../../utils/packageTypeDetector";
|
|
18
|
-
import { getCreditAllocation } from "../../utils/creditMapper";
|
|
19
14
|
import type {
|
|
20
15
|
SubscriptionSettingsConfig,
|
|
21
16
|
SubscriptionStatusType,
|
|
@@ -31,173 +26,103 @@ export type {
|
|
|
31
26
|
|
|
32
27
|
/**
|
|
33
28
|
* Hook that returns ready-to-use subscription config for settings
|
|
34
|
-
*
|
|
29
|
+
* Single Source of Truth: Firestore credits document
|
|
35
30
|
*/
|
|
36
31
|
export const useSubscriptionSettingsConfig = (
|
|
37
32
|
params: UseSubscriptionSettingsConfigParams
|
|
38
33
|
): SubscriptionSettingsConfig => {
|
|
39
|
-
const {
|
|
40
|
-
userId,
|
|
41
|
-
translations,
|
|
42
|
-
creditLimit,
|
|
43
|
-
upgradePrompt,
|
|
44
|
-
} = params;
|
|
34
|
+
const { userId, translations, creditLimit, upgradePrompt } = params;
|
|
45
35
|
|
|
46
|
-
//
|
|
36
|
+
// Single Source of Truth: Firestore credits document
|
|
47
37
|
const { credits } = useCredits({ userId, enabled: !!userId });
|
|
48
|
-
const {
|
|
49
|
-
isPremium: subscriptionActive,
|
|
50
|
-
expirationDate: statusExpirationDate,
|
|
51
|
-
} = useSubscriptionStatus({
|
|
52
|
-
userId,
|
|
53
|
-
enabled: !!userId,
|
|
54
|
-
});
|
|
55
|
-
const { customerInfo } = useCustomerInfo();
|
|
56
38
|
const { openPaywall } = usePaywallVisibility();
|
|
57
39
|
|
|
58
40
|
const handleOpenPaywall = useCallback(() => {
|
|
59
41
|
openPaywall("settings");
|
|
60
42
|
}, [openPaywall]);
|
|
61
43
|
|
|
62
|
-
//
|
|
63
|
-
const
|
|
64
|
-
const
|
|
44
|
+
// All data from Firestore (Single Source of Truth)
|
|
45
|
+
const isPremium = credits?.isPremium ?? false;
|
|
46
|
+
const willRenew = credits?.willRenew ?? false;
|
|
65
47
|
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
const isPremium = !!premiumEntitlement || subscriptionActive;
|
|
48
|
+
// Expiration date from Firestore
|
|
49
|
+
const expiresAtIso = credits?.expirationDate?.toISOString() ?? null;
|
|
69
50
|
|
|
51
|
+
// Purchase date from Firestore
|
|
52
|
+
const purchasedAtIso = credits?.purchasedAt?.toISOString() ?? null;
|
|
53
|
+
|
|
54
|
+
// Credit limit from Firestore or config fallback
|
|
70
55
|
const dynamicCreditLimit = useMemo(() => {
|
|
56
|
+
if (credits?.creditLimit) return credits.creditLimit;
|
|
71
57
|
const config = getCreditsConfig();
|
|
72
|
-
|
|
73
|
-
// 1. ÖNCE FIRESTORE'DAN OKU (Single Source of Truth)
|
|
74
|
-
if (credits?.creditLimit) {
|
|
75
|
-
return credits.creditLimit;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// 2. FALLBACK: RevenueCat'ten detect et
|
|
79
|
-
if (premiumEntitlement?.productIdentifier) {
|
|
80
|
-
const packageType = detectPackageType(premiumEntitlement.productIdentifier);
|
|
81
|
-
const allocation = getCreditAllocation(packageType, config.packageAllocations);
|
|
82
|
-
if (allocation !== null) return allocation;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// 3. LAST RESORT: Credit miktarına bakarak tahmin et
|
|
86
|
-
if (credits?.credits && config.packageAllocations) {
|
|
87
|
-
const currentCredits = credits.credits;
|
|
88
|
-
const allocations = Object.values(config.packageAllocations).map(a => a.credits);
|
|
89
|
-
const closest = allocations.find(a => a >= currentCredits) || Math.max(...allocations);
|
|
90
|
-
return closest;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// 4. FINAL FALLBACK: Config'den al
|
|
94
58
|
return creditLimit ?? config.creditLimit;
|
|
95
|
-
}, [credits?.creditLimit,
|
|
96
|
-
|
|
97
|
-
// Get expiration date directly from RevenueCat (source of truth)
|
|
98
|
-
const entitlementExpirationDate = premiumEntitlement?.expirationDate ?? null;
|
|
99
|
-
|
|
100
|
-
// Prefer CustomerInfo expiration (real-time) over cached status
|
|
101
|
-
const expiresAtIso = entitlementExpirationDate || (statusExpirationDate
|
|
102
|
-
? statusExpirationDate.toISOString()
|
|
103
|
-
: null);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const willRenew = premiumEntitlement?.willRenew || false;
|
|
108
|
-
const purchasedAtIso = convertPurchasedAt(credits?.purchasedAt);
|
|
59
|
+
}, [credits?.creditLimit, creditLimit]);
|
|
109
60
|
|
|
110
61
|
// Formatted dates
|
|
111
|
-
const formattedExpirationDate = useMemo(
|
|
112
|
-
|
|
113
|
-
[expiresAtIso]
|
|
114
|
-
);
|
|
62
|
+
const formattedExpirationDate = useMemo(() => formatDate(expiresAtIso), [expiresAtIso]);
|
|
63
|
+
const formattedPurchaseDate = useMemo(() => formatDate(purchasedAtIso), [purchasedAtIso]);
|
|
115
64
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
[purchasedAtIso]
|
|
119
|
-
);
|
|
65
|
+
// Days remaining
|
|
66
|
+
const daysRemaining = useMemo(() => calculateDaysRemaining(expiresAtIso), [expiresAtIso]);
|
|
120
67
|
|
|
121
|
-
//
|
|
122
|
-
const
|
|
123
|
-
(
|
|
124
|
-
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
// Status type
|
|
128
|
-
const statusType: SubscriptionStatusType = getSubscriptionStatusType(isPremium);
|
|
68
|
+
// Status type from Firestore or derived
|
|
69
|
+
const statusType: SubscriptionStatusType = credits?.status
|
|
70
|
+
? (credits.status as SubscriptionStatusType)
|
|
71
|
+
: getSubscriptionStatusType(isPremium);
|
|
129
72
|
|
|
130
73
|
const creditsArray = useCreditsArray(credits, dynamicCreditLimit, translations);
|
|
131
74
|
|
|
132
|
-
// Centralized display flags
|
|
75
|
+
// Centralized display flags
|
|
133
76
|
const hasCredits = creditsArray.length > 0;
|
|
134
77
|
const display = useMemo(() => ({
|
|
135
78
|
showHeader: isPremium || hasCredits,
|
|
136
79
|
showCredits: hasCredits,
|
|
137
80
|
showUpgradePrompt: !isPremium && !hasCredits && !!upgradePrompt,
|
|
138
|
-
showExpirationDate: isPremium && !!expiresAtIso,
|
|
81
|
+
showExpirationDate: (isPremium || hasCredits) && !!expiresAtIso,
|
|
139
82
|
}), [isPremium, hasCredits, upgradePrompt, expiresAtIso]);
|
|
140
83
|
|
|
141
84
|
// Build config
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
description: translations.description,
|
|
148
|
-
isPremium,
|
|
149
|
-
statusLabel: isPremium
|
|
150
|
-
? translations.statusActive
|
|
151
|
-
: translations.statusFree,
|
|
152
|
-
icon: "diamond",
|
|
153
|
-
onPress: handleOpenPaywall,
|
|
154
|
-
},
|
|
155
|
-
sectionConfig: {
|
|
156
|
-
statusType,
|
|
157
|
-
isPremium,
|
|
158
|
-
display,
|
|
159
|
-
expirationDate: formattedExpirationDate,
|
|
160
|
-
purchaseDate: formattedPurchaseDate,
|
|
161
|
-
isLifetime: isPremium && !expiresAtIso,
|
|
162
|
-
daysRemaining,
|
|
163
|
-
willRenew,
|
|
164
|
-
credits: creditsArray,
|
|
165
|
-
translations: {
|
|
166
|
-
title: translations.title,
|
|
167
|
-
statusLabel: translations.statusLabel,
|
|
168
|
-
statusActive: translations.statusActive,
|
|
169
|
-
statusExpired: translations.statusExpired,
|
|
170
|
-
statusFree: translations.statusFree,
|
|
171
|
-
statusCanceled: translations.statusCanceled,
|
|
172
|
-
expiresLabel: translations.expiresLabel,
|
|
173
|
-
purchasedLabel: translations.purchasedLabel,
|
|
174
|
-
lifetimeLabel: translations.lifetimeLabel,
|
|
175
|
-
creditsTitle: translations.creditsTitle,
|
|
176
|
-
remainingLabel: translations.remainingLabel,
|
|
177
|
-
manageButton: translations.manageButton,
|
|
178
|
-
upgradeButton: translations.upgradeButton,
|
|
179
|
-
},
|
|
180
|
-
onUpgrade: handleOpenPaywall,
|
|
181
|
-
upgradePrompt,
|
|
182
|
-
},
|
|
183
|
-
}),
|
|
184
|
-
[
|
|
185
|
-
translations,
|
|
85
|
+
return useMemo((): SubscriptionSettingsConfig => ({
|
|
86
|
+
enabled: true,
|
|
87
|
+
settingsItem: {
|
|
88
|
+
title: translations.title,
|
|
89
|
+
description: translations.description,
|
|
186
90
|
isPremium,
|
|
91
|
+
statusLabel: isPremium ? translations.statusActive : translations.statusFree,
|
|
92
|
+
icon: "diamond",
|
|
93
|
+
onPress: handleOpenPaywall,
|
|
94
|
+
},
|
|
95
|
+
sectionConfig: {
|
|
187
96
|
statusType,
|
|
97
|
+
isPremium,
|
|
188
98
|
display,
|
|
189
|
-
formattedExpirationDate,
|
|
190
|
-
formattedPurchaseDate,
|
|
191
|
-
expiresAtIso,
|
|
99
|
+
expirationDate: formattedExpirationDate,
|
|
100
|
+
purchaseDate: formattedPurchaseDate,
|
|
101
|
+
isLifetime: isPremium && !expiresAtIso,
|
|
192
102
|
daysRemaining,
|
|
193
103
|
willRenew,
|
|
194
|
-
creditsArray,
|
|
195
|
-
|
|
104
|
+
credits: creditsArray,
|
|
105
|
+
translations: {
|
|
106
|
+
title: translations.title,
|
|
107
|
+
statusLabel: translations.statusLabel,
|
|
108
|
+
statusActive: translations.statusActive,
|
|
109
|
+
statusExpired: translations.statusExpired,
|
|
110
|
+
statusFree: translations.statusFree,
|
|
111
|
+
statusCanceled: translations.statusCanceled,
|
|
112
|
+
expiresLabel: translations.expiresLabel,
|
|
113
|
+
purchasedLabel: translations.purchasedLabel,
|
|
114
|
+
lifetimeLabel: translations.lifetimeLabel,
|
|
115
|
+
creditsTitle: translations.creditsTitle,
|
|
116
|
+
remainingLabel: translations.remainingLabel,
|
|
117
|
+
manageButton: translations.manageButton,
|
|
118
|
+
upgradeButton: translations.upgradeButton,
|
|
119
|
+
},
|
|
120
|
+
onUpgrade: handleOpenPaywall,
|
|
196
121
|
upgradePrompt,
|
|
197
|
-
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
122
|
+
},
|
|
123
|
+
}), [
|
|
124
|
+
translations, isPremium, statusType, display, formattedExpirationDate,
|
|
125
|
+
formattedPurchaseDate, expiresAtIso, daysRemaining, willRenew,
|
|
126
|
+
creditsArray, handleOpenPaywall, upgradePrompt,
|
|
127
|
+
]);
|
|
203
128
|
};
|