@umituz/react-native-subscription 2.25.0 → 2.25.1
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 +2 -6
- package/src/domain/entities/SubscriptionStatus.ts +38 -3
- package/src/index.ts +8 -2
- package/src/infrastructure/mappers/CreditsMapper.ts +14 -34
- package/src/infrastructure/services/CreditsInitializer.ts +15 -25
- package/src/presentation/hooks/useSubscriptionSettingsConfig.utils.ts +11 -33
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.25.
|
|
3
|
+
"version": "2.25.1",
|
|
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",
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { SubscriptionPackageType } from "../../utils/packageTypeDetector";
|
|
9
|
+
import type { SubscriptionStatusType, PeriodType } from "./SubscriptionStatus";
|
|
9
10
|
|
|
10
11
|
export type CreditType = "text" | "image";
|
|
11
12
|
|
|
@@ -19,16 +20,11 @@ export type PurchaseSource =
|
|
|
19
20
|
|
|
20
21
|
export type PurchaseType = "initial" | "renewal" | "upgrade" | "downgrade";
|
|
21
22
|
|
|
22
|
-
export type SubscriptionStatus = "active" | "trial" | "trial_canceled" | "expired" | "canceled" | "free";
|
|
23
|
-
|
|
24
|
-
/** RevenueCat period types */
|
|
25
|
-
export type PeriodType = "NORMAL" | "INTRO" | "TRIAL";
|
|
26
|
-
|
|
27
23
|
/** Single Source of Truth for user subscription + credits data */
|
|
28
24
|
export interface UserCredits {
|
|
29
25
|
// Core subscription
|
|
30
26
|
isPremium: boolean;
|
|
31
|
-
status:
|
|
27
|
+
status: SubscriptionStatusType;
|
|
32
28
|
|
|
33
29
|
// Dates
|
|
34
30
|
purchasedAt: Date | null;
|
|
@@ -9,8 +9,14 @@ export const SUBSCRIPTION_STATUS = {
|
|
|
9
9
|
NONE: 'none',
|
|
10
10
|
} as const;
|
|
11
11
|
|
|
12
|
-
/** RevenueCat period
|
|
13
|
-
export
|
|
12
|
+
/** RevenueCat period type constants */
|
|
13
|
+
export const PERIOD_TYPE = {
|
|
14
|
+
NORMAL: 'NORMAL',
|
|
15
|
+
INTRO: 'INTRO',
|
|
16
|
+
TRIAL: 'TRIAL',
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
export type PeriodType = (typeof PERIOD_TYPE)[keyof typeof PERIOD_TYPE];
|
|
14
20
|
|
|
15
21
|
export type SubscriptionStatusType = (typeof SUBSCRIPTION_STATUS)[keyof typeof SUBSCRIPTION_STATUS];
|
|
16
22
|
|
|
@@ -35,7 +41,7 @@ export const createDefaultSubscriptionStatus = (): SubscriptionStatus => ({
|
|
|
35
41
|
purchasedAt: null,
|
|
36
42
|
customerId: null,
|
|
37
43
|
syncedAt: null,
|
|
38
|
-
status:
|
|
44
|
+
status: SUBSCRIPTION_STATUS.NONE,
|
|
39
45
|
});
|
|
40
46
|
|
|
41
47
|
export const isSubscriptionValid = (status: SubscriptionStatus | null): boolean => {
|
|
@@ -50,3 +56,32 @@ export const calculateDaysRemaining = (expiresAt: string | null): number | null
|
|
|
50
56
|
return timezoneService.getDaysUntil(new Date(expiresAt));
|
|
51
57
|
};
|
|
52
58
|
|
|
59
|
+
/** Subscription status resolver input */
|
|
60
|
+
export interface StatusResolverInput {
|
|
61
|
+
isPremium: boolean;
|
|
62
|
+
willRenew?: boolean;
|
|
63
|
+
isExpired?: boolean;
|
|
64
|
+
periodType?: PeriodType;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Resolves subscription status from input parameters
|
|
69
|
+
* Single source of truth for status determination logic
|
|
70
|
+
*/
|
|
71
|
+
export const resolveSubscriptionStatus = (input: StatusResolverInput): SubscriptionStatusType => {
|
|
72
|
+
const { isPremium, willRenew, isExpired, periodType } = input;
|
|
73
|
+
|
|
74
|
+
if (!isPremium || isExpired) {
|
|
75
|
+
return isExpired ? SUBSCRIPTION_STATUS.EXPIRED : SUBSCRIPTION_STATUS.NONE;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const isTrial = periodType === PERIOD_TYPE.TRIAL;
|
|
79
|
+
const isCanceled = willRenew === false;
|
|
80
|
+
|
|
81
|
+
if (isTrial) {
|
|
82
|
+
return isCanceled ? SUBSCRIPTION_STATUS.TRIAL_CANCELED : SUBSCRIPTION_STATUS.TRIAL;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return isCanceled ? SUBSCRIPTION_STATUS.CANCELED : SUBSCRIPTION_STATUS.ACTIVE;
|
|
86
|
+
};
|
|
87
|
+
|
package/src/index.ts
CHANGED
|
@@ -7,8 +7,14 @@ export * from "./domains/paywall";
|
|
|
7
7
|
export * from "./domains/config";
|
|
8
8
|
|
|
9
9
|
// Domain Layer
|
|
10
|
-
export {
|
|
11
|
-
|
|
10
|
+
export {
|
|
11
|
+
SUBSCRIPTION_STATUS,
|
|
12
|
+
PERIOD_TYPE,
|
|
13
|
+
createDefaultSubscriptionStatus,
|
|
14
|
+
isSubscriptionValid,
|
|
15
|
+
resolveSubscriptionStatus,
|
|
16
|
+
} from "./domain/entities/SubscriptionStatus";
|
|
17
|
+
export type { SubscriptionStatus, SubscriptionStatusType, PeriodType, StatusResolverInput } from "./domain/entities/SubscriptionStatus";
|
|
12
18
|
export type { SubscriptionConfig } from "./domain/value-objects/SubscriptionConfig";
|
|
13
19
|
export type { ISubscriptionRepository } from "./application/ports/ISubscriptionRepository";
|
|
14
20
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { UserCredits
|
|
1
|
+
import type { UserCredits } from "../../domain/entities/Credits";
|
|
2
|
+
import { resolveSubscriptionStatus, type PeriodType, type SubscriptionStatusType } from "../../domain/entities/SubscriptionStatus";
|
|
2
3
|
import type { UserCreditsDocumentRead } from "../models/UserCreditsDocument";
|
|
3
4
|
|
|
4
5
|
/** Maps Firestore document to domain entity with expiration validation */
|
|
@@ -51,43 +52,22 @@ export class CreditsMapper {
|
|
|
51
52
|
doc: UserCreditsDocumentRead,
|
|
52
53
|
expirationDate: Date | null,
|
|
53
54
|
periodType?: PeriodType
|
|
54
|
-
): { isPremium: boolean; status:
|
|
55
|
-
const
|
|
55
|
+
): { isPremium: boolean; status: SubscriptionStatusType } {
|
|
56
|
+
const isPremium = doc.isPremium ?? false;
|
|
56
57
|
const willRenew = doc.willRenew ?? false;
|
|
58
|
+
const isExpired = expirationDate ? expirationDate < new Date() : false;
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Check if subscription has expired
|
|
67
|
-
const isExpired = expirationDate < new Date();
|
|
68
|
-
|
|
69
|
-
if (isExpired) {
|
|
70
|
-
// Subscription expired - override document's isPremium
|
|
71
|
-
return { isPremium: false, status: "expired" };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Handle trial period
|
|
75
|
-
if (periodType === "TRIAL") {
|
|
76
|
-
return {
|
|
77
|
-
isPremium: docIsPremium,
|
|
78
|
-
status: willRenew === false ? "trial_canceled" : "trial",
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Handle canceled subscription (will not renew but still active)
|
|
83
|
-
if (docIsPremium && willRenew === false) {
|
|
84
|
-
return { isPremium: true, status: "canceled" };
|
|
85
|
-
}
|
|
60
|
+
const status = resolveSubscriptionStatus({
|
|
61
|
+
isPremium,
|
|
62
|
+
willRenew,
|
|
63
|
+
isExpired,
|
|
64
|
+
periodType,
|
|
65
|
+
});
|
|
86
66
|
|
|
87
|
-
//
|
|
67
|
+
// Override isPremium if expired
|
|
88
68
|
return {
|
|
89
|
-
isPremium:
|
|
90
|
-
status
|
|
69
|
+
isPremium: isExpired ? false : isPremium,
|
|
70
|
+
status,
|
|
91
71
|
};
|
|
92
72
|
}
|
|
93
73
|
}
|
|
@@ -15,9 +15,8 @@ import type {
|
|
|
15
15
|
PurchaseSource,
|
|
16
16
|
PurchaseType,
|
|
17
17
|
PurchaseMetadata,
|
|
18
|
-
SubscriptionDocStatus,
|
|
19
|
-
PeriodType,
|
|
20
18
|
} from "../models/UserCreditsDocument";
|
|
19
|
+
import { SUBSCRIPTION_STATUS, resolveSubscriptionStatus, type PeriodType } from "../../domain/entities/SubscriptionStatus";
|
|
21
20
|
import { TRIAL_CONFIG } from "./TrialService";
|
|
22
21
|
import { detectPackageType } from "../../utils/packageTypeDetector";
|
|
23
22
|
import { getCreditAllocation } from "../../utils/creditMapper";
|
|
@@ -126,34 +125,23 @@ export async function initializeCreditsTransaction(
|
|
|
126
125
|
? [...(existing?.purchaseHistory || []), purchaseMetadata].slice(-10)
|
|
127
126
|
: existing?.purchaseHistory;
|
|
128
127
|
|
|
129
|
-
// Determine subscription status
|
|
128
|
+
// Determine subscription status
|
|
130
129
|
const isPremium = metadata?.isPremium ?? true;
|
|
131
130
|
const willRenew = metadata?.willRenew;
|
|
132
131
|
const periodType = metadata?.periodType;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
// - active: premium and will renew (non-trial)
|
|
141
|
-
let status: SubscriptionDocStatus;
|
|
142
|
-
if (!isPremium) {
|
|
143
|
-
status = "expired";
|
|
144
|
-
} else if (isTrialing) {
|
|
145
|
-
status = willRenew === false ? "trial_canceled" : "trial";
|
|
146
|
-
} else if (willRenew === false) {
|
|
147
|
-
status = "canceled";
|
|
148
|
-
} else {
|
|
149
|
-
status = "active";
|
|
150
|
-
}
|
|
132
|
+
|
|
133
|
+
const status = resolveSubscriptionStatus({
|
|
134
|
+
isPremium,
|
|
135
|
+
willRenew,
|
|
136
|
+
isExpired: !isPremium,
|
|
137
|
+
periodType,
|
|
138
|
+
});
|
|
151
139
|
|
|
152
140
|
// Determine credits based on status
|
|
153
141
|
// Trial: 5 credits, Trial canceled: 0 credits, Normal: plan-based credits
|
|
154
|
-
if (status ===
|
|
142
|
+
if (status === SUBSCRIPTION_STATUS.TRIAL) {
|
|
155
143
|
newCredits = TRIAL_CONFIG.CREDITS;
|
|
156
|
-
} else if (status ===
|
|
144
|
+
} else if (status === SUBSCRIPTION_STATUS.TRIAL_CANCELED) {
|
|
157
145
|
newCredits = 0;
|
|
158
146
|
}
|
|
159
147
|
|
|
@@ -198,11 +186,13 @@ export async function initializeCreditsTransaction(
|
|
|
198
186
|
}
|
|
199
187
|
|
|
200
188
|
// Trial-specific fields
|
|
189
|
+
const isTrialing = status === SUBSCRIPTION_STATUS.TRIAL || status === SUBSCRIPTION_STATUS.TRIAL_CANCELED;
|
|
190
|
+
|
|
201
191
|
if (periodType) {
|
|
202
192
|
creditsData.periodType = periodType;
|
|
203
193
|
}
|
|
204
194
|
if (isTrialing) {
|
|
205
|
-
creditsData.isTrialing =
|
|
195
|
+
creditsData.isTrialing = status === SUBSCRIPTION_STATUS.TRIAL;
|
|
206
196
|
creditsData.trialCredits = TRIAL_CONFIG.CREDITS;
|
|
207
197
|
// Set trial dates if this is a new trial
|
|
208
198
|
if (!existing?.trialStartDate) {
|
|
@@ -211,7 +201,7 @@ export async function initializeCreditsTransaction(
|
|
|
211
201
|
if (metadata?.expirationDate) {
|
|
212
202
|
creditsData.trialEndDate = Timestamp.fromDate(new Date(metadata.expirationDate));
|
|
213
203
|
}
|
|
214
|
-
} else if (existing?.isTrialing &&
|
|
204
|
+
} else if (existing?.isTrialing && isPremium) {
|
|
215
205
|
// User converted from trial to paid
|
|
216
206
|
creditsData.isTrialing = false;
|
|
217
207
|
creditsData.convertedFromTrial = true;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { useMemo } from "react";
|
|
7
7
|
import type { UserCredits } from "../../domain/entities/Credits";
|
|
8
|
+
import { resolveSubscriptionStatus, type PeriodType, type SubscriptionStatusType } from "../../domain/entities/SubscriptionStatus";
|
|
8
9
|
import type { SubscriptionSettingsTranslations } from "../types/SubscriptionSettingsTypes";
|
|
9
10
|
|
|
10
11
|
export interface CreditsInfo {
|
|
@@ -38,42 +39,19 @@ export function useCreditsArray(
|
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
41
|
* Calculates subscription status type based on premium, renewal status, and period type
|
|
41
|
-
* @param isPremium - Whether user has premium subscription
|
|
42
|
-
* @param willRenew - Whether subscription will auto-renew (false = canceled)
|
|
43
|
-
* @param expiresAt - Expiration date ISO string (null for lifetime)
|
|
44
|
-
* @param periodType - RevenueCat period type: NORMAL, INTRO, or TRIAL
|
|
45
42
|
*/
|
|
46
43
|
export function getSubscriptionStatusType(
|
|
47
44
|
isPremium: boolean,
|
|
48
45
|
willRenew?: boolean,
|
|
49
46
|
expiresAt?: string | null,
|
|
50
|
-
periodType?:
|
|
51
|
-
):
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Check if expired
|
|
62
|
-
const now = new Date();
|
|
63
|
-
const expDate = new Date(expiresAt);
|
|
64
|
-
if (expDate < now) {
|
|
65
|
-
return "expired";
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Trial period handling
|
|
69
|
-
if (periodType === "TRIAL") {
|
|
70
|
-
return willRenew === false ? "trial_canceled" : "trial";
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Premium with willRenew=false means subscription is canceled but still active until expiration
|
|
74
|
-
if (willRenew === false) {
|
|
75
|
-
return "canceled";
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return "active";
|
|
47
|
+
periodType?: PeriodType
|
|
48
|
+
): SubscriptionStatusType {
|
|
49
|
+
const isExpired = expiresAt ? new Date(expiresAt) < new Date() : false;
|
|
50
|
+
|
|
51
|
+
return resolveSubscriptionStatus({
|
|
52
|
+
isPremium,
|
|
53
|
+
willRenew,
|
|
54
|
+
isExpired,
|
|
55
|
+
periodType,
|
|
56
|
+
});
|
|
79
57
|
}
|