@umituz/react-native-subscription 2.14.87 → 2.14.89
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 +3 -2
- package/src/domain/entities/SubscriptionStatus.ts +8 -1
- package/src/domains/wallet/presentation/components/TransactionItem.tsx +2 -1
- package/src/infrastructure/mappers/CreditsMapper.ts +27 -0
- package/src/infrastructure/repositories/CreditsRepository.ts +11 -2
- package/src/presentation/components/details/CreditRow.tsx +38 -24
- package/src/presentation/hooks/useSubscriptionDetails.ts +22 -36
- package/src/presentation/screens/components/CreditsList.tsx +2 -2
- package/src/presentation/types/SubscriptionDetailTypes.ts +2 -2
- package/src/presentation/utils/subscriptionDateUtils.ts +12 -11
- package/src/revenuecat/infrastructure/services/RevenueCatInitializer.ts +13 -11
- package/src/presentation/screens/components/CreditItem.tsx +0 -95
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.14.
|
|
3
|
+
"version": "2.14.89",
|
|
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",
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"@umituz/react-native-design-system": "latest",
|
|
36
36
|
"@umituz/react-native-firebase": "latest",
|
|
37
37
|
"@umituz/react-native-localization": "latest",
|
|
38
|
-
"@umituz/react-native-storage": "latest"
|
|
38
|
+
"@umituz/react-native-storage": "latest",
|
|
39
|
+
"@umituz/react-native-timezone": "^1.3.5"
|
|
39
40
|
},
|
|
40
41
|
"peerDependencies": {
|
|
41
42
|
"@tanstack/react-query": ">=5.0.0",
|
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
* Subscription Status Entity
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export
|
|
5
|
+
export const SUBSCRIPTION_STATUS = {
|
|
6
|
+
ACTIVE: 'active',
|
|
7
|
+
EXPIRED: 'expired',
|
|
8
|
+
CANCELED: 'canceled',
|
|
9
|
+
NONE: 'none',
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
export type SubscriptionStatusType = (typeof SUBSCRIPTION_STATUS)[keyof typeof SUBSCRIPTION_STATUS];
|
|
6
13
|
|
|
7
14
|
export interface SubscriptionStatus {
|
|
8
15
|
isPremium: boolean;
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
AtomicText,
|
|
13
13
|
AtomicIcon,
|
|
14
14
|
} from "@umituz/react-native-design-system";
|
|
15
|
+
import { timezoneService } from "@umituz/react-native-timezone";
|
|
15
16
|
import type { CreditLog, TransactionReason } from "../../domain/types/transaction.types";
|
|
16
17
|
|
|
17
18
|
export interface TransactionItemTranslations {
|
|
@@ -47,7 +48,7 @@ const getReasonIcon = (reason: TransactionReason): string => {
|
|
|
47
48
|
|
|
48
49
|
const defaultDateFormatter = (timestamp: number): string => {
|
|
49
50
|
const date = new Date(timestamp);
|
|
50
|
-
return
|
|
51
|
+
return timezoneService.formatDateTime(date, "en-US", {
|
|
51
52
|
month: "short",
|
|
52
53
|
day: "numeric",
|
|
53
54
|
hour: "2-digit",
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credits Mapper
|
|
3
|
+
* Maps Firestore data to UserCredits entity and vice-versa.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { UserCredits } from "../../domain/entities/Credits";
|
|
7
|
+
import type { UserCreditsDocumentRead } from "../models/UserCreditsDocument";
|
|
8
|
+
|
|
9
|
+
export class CreditsMapper {
|
|
10
|
+
static toEntity(snapData: UserCreditsDocumentRead): UserCredits {
|
|
11
|
+
return {
|
|
12
|
+
textCredits: snapData.textCredits,
|
|
13
|
+
imageCredits: snapData.imageCredits,
|
|
14
|
+
purchasedAt: snapData.purchasedAt?.toDate?.() || new Date(),
|
|
15
|
+
lastUpdatedAt: snapData.lastUpdatedAt?.toDate?.() || new Date(),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static toFirestore(data: Partial<UserCredits>): Record<string, any> {
|
|
20
|
+
return {
|
|
21
|
+
textCredits: data.textCredits,
|
|
22
|
+
imageCredits: data.imageCredits,
|
|
23
|
+
// Timestamps are usually handled by serverTimestamp() in repos,
|
|
24
|
+
// but we can map them if needed.
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -10,6 +10,8 @@ import { initializeCreditsTransaction } from "../services/CreditsInitializer";
|
|
|
10
10
|
import { detectPackageType } from "../../utils/packageTypeDetector";
|
|
11
11
|
import { getCreditAllocation } from "../../utils/creditMapper";
|
|
12
12
|
|
|
13
|
+
import { CreditsMapper } from "../mappers/CreditsMapper";
|
|
14
|
+
|
|
13
15
|
export class CreditsRepository extends BaseRepository {
|
|
14
16
|
constructor(private config: CreditsConfig) { super(); }
|
|
15
17
|
|
|
@@ -26,7 +28,7 @@ export class CreditsRepository extends BaseRepository {
|
|
|
26
28
|
const snap = await getDoc(this.getRef(db, userId));
|
|
27
29
|
if (!snap.exists()) return { success: true, data: undefined };
|
|
28
30
|
const d = snap.data() as UserCreditsDocumentRead;
|
|
29
|
-
return { success: true, data:
|
|
31
|
+
return { success: true, data: CreditsMapper.toEntity(d) };
|
|
30
32
|
} catch (e: any) { return { success: false, error: { message: e.message, code: "FETCH_ERR" } }; }
|
|
31
33
|
}
|
|
32
34
|
|
|
@@ -44,7 +46,14 @@ export class CreditsRepository extends BaseRepository {
|
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
48
|
const res = await initializeCreditsTransaction(db, this.getRef(db, userId), cfg, purchaseId);
|
|
47
|
-
return {
|
|
49
|
+
return {
|
|
50
|
+
success: true,
|
|
51
|
+
data: CreditsMapper.toEntity({
|
|
52
|
+
...res,
|
|
53
|
+
purchasedAt: { toDate: () => new Date() } as any,
|
|
54
|
+
lastUpdatedAt: { toDate: () => new Date() } as any,
|
|
55
|
+
})
|
|
56
|
+
};
|
|
48
57
|
} catch (e: any) { return { success: false, error: { message: e.message, code: "INIT_ERR" } }; }
|
|
49
58
|
}
|
|
50
59
|
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Credit Row Component
|
|
3
|
-
* Displays credit information with progress bar
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import React from "react";
|
|
1
|
+
import React, { useMemo } from "react";
|
|
7
2
|
import { View, StyleSheet } from "react-native";
|
|
8
3
|
import { useAppDesignTokens, AtomicText } from "@umituz/react-native-design-system";
|
|
9
4
|
|
|
@@ -22,56 +17,75 @@ export const CreditRow: React.FC<CreditRowProps> = ({
|
|
|
22
17
|
}) => {
|
|
23
18
|
const tokens = useAppDesignTokens();
|
|
24
19
|
const percentage = total > 0 ? (current / total) * 100 : 0;
|
|
25
|
-
|
|
20
|
+
|
|
21
|
+
// Progress color based on percentage
|
|
22
|
+
const progressColor = useMemo(() => {
|
|
23
|
+
if (percentage <= 20) return tokens.colors.error;
|
|
24
|
+
if (percentage <= 50) return tokens.colors.warning;
|
|
25
|
+
return tokens.colors.success;
|
|
26
|
+
}, [percentage, tokens.colors]);
|
|
26
27
|
|
|
27
28
|
return (
|
|
28
29
|
<View style={styles.container}>
|
|
29
30
|
<View style={styles.header}>
|
|
30
|
-
<AtomicText type="
|
|
31
|
+
<AtomicText type="bodyMedium" style={[styles.label, { color: tokens.colors.textPrimary }]}>
|
|
31
32
|
{label}
|
|
32
33
|
</AtomicText>
|
|
33
|
-
<
|
|
34
|
-
type="
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
>
|
|
39
|
-
{current} / {total} {remainingLabel}
|
|
40
|
-
</AtomicText>
|
|
34
|
+
<View style={[styles.badge, { backgroundColor: tokens.colors.surfaceSecondary }]}>
|
|
35
|
+
<AtomicText type="labelSmall" style={[styles.count, { color: progressColor }]}>
|
|
36
|
+
{current} / {total}
|
|
37
|
+
</AtomicText>
|
|
38
|
+
</View>
|
|
41
39
|
</View>
|
|
42
|
-
<View
|
|
43
|
-
style={[styles.progressBar, { backgroundColor: tokens.colors.surfaceSecondary }]}
|
|
44
|
-
>
|
|
40
|
+
<View style={[styles.progressBar, { backgroundColor: tokens.colors.surfaceSecondary }]}>
|
|
45
41
|
<View
|
|
46
42
|
style={[
|
|
47
43
|
styles.progressFill,
|
|
48
44
|
{
|
|
49
45
|
width: `${percentage}%`,
|
|
50
|
-
backgroundColor:
|
|
46
|
+
backgroundColor: progressColor,
|
|
51
47
|
},
|
|
52
48
|
]}
|
|
53
49
|
/>
|
|
54
50
|
</View>
|
|
51
|
+
{remainingLabel && (
|
|
52
|
+
<AtomicText type="bodySmall" style={{ color: tokens.colors.textSecondary }}>
|
|
53
|
+
{current} {remainingLabel}
|
|
54
|
+
</AtomicText>
|
|
55
|
+
)}
|
|
55
56
|
</View>
|
|
56
57
|
);
|
|
57
58
|
};
|
|
58
59
|
|
|
59
60
|
const styles = StyleSheet.create({
|
|
60
61
|
container: {
|
|
61
|
-
gap:
|
|
62
|
+
gap: 8,
|
|
63
|
+
marginVertical: 4,
|
|
62
64
|
},
|
|
63
65
|
header: {
|
|
64
66
|
flexDirection: "row",
|
|
65
67
|
justifyContent: "space-between",
|
|
66
68
|
alignItems: "center",
|
|
67
69
|
},
|
|
70
|
+
label: {
|
|
71
|
+
fontWeight: "500",
|
|
72
|
+
},
|
|
73
|
+
badge: {
|
|
74
|
+
paddingHorizontal: 8,
|
|
75
|
+
paddingVertical: 2,
|
|
76
|
+
borderRadius: 6,
|
|
77
|
+
},
|
|
78
|
+
count: {
|
|
79
|
+
fontWeight: "600",
|
|
80
|
+
},
|
|
68
81
|
progressBar: {
|
|
69
|
-
height:
|
|
70
|
-
borderRadius:
|
|
82
|
+
height: 8,
|
|
83
|
+
borderRadius: 4,
|
|
71
84
|
overflow: "hidden",
|
|
72
85
|
},
|
|
73
86
|
progressFill: {
|
|
74
87
|
height: "100%",
|
|
75
|
-
borderRadius:
|
|
88
|
+
borderRadius: 4,
|
|
76
89
|
},
|
|
77
90
|
});
|
|
91
|
+
|
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useSubscriptionDetails Hook
|
|
3
|
-
* Provides formatted subscription details for display
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
1
|
import { useMemo } from "react";
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from "../../
|
|
2
|
+
import {
|
|
3
|
+
type SubscriptionStatus,
|
|
4
|
+
SUBSCRIPTION_STATUS,
|
|
5
|
+
type SubscriptionStatusType
|
|
6
|
+
} from "../../domain/entities/SubscriptionStatus";
|
|
7
|
+
import { isSubscriptionExpired } from "../../utils/dateValidationUtils";
|
|
8
|
+
import { formatDateForLocale, calculateDaysRemaining } from "../utils/subscriptionDateUtils";
|
|
12
9
|
|
|
13
10
|
export interface SubscriptionDetails {
|
|
14
11
|
/** Raw subscription status */
|
|
@@ -26,7 +23,7 @@ export interface SubscriptionDetails {
|
|
|
26
23
|
/** Formatted purchase date */
|
|
27
24
|
formattedPurchaseDate: string | null;
|
|
28
25
|
/** Status text key for localization */
|
|
29
|
-
statusKey:
|
|
26
|
+
statusKey: SubscriptionStatusType;
|
|
30
27
|
}
|
|
31
28
|
|
|
32
29
|
interface UseSubscriptionDetailsParams {
|
|
@@ -34,24 +31,6 @@ interface UseSubscriptionDetailsParams {
|
|
|
34
31
|
locale?: string;
|
|
35
32
|
}
|
|
36
33
|
|
|
37
|
-
/**
|
|
38
|
-
* Format date to localized string
|
|
39
|
-
*/
|
|
40
|
-
function formatDate(dateString: string | null, locale: string): string | null {
|
|
41
|
-
if (!dateString) return null;
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
const date = new Date(dateString);
|
|
45
|
-
return date.toLocaleDateString(locale, {
|
|
46
|
-
year: "numeric",
|
|
47
|
-
month: "long",
|
|
48
|
-
day: "numeric",
|
|
49
|
-
});
|
|
50
|
-
} catch {
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
34
|
/**
|
|
56
35
|
* Hook for formatted subscription details
|
|
57
36
|
*/
|
|
@@ -70,18 +49,24 @@ export function useSubscriptionDetails(
|
|
|
70
49
|
daysRemaining: null,
|
|
71
50
|
formattedExpirationDate: null,
|
|
72
51
|
formattedPurchaseDate: null,
|
|
73
|
-
statusKey:
|
|
52
|
+
statusKey: SUBSCRIPTION_STATUS.NONE,
|
|
74
53
|
};
|
|
75
54
|
}
|
|
76
55
|
|
|
77
56
|
const isExpired = isSubscriptionExpired(status);
|
|
78
57
|
const isLifetime = status.isPremium && !status.expiresAt;
|
|
79
|
-
const
|
|
58
|
+
const daysRemainingValue = calculateDaysRemaining(status.expiresAt ?? null);
|
|
80
59
|
const isPremium = status.isPremium && !isExpired;
|
|
81
60
|
|
|
82
|
-
let statusKey:
|
|
61
|
+
let statusKey: SubscriptionStatusType = status.status || SUBSCRIPTION_STATUS.NONE;
|
|
62
|
+
|
|
63
|
+
// Override status key based on current calculation for active/expired
|
|
83
64
|
if (status.isPremium) {
|
|
84
|
-
statusKey = isExpired ?
|
|
65
|
+
statusKey = isExpired ? SUBSCRIPTION_STATUS.EXPIRED : SUBSCRIPTION_STATUS.ACTIVE;
|
|
66
|
+
} else if (status.status === SUBSCRIPTION_STATUS.CANCELED) {
|
|
67
|
+
statusKey = SUBSCRIPTION_STATUS.CANCELED;
|
|
68
|
+
} else {
|
|
69
|
+
statusKey = SUBSCRIPTION_STATUS.NONE;
|
|
85
70
|
}
|
|
86
71
|
|
|
87
72
|
return {
|
|
@@ -89,10 +74,11 @@ export function useSubscriptionDetails(
|
|
|
89
74
|
isPremium,
|
|
90
75
|
isExpired,
|
|
91
76
|
isLifetime,
|
|
92
|
-
daysRemaining,
|
|
93
|
-
formattedExpirationDate:
|
|
94
|
-
formattedPurchaseDate:
|
|
77
|
+
daysRemaining: daysRemainingValue,
|
|
78
|
+
formattedExpirationDate: formatDateForLocale(status.expiresAt ?? null, locale),
|
|
79
|
+
formattedPurchaseDate: formatDateForLocale(status.purchasedAt ?? null, locale),
|
|
95
80
|
statusKey,
|
|
96
81
|
};
|
|
97
82
|
}, [status, locale]);
|
|
98
83
|
}
|
|
84
|
+
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import React, { useMemo } from "react";
|
|
7
7
|
import { View, StyleSheet } from "react-native";
|
|
8
8
|
import { useAppDesignTokens, AtomicText } from "@umituz/react-native-design-system";
|
|
9
|
-
import {
|
|
9
|
+
import { CreditRow } from "../../components/details/CreditRow";
|
|
10
10
|
import type { CreditsListProps } from "../../types/SubscriptionDetailTypes";
|
|
11
11
|
|
|
12
12
|
export const CreditsList: React.FC<CreditsListProps> = ({
|
|
@@ -59,7 +59,7 @@ export const CreditsList: React.FC<CreditsListProps> = ({
|
|
|
59
59
|
)}
|
|
60
60
|
<View style={styles.list}>
|
|
61
61
|
{credits.map((credit) => (
|
|
62
|
-
<
|
|
62
|
+
<CreditRow
|
|
63
63
|
key={credit.id}
|
|
64
64
|
label={credit.label}
|
|
65
65
|
current={credit.current}
|
|
@@ -105,8 +105,8 @@ export interface CreditsListProps {
|
|
|
105
105
|
remainingLabel?: string;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
/** Props for credit
|
|
109
|
-
export interface
|
|
108
|
+
/** Props for credit row component */
|
|
109
|
+
export interface CreditRowProps {
|
|
110
110
|
label: string;
|
|
111
111
|
current: number;
|
|
112
112
|
total: number;
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Subscription Date Utilities
|
|
3
|
-
* Date formatting and calculation utilities for subscription
|
|
4
|
-
*/
|
|
1
|
+
import { timezoneService } from "@umituz/react-native-timezone";
|
|
5
2
|
|
|
6
3
|
/**
|
|
7
4
|
* Converts Firestore timestamp or Date to ISO string
|
|
@@ -14,11 +11,13 @@ export const convertPurchasedAt = (purchasedAt: unknown): string | null => {
|
|
|
14
11
|
purchasedAt !== null &&
|
|
15
12
|
"toDate" in purchasedAt
|
|
16
13
|
) {
|
|
17
|
-
return
|
|
14
|
+
return timezoneService.formatToISOString(
|
|
15
|
+
(purchasedAt as { toDate: () => Date }).toDate()
|
|
16
|
+
);
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
if (purchasedAt instanceof Date) {
|
|
21
|
-
return
|
|
20
|
+
return timezoneService.formatToISOString(purchasedAt);
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
return null;
|
|
@@ -34,11 +33,11 @@ export const formatDateForLocale = (
|
|
|
34
33
|
if (!dateStr) return null;
|
|
35
34
|
|
|
36
35
|
try {
|
|
37
|
-
return new
|
|
36
|
+
return timezoneService.formatDate(new Date(dateStr), locale, {
|
|
38
37
|
year: "numeric",
|
|
39
38
|
month: "long",
|
|
40
39
|
day: "numeric",
|
|
41
|
-
})
|
|
40
|
+
});
|
|
42
41
|
} catch {
|
|
43
42
|
return null;
|
|
44
43
|
}
|
|
@@ -52,9 +51,11 @@ export const calculateDaysRemaining = (
|
|
|
52
51
|
): number | null => {
|
|
53
52
|
if (!expiresAtIso) return null;
|
|
54
53
|
|
|
55
|
-
const
|
|
54
|
+
const expiresDate = new Date(expiresAtIso);
|
|
56
55
|
const now = new Date();
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
|
|
57
|
+
// Use timezoneService's mathematical approach if possible, or keep existing log
|
|
58
|
+
const diff = expiresDate.getTime() - now.getTime();
|
|
59
59
|
return Math.max(0, Math.ceil(diff / (1000 * 60 * 60 * 24)));
|
|
60
60
|
};
|
|
61
|
+
|
|
@@ -45,6 +45,16 @@ function configureLogHandler(): void {
|
|
|
45
45
|
isLogHandlerConfigured = true;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function buildSuccessResult(
|
|
49
|
+
deps: InitializerDeps,
|
|
50
|
+
customerInfo: any,
|
|
51
|
+
offerings: any
|
|
52
|
+
): InitializeResult {
|
|
53
|
+
const entitlementId = deps.config.entitlementIdentifier;
|
|
54
|
+
const hasPremium = !!customerInfo.entitlements.active[entitlementId];
|
|
55
|
+
return { success: true, offering: offerings.current, hasPremium };
|
|
56
|
+
}
|
|
57
|
+
|
|
48
58
|
export async function initializeSDK(
|
|
49
59
|
deps: InitializerDeps,
|
|
50
60
|
userId: string,
|
|
@@ -57,9 +67,7 @@ export async function initializeSDK(
|
|
|
57
67
|
Purchases.getCustomerInfo(),
|
|
58
68
|
Purchases.getOfferings(),
|
|
59
69
|
]);
|
|
60
|
-
|
|
61
|
-
const hasPremium = !!customerInfo.entitlements.active[entitlementId];
|
|
62
|
-
return { success: true, offering: offerings.current, hasPremium };
|
|
70
|
+
return buildSuccessResult(deps, customerInfo, offerings);
|
|
63
71
|
} catch {
|
|
64
72
|
return { success: false, offering: null, hasPremium: false };
|
|
65
73
|
}
|
|
@@ -82,10 +90,7 @@ export async function initializeSDK(
|
|
|
82
90
|
deps.setCurrentUserId(userId);
|
|
83
91
|
|
|
84
92
|
const offerings = await Purchases.getOfferings();
|
|
85
|
-
|
|
86
|
-
const hasPremium = !!customerInfo.entitlements.active[entitlementId];
|
|
87
|
-
|
|
88
|
-
return { success: true, offering: offerings.current, hasPremium };
|
|
93
|
+
return buildSuccessResult(deps, customerInfo, offerings);
|
|
89
94
|
} catch {
|
|
90
95
|
return { success: false, offering: null, hasPremium: false };
|
|
91
96
|
}
|
|
@@ -114,10 +119,7 @@ export async function initializeSDK(
|
|
|
114
119
|
Purchases.getOfferings(),
|
|
115
120
|
]);
|
|
116
121
|
|
|
117
|
-
|
|
118
|
-
const hasPremium = !!customerInfo.entitlements.active[entitlementId];
|
|
119
|
-
|
|
120
|
-
return { success: true, offering: offerings.current, hasPremium };
|
|
122
|
+
return buildSuccessResult(deps, customerInfo, offerings);
|
|
121
123
|
} catch (error) {
|
|
122
124
|
getErrorMessage(error, "RevenueCat init failed");
|
|
123
125
|
return { success: false, offering: null, hasPremium: false };
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Credit Item Component
|
|
3
|
-
* Displays individual credit usage with progress bar
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import React, { useMemo } from "react";
|
|
7
|
-
import { View, StyleSheet } from "react-native";
|
|
8
|
-
import { useAppDesignTokens, AtomicText } from "@umituz/react-native-design-system";
|
|
9
|
-
import type { CreditItemProps } from "../../types/SubscriptionDetailTypes";
|
|
10
|
-
|
|
11
|
-
export const CreditItem: React.FC<CreditItemProps> = ({
|
|
12
|
-
label,
|
|
13
|
-
current,
|
|
14
|
-
total,
|
|
15
|
-
remainingLabel,
|
|
16
|
-
}) => {
|
|
17
|
-
const tokens = useAppDesignTokens();
|
|
18
|
-
const percentage = total > 0 ? (current / total) * 100 : 0;
|
|
19
|
-
const isLow = percentage <= 20;
|
|
20
|
-
const isMedium = percentage > 20 && percentage <= 50;
|
|
21
|
-
|
|
22
|
-
const progressColor = useMemo(() => {
|
|
23
|
-
if (isLow) return tokens.colors.error;
|
|
24
|
-
if (isMedium) return tokens.colors.warning;
|
|
25
|
-
return tokens.colors.success;
|
|
26
|
-
}, [isLow, isMedium, tokens.colors]);
|
|
27
|
-
|
|
28
|
-
const styles = useMemo(
|
|
29
|
-
() =>
|
|
30
|
-
StyleSheet.create({
|
|
31
|
-
container: {
|
|
32
|
-
gap: tokens.spacing.sm,
|
|
33
|
-
},
|
|
34
|
-
header: {
|
|
35
|
-
flexDirection: "row",
|
|
36
|
-
justifyContent: "space-between",
|
|
37
|
-
alignItems: "center",
|
|
38
|
-
},
|
|
39
|
-
label: {
|
|
40
|
-
fontWeight: "500",
|
|
41
|
-
},
|
|
42
|
-
badge: {
|
|
43
|
-
paddingHorizontal: tokens.spacing.md,
|
|
44
|
-
paddingVertical: tokens.spacing.xs,
|
|
45
|
-
borderRadius: tokens.radius.md,
|
|
46
|
-
backgroundColor: tokens.colors.surfaceSecondary,
|
|
47
|
-
},
|
|
48
|
-
count: {
|
|
49
|
-
fontWeight: "600",
|
|
50
|
-
},
|
|
51
|
-
progressBar: {
|
|
52
|
-
height: 8,
|
|
53
|
-
borderRadius: tokens.radius.xs,
|
|
54
|
-
overflow: "hidden",
|
|
55
|
-
backgroundColor: tokens.colors.surfaceSecondary,
|
|
56
|
-
},
|
|
57
|
-
progressFill: {
|
|
58
|
-
height: "100%",
|
|
59
|
-
borderRadius: tokens.radius.xs,
|
|
60
|
-
width: `${percentage}%`,
|
|
61
|
-
backgroundColor: progressColor,
|
|
62
|
-
},
|
|
63
|
-
}),
|
|
64
|
-
[tokens, percentage, progressColor]
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
return (
|
|
68
|
-
<View style={styles.container}>
|
|
69
|
-
<View style={styles.header}>
|
|
70
|
-
<AtomicText
|
|
71
|
-
type="bodyMedium"
|
|
72
|
-
style={[styles.label, { color: tokens.colors.textPrimary }]}
|
|
73
|
-
>
|
|
74
|
-
{label}
|
|
75
|
-
</AtomicText>
|
|
76
|
-
<View style={styles.badge}>
|
|
77
|
-
<AtomicText
|
|
78
|
-
type="labelSmall"
|
|
79
|
-
style={[styles.count, { color: progressColor }]}
|
|
80
|
-
>
|
|
81
|
-
{current} / {total}
|
|
82
|
-
</AtomicText>
|
|
83
|
-
</View>
|
|
84
|
-
</View>
|
|
85
|
-
<View style={styles.progressBar}>
|
|
86
|
-
<View style={styles.progressFill} />
|
|
87
|
-
</View>
|
|
88
|
-
{remainingLabel && (
|
|
89
|
-
<AtomicText type="bodySmall" style={{ color: tokens.colors.textSecondary }}>
|
|
90
|
-
{current} {remainingLabel}
|
|
91
|
-
</AtomicText>
|
|
92
|
-
)}
|
|
93
|
-
</View>
|
|
94
|
-
);
|
|
95
|
-
};
|