@umituz/react-native-subscription 2.14.87 → 2.14.88

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.14.87",
3
+ "version": "2.14.88",
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",
@@ -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: { textCredits: d.textCredits, imageCredits: d.imageCredits, purchasedAt: d.purchasedAt?.toDate?.() || new Date(), lastUpdatedAt: d.lastUpdatedAt?.toDate?.() || new Date() } };
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 { success: true, data: { textCredits: res.textCredits, imageCredits: res.imageCredits, purchasedAt: new Date(), lastUpdatedAt: new Date() } };
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
- const isLow = percentage <= 20;
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="bodySmall" style={{ color: tokens.colors.textPrimary }}>
31
+ <AtomicText type="bodyMedium" style={[styles.label, { color: tokens.colors.textPrimary }]}>
31
32
  {label}
32
33
  </AtomicText>
33
- <AtomicText
34
- type="bodySmall"
35
- style={{
36
- color: isLow ? tokens.colors.warning : tokens.colors.textSecondary,
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: isLow ? tokens.colors.warning : tokens.colors.primary,
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: 4,
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: 6,
70
- borderRadius: 3,
82
+ height: 8,
83
+ borderRadius: 4,
71
84
  overflow: "hidden",
72
85
  },
73
86
  progressFill: {
74
87
  height: "100%",
75
- borderRadius: 3,
88
+ borderRadius: 4,
76
89
  },
77
90
  });
91
+
@@ -1,14 +1,7 @@
1
- /**
2
- * useSubscriptionDetails Hook
3
- * Provides formatted subscription details for display
4
- */
5
-
6
1
  import { useMemo } from "react";
7
2
  import type { SubscriptionStatus } from "../../domain/entities/SubscriptionStatus";
8
- import {
9
- getDaysUntilExpiration,
10
- isSubscriptionExpired,
11
- } from "../../utils/dateValidationUtils";
3
+ import { isSubscriptionExpired } from "../../utils/dateValidationUtils";
4
+ import { formatDateForLocale, calculateDaysRemaining } from "../utils/subscriptionDateUtils";
12
5
 
13
6
  export interface SubscriptionDetails {
14
7
  /** Raw subscription status */
@@ -34,24 +27,6 @@ interface UseSubscriptionDetailsParams {
34
27
  locale?: string;
35
28
  }
36
29
 
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
30
  /**
56
31
  * Hook for formatted subscription details
57
32
  */
@@ -76,7 +51,7 @@ export function useSubscriptionDetails(
76
51
 
77
52
  const isExpired = isSubscriptionExpired(status);
78
53
  const isLifetime = status.isPremium && !status.expiresAt;
79
- const daysRemaining = getDaysUntilExpiration(status);
54
+ const daysRemainingValue = calculateDaysRemaining(status.expiresAt ?? null);
80
55
  const isPremium = status.isPremium && !isExpired;
81
56
 
82
57
  let statusKey: "active" | "expired" | "none" = "none";
@@ -89,10 +64,11 @@ export function useSubscriptionDetails(
89
64
  isPremium,
90
65
  isExpired,
91
66
  isLifetime,
92
- daysRemaining,
93
- formattedExpirationDate: formatDate(status.expiresAt ?? null, locale),
94
- formattedPurchaseDate: formatDate(status.purchasedAt ?? null, locale),
67
+ daysRemaining: daysRemainingValue,
68
+ formattedExpirationDate: formatDateForLocale(status.expiresAt ?? null, locale),
69
+ formattedPurchaseDate: formatDateForLocale(status.purchasedAt ?? null, locale),
95
70
  statusKey,
96
71
  };
97
72
  }, [status, locale]);
98
73
  }
74
+
@@ -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 { CreditItem } from "./CreditItem";
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
- <CreditItem
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 item component */
109
- export interface CreditItemProps {
108
+ /** Props for credit row component */
109
+ export interface CreditRowProps {
110
110
  label: string;
111
111
  current: number;
112
112
  total: number;
@@ -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
- const entitlementId = deps.config.entitlementIdentifier;
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
- const entitlementId = deps.config.entitlementIdentifier;
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
- const entitlementId = deps.config.entitlementIdentifier;
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
- };