@umituz/react-native-subscription 2.40.3 → 2.40.5

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.
Files changed (25) hide show
  1. package/package.json +1 -1
  2. package/src/domains/credits/application/CreditsInitializer.ts +1 -1
  3. package/src/domains/credits/infrastructure/CreditsRepository.ts +1 -1
  4. package/src/domains/paywall/components/PaywallFeatures.tsx +4 -3
  5. package/src/domains/paywall/components/PaywallFooter.tsx +2 -2
  6. package/src/domains/paywall/components/PaywallScreen.tsx +13 -13
  7. package/src/domains/subscription/presentation/components/details/CreditRow.tsx +6 -7
  8. package/src/domains/subscription/presentation/components/details/DetailRow.tsx +2 -2
  9. package/src/domains/subscription/presentation/components/details/PremiumDetailsCard.tsx +4 -3
  10. package/src/domains/subscription/presentation/components/details/PremiumDetailsCardActions.tsx +2 -2
  11. package/src/domains/subscription/presentation/components/details/PremiumDetailsCardHeader.tsx +2 -2
  12. package/src/domains/subscription/presentation/components/details/PremiumStatusBadge.tsx +2 -2
  13. package/src/domains/subscription/presentation/components/sections/SubscriptionSection.tsx +2 -2
  14. package/src/domains/subscription/presentation/screens/SubscriptionDetailScreen.tsx +2 -2
  15. package/src/domains/subscription/presentation/screens/components/CreditsList.tsx +4 -3
  16. package/src/domains/subscription/presentation/screens/components/SubscriptionHeader.tsx +2 -2
  17. package/src/domains/subscription/presentation/screens/components/UpgradePrompt.tsx +4 -3
  18. package/src/domains/subscription/utils/progressCalculations.ts +55 -0
  19. package/src/domains/wallet/presentation/components/BalanceCard.tsx +2 -2
  20. package/src/domains/wallet/presentation/components/TransactionItem.tsx +2 -2
  21. package/src/domains/wallet/presentation/components/TransactionList.tsx +23 -12
  22. package/src/domains/wallet/presentation/components/TransactionListStates.tsx +4 -4
  23. package/src/domains/wallet/presentation/screens/WalletScreen.tsx +2 -2
  24. package/src/shared/utils/arrayUtils.ts +65 -0
  25. package/src/shared/utils/stringUtils.ts +64 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.40.3",
3
+ "version": "2.40.5",
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",
@@ -2,7 +2,7 @@ import type { CreditsConfig } from "../core/Credits";
2
2
  import { getAppVersion, validatePlatform } from "../../../utils/appUtils";
3
3
 
4
4
  import type { InitializeCreditsMetadata, InitializationResult } from "../../subscription/application/SubscriptionInitializerTypes";
5
- import { runTransaction, serverTimestamp, type Transaction, type DocumentReference, type Firestore } from "@umituz/react-native-firebase";
5
+ import { runTransaction, type Transaction, type DocumentReference, type Firestore } from "@umituz/react-native-firebase";
6
6
  import { getCreditDocumentOrDefault } from "./creditDocumentHelpers";
7
7
  import { calculateNewCredits, buildCreditsData } from "./creditOperationUtils";
8
8
  import { calculateCreditLimit } from "./CreditLimitCalculator";
@@ -93,7 +93,7 @@ export class CreditsRepository extends BaseRepository {
93
93
  willRenew: boolean,
94
94
  expirationDate: string | null,
95
95
  periodType: string | null,
96
- storeTransactionId?: string | null,
96
+ _storeTransactionId?: string | null,
97
97
  ): Promise<boolean> {
98
98
  const db = requireFirestore();
99
99
  const creditLimit = calculateCreditLimit(productId, this.config);
@@ -4,10 +4,11 @@ import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms
4
4
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
5
5
  import type { SubscriptionFeature } from "../entities/types";
6
6
  import { paywallScreenStyles as styles } from "./PaywallScreen.styles";
7
+ import { isEmptyArray } from "../../../shared/utils/arrayUtils";
7
8
 
8
- export const PaywallFeatures: React.FC<{ features: SubscriptionFeature[] }> = ({ features }) => {
9
+ export const PaywallFeatures: React.FC<{ features: SubscriptionFeature[] }> = React.memo(({ features }) => {
9
10
  const tokens = useAppDesignTokens();
10
- if (!features.length) return null;
11
+ if (isEmptyArray(features)) return null;
11
12
 
12
13
  return (
13
14
  <View style={[styles.featuresContainer, { backgroundColor: tokens.colors.surfaceSecondary }]}>
@@ -23,4 +24,4 @@ export const PaywallFeatures: React.FC<{ features: SubscriptionFeature[] }> = ({
23
24
  ))}
24
25
  </View>
25
26
  );
26
- };
27
+ });
@@ -13,7 +13,7 @@ interface PaywallFooterProps {
13
13
  onLegalClick: (url: string | undefined) => void;
14
14
  }
15
15
 
16
- export const PaywallFooter: React.FC<PaywallFooterProps> = ({
16
+ export const PaywallFooter: React.FC<PaywallFooterProps> = React.memo(({
17
17
  translations,
18
18
  legalUrls,
19
19
  isProcessing,
@@ -49,4 +49,4 @@ export const PaywallFooter: React.FC<PaywallFooterProps> = ({
49
49
  </View>
50
50
  </View>
51
51
  );
52
- };
52
+ });
@@ -5,12 +5,12 @@
5
5
  */
6
6
 
7
7
  import React, { useCallback, useEffect, useMemo } from "react";
8
- import {
9
- View,
10
- TouchableOpacity,
11
- Linking,
12
- FlatList,
13
- ListRenderItem,
8
+ import {
9
+ View,
10
+ TouchableOpacity,
11
+ Linking,
12
+ FlatList,
13
+ ListRenderItem,
14
14
  StatusBar,
15
15
  } from "react-native";
16
16
  import { AtomicText, AtomicIcon, AtomicSpinner } from "@umituz/react-native-design-system/atoms";
@@ -20,13 +20,13 @@ import { Image } from "expo-image";
20
20
  import { PlanCard } from "./PlanCard";
21
21
  import { paywallScreenStyles as styles } from "./PaywallScreen.styles";
22
22
  import { PaywallFooter } from "./PaywallFooter";
23
- import { PurchaseLoadingOverlay } from "../../subscription/presentation/components/overlay/PurchaseLoadingOverlay";
24
23
  import { usePaywallActions } from "../hooks/usePaywallActions";
25
24
  import { PaywallScreenProps } from "./PaywallScreen.types";
26
- import {
27
- calculatePaywallItemLayout,
28
- type PaywallListItem
25
+ import {
26
+ calculatePaywallItemLayout,
27
+ type PaywallListItem
29
28
  } from "../utils/paywallLayoutUtils";
29
+ import { hasItems } from "../../../shared/utils/arrayUtils";
30
30
 
31
31
  export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) => {
32
32
  const {
@@ -78,7 +78,7 @@ export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) =>
78
78
 
79
79
  // Auto-select first package
80
80
  useEffect(() => {
81
- if (packages.length > 0 && !selectedPlanId) {
81
+ if (hasItems(packages) && !selectedPlanId) {
82
82
  setSelectedPlanId(packages[0].product.identifier);
83
83
  }
84
84
  }, [packages, selectedPlanId, setSelectedPlanId]);
@@ -100,7 +100,7 @@ export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) =>
100
100
  data.push({ type: 'HEADER' });
101
101
 
102
102
  // 2. Features Section
103
- if (features.length > 0) {
103
+ if (hasItems(features)) {
104
104
  data.push({ type: 'FEATURE_HEADER' });
105
105
  features.forEach(feature => {
106
106
  data.push({ type: 'FEATURE', feature });
@@ -108,7 +108,7 @@ export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) =>
108
108
  }
109
109
 
110
110
  // 3. Plans Section
111
- if (packages.length > 0) {
111
+ if (hasItems(packages)) {
112
112
  data.push({ type: 'PLAN_HEADER' });
113
113
  packages.forEach(pkg => {
114
114
  data.push({ type: 'PLAN', pkg });
@@ -2,6 +2,7 @@ import React, { useMemo } from "react";
2
2
  import { View, StyleSheet } from "react-native";
3
3
  import { AtomicText } from "@umituz/react-native-design-system/atoms";
4
4
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
5
+ import { calculatePercentage, getProgressColor } from "../../../utils/progressCalculations";
5
6
 
6
7
  interface CreditRowProps {
7
8
  label: string;
@@ -10,20 +11,18 @@ interface CreditRowProps {
10
11
  remainingLabel?: string;
11
12
  }
12
13
 
13
- export const CreditRow: React.FC<CreditRowProps> = ({
14
+ export const CreditRow: React.FC<CreditRowProps> = React.memo(({
14
15
  label,
15
16
  current,
16
17
  total,
17
18
  remainingLabel,
18
19
  }) => {
19
20
  const tokens = useAppDesignTokens();
20
- const percentage = total > 0 ? (current / total) * 100 : 0;
21
-
21
+ const percentage = useMemo(() => calculatePercentage(current, total), [current, total]);
22
+
22
23
  // Progress color based on percentage
23
24
  const progressColor = useMemo(() => {
24
- if (percentage <= 20) return tokens.colors.error;
25
- if (percentage <= 50) return tokens.colors.warning;
26
- return tokens.colors.success;
25
+ return getProgressColor(percentage, tokens.colors);
27
26
  }, [percentage, tokens.colors]);
28
27
 
29
28
  return (
@@ -56,7 +55,7 @@ export const CreditRow: React.FC<CreditRowProps> = ({
56
55
  )}
57
56
  </View>
58
57
  );
59
- };
58
+ });
60
59
 
61
60
  const styles = StyleSheet.create({
62
61
  container: {
@@ -17,7 +17,7 @@ interface DetailRowProps {
17
17
  valueStyle?: TextStyle;
18
18
  }
19
19
 
20
- export const DetailRow: React.FC<DetailRowProps> = ({
20
+ export const DetailRow: React.FC<DetailRowProps> = React.memo(({
21
21
  label,
22
22
  value,
23
23
  highlight = false,
@@ -49,7 +49,7 @@ export const DetailRow: React.FC<DetailRowProps> = ({
49
49
  </AtomicText>
50
50
  </View>
51
51
  );
52
- };
52
+ });
53
53
 
54
54
  const styles = StyleSheet.create({
55
55
  container: {
@@ -9,10 +9,11 @@ import type { PremiumDetailsCardProps } from "./PremiumDetailsCardTypes";
9
9
  import { PremiumDetailsCardHeader } from "./PremiumDetailsCardHeader";
10
10
  import { PremiumDetailsCardActions } from "./PremiumDetailsCardActions";
11
11
  import { shouldHighlightExpiration } from "../../../../subscription/utils/expirationHelpers";
12
+ import { hasItems } from "../../../../../shared/utils/arrayUtils";
12
13
 
13
14
  export type { CreditInfo, PremiumDetailsTranslations, PremiumDetailsCardProps } from "./PremiumDetailsCardTypes";
14
15
 
15
- export const PremiumDetailsCard: React.FC<PremiumDetailsCardProps> = ({
16
+ export const PremiumDetailsCard: React.FC<PremiumDetailsCardProps> = React.memo(({
16
17
  statusType,
17
18
  isPremium,
18
19
  expirationDate,
@@ -24,7 +25,7 @@ export const PremiumDetailsCard: React.FC<PremiumDetailsCardProps> = ({
24
25
  onUpgrade,
25
26
  }) => {
26
27
  const tokens = useAppDesignTokens();
27
- const showCredits = isPremium && credits && credits.length > 0;
28
+ const showCredits = isPremium && hasItems(credits);
28
29
 
29
30
  return (
30
31
  <View style={[styles.card, { backgroundColor: tokens.colors.surface }]}>
@@ -67,4 +68,4 @@ export const PremiumDetailsCard: React.FC<PremiumDetailsCardProps> = ({
67
68
  />
68
69
  </View>
69
70
  );
70
- };
71
+ });
@@ -12,7 +12,7 @@ interface PremiumDetailsCardActionsProps {
12
12
  translations: PremiumDetailsTranslations;
13
13
  }
14
14
 
15
- export const PremiumDetailsCardActions: React.FC<PremiumDetailsCardActionsProps> = ({
15
+ export const PremiumDetailsCardActions: React.FC<PremiumDetailsCardActionsProps> = React.memo(({
16
16
  isPremium,
17
17
  onManageSubscription,
18
18
  onUpgrade,
@@ -44,4 +44,4 @@ export const PremiumDetailsCardActions: React.FC<PremiumDetailsCardActionsProps>
44
44
  )}
45
45
  </View>
46
46
  );
47
- };
47
+ });
@@ -12,7 +12,7 @@ interface PremiumDetailsCardHeaderProps {
12
12
  translations: PremiumDetailsTranslations;
13
13
  }
14
14
 
15
- export const PremiumDetailsCardHeader: React.FC<PremiumDetailsCardHeaderProps> = ({ statusType, translations }) => {
15
+ export const PremiumDetailsCardHeader: React.FC<PremiumDetailsCardHeaderProps> = React.memo(({ statusType, translations }) => {
16
16
  const tokens = useAppDesignTokens();
17
17
 
18
18
  return (
@@ -31,4 +31,4 @@ export const PremiumDetailsCardHeader: React.FC<PremiumDetailsCardHeaderProps> =
31
31
  />
32
32
  </View>
33
33
  );
34
- };
34
+ });
@@ -13,7 +13,7 @@ import type { SubscriptionStatusType } from "../../../core/SubscriptionConstants
13
13
 
14
14
  export type { PremiumStatusBadgeProps };
15
15
 
16
- export const PremiumStatusBadge: React.FC<PremiumStatusBadgeProps> = ({
16
+ export const PremiumStatusBadge: React.FC<PremiumStatusBadgeProps> = React.memo(({
17
17
  status,
18
18
  activeLabel,
19
19
  expiredLabel,
@@ -63,4 +63,4 @@ export const PremiumStatusBadge: React.FC<PremiumStatusBadgeProps> = ({
63
63
  </AtomicText>
64
64
  </View>
65
65
  );
66
- };
66
+ });
@@ -11,7 +11,7 @@ import type { SubscriptionSectionConfig, SubscriptionSectionProps } from "./Subs
11
11
 
12
12
  export type { SubscriptionSectionConfig, SubscriptionSectionProps };
13
13
 
14
- export const SubscriptionSection: React.FC<SubscriptionSectionProps> = ({
14
+ export const SubscriptionSection: React.FC<SubscriptionSectionProps> = React.memo(({
15
15
  config,
16
16
  containerStyle,
17
17
  }) => {
@@ -42,4 +42,4 @@ export const SubscriptionSection: React.FC<SubscriptionSectionProps> = ({
42
42
  }
43
43
 
44
44
  return <View style={containerStyle}>{content}</View>;
45
- };
45
+ });
@@ -15,7 +15,7 @@ import { SubscriptionDetailScreenProps } from "./SubscriptionDetailScreen.types"
15
15
 
16
16
  const IS_DEV = typeof __DEV__ !== "undefined" && __DEV__;
17
17
 
18
- export const SubscriptionDetailScreen: React.FC<SubscriptionDetailScreenProps> = ({ config }) => {
18
+ export const SubscriptionDetailScreen: React.FC<SubscriptionDetailScreenProps> = React.memo(({ config }) => {
19
19
  const tokens = useAppDesignTokens();
20
20
  const { showHeader, showCredits, showUpgradePrompt, showExpirationDate } = config.display;
21
21
 
@@ -91,7 +91,7 @@ export const SubscriptionDetailScreen: React.FC<SubscriptionDetailScreenProps> =
91
91
  </View>
92
92
  </ScreenLayout>
93
93
  );
94
- };
94
+ });
95
95
 
96
96
  /* ─── DEV TEST PANEL ─── Only rendered in __DEV__ ─── */
97
97
 
@@ -8,6 +8,7 @@ import { View, StyleSheet } from "react-native";
8
8
  import { AtomicText } from "@umituz/react-native-design-system/atoms";
9
9
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
10
10
  import { CreditRow } from "../../components/details/CreditRow";
11
+ import { isEmptyArray } from "../../../../../shared/utils/arrayUtils";
11
12
 
12
13
  interface CreditItem {
13
14
  id: string;
@@ -23,7 +24,7 @@ interface CreditsListProps {
23
24
  remainingLabel?: string;
24
25
  }
25
26
 
26
- export const CreditsList: React.FC<CreditsListProps> = ({
27
+ export const CreditsList: React.FC<CreditsListProps> = React.memo(({
27
28
  credits,
28
29
  title,
29
30
  description,
@@ -53,7 +54,7 @@ export const CreditsList: React.FC<CreditsListProps> = ({
53
54
  [tokens]
54
55
  );
55
56
 
56
- if (!credits || credits.length === 0) return null;
57
+ if (!credits || isEmptyArray(credits)) return null;
57
58
 
58
59
  return (
59
60
  <View style={styles.container}>
@@ -86,4 +87,4 @@ export const CreditsList: React.FC<CreditsListProps> = ({
86
87
  </View>
87
88
  </View>
88
89
  );
89
- };
90
+ });
@@ -9,7 +9,7 @@ import { SubscriptionHeaderContent } from "./SubscriptionHeaderContent";
9
9
 
10
10
  const EXPIRING_SOON_THRESHOLD_DAYS = 7;
11
11
 
12
- export const SubscriptionHeader: React.FC<SubscriptionHeaderProps> = ({
12
+ export const SubscriptionHeader: React.FC<SubscriptionHeaderProps> = React.memo(({
13
13
  statusType,
14
14
  showExpirationDate,
15
15
  expirationDate,
@@ -67,4 +67,4 @@ export const SubscriptionHeader: React.FC<SubscriptionHeaderProps> = ({
67
67
  />
68
68
  </View>
69
69
  );
70
- };
70
+ });
@@ -3,8 +3,9 @@ import { View, StyleSheet, TouchableOpacity } from "react-native";
3
3
  import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
4
4
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
5
5
  import { UpgradePromptProps } from "./UpgradePrompt.types";
6
+ import { hasItems } from "../../../../../shared/utils/arrayUtils";
6
7
 
7
- export const UpgradePrompt: React.FC<UpgradePromptProps> = ({ title, subtitle, benefits, upgradeButtonLabel, onUpgrade }) => {
8
+ export const UpgradePrompt: React.FC<UpgradePromptProps> = React.memo(({ title, subtitle, benefits, upgradeButtonLabel, onUpgrade }) => {
8
9
  const tokens = useAppDesignTokens();
9
10
  const styles = useMemo(() => StyleSheet.create({
10
11
  container: { gap: tokens.spacing.lg },
@@ -39,7 +40,7 @@ export const UpgradePrompt: React.FC<UpgradePromptProps> = ({ title, subtitle, b
39
40
  <AtomicText type="headlineSmall" style={[styles.title, { color: tokens.colors.textPrimary }]}>{title}</AtomicText>
40
41
  {subtitle && <AtomicText type="bodyMedium" style={[styles.subtitle, { color: tokens.colors.textSecondary }]}>{subtitle}</AtomicText>}
41
42
  </View>
42
- {benefits && benefits.length > 0 && (
43
+ {hasItems(benefits) && (
43
44
  <View style={styles.benefitsCard}>
44
45
  {benefits.map((benefit) => (
45
46
  <View key={benefit.text} style={styles.benefitItem}>
@@ -58,4 +59,4 @@ export const UpgradePrompt: React.FC<UpgradePromptProps> = ({ title, subtitle, b
58
59
  )}
59
60
  </View>
60
61
  );
61
- };
62
+ });
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Progress calculation utilities
3
+ * All progress bar and percentage calculations
4
+ */
5
+
6
+ /**
7
+ * Calculate percentage for progress bars
8
+ * Returns 0 for invalid inputs (zero or negative total)
9
+ */
10
+ export function calculatePercentage(current: number, total: number): number {
11
+ if (total <= 0) return 0;
12
+ return (current / total) * 100;
13
+ }
14
+
15
+ /**
16
+ * Determine progress color based on percentage thresholds
17
+ * @param percentage - Progress percentage (0-100)
18
+ * @param colors - Color object with error, warning, and success colors
19
+ * @returns Appropriate color based on percentage
20
+ */
21
+ export interface ProgressColors {
22
+ error: string;
23
+ warning: string;
24
+ success: string;
25
+ }
26
+
27
+ export function getProgressColor(
28
+ percentage: number,
29
+ colors: ProgressColors
30
+ ): string {
31
+ if (percentage <= 20) return colors.error;
32
+ if (percentage <= 50) return colors.warning;
33
+ return colors.success;
34
+ }
35
+
36
+ /**
37
+ * Check if percentage is in critical range (<= 20%)
38
+ */
39
+ export function isCriticalPercentage(percentage: number): boolean {
40
+ return percentage <= 20;
41
+ }
42
+
43
+ /**
44
+ * Check if percentage is in warning range (21-50%)
45
+ */
46
+ export function isWarningPercentage(percentage: number): boolean {
47
+ return percentage > 20 && percentage <= 50;
48
+ }
49
+
50
+ /**
51
+ * Check if percentage is in healthy range (> 50%)
52
+ */
53
+ export function isHealthyPercentage(percentage: number): boolean {
54
+ return percentage > 50;
55
+ }
@@ -14,7 +14,7 @@ import type { BalanceCardTranslations, BalanceCardProps } from "./BalanceCard.ty
14
14
 
15
15
  export type { BalanceCardTranslations };
16
16
 
17
- export const BalanceCard: React.FC<BalanceCardProps> = ({
17
+ export const BalanceCard: React.FC<BalanceCardProps> = React.memo(({
18
18
  balance,
19
19
  translations,
20
20
  iconName = "wallet",
@@ -58,7 +58,7 @@ export const BalanceCard: React.FC<BalanceCardProps> = ({
58
58
  </View>
59
59
  </View>
60
60
  );
61
- };
61
+ });
62
62
 
63
63
  const styles = StyleSheet.create({
64
64
  container: {
@@ -7,7 +7,7 @@ import { transactionItemStyles } from "./TransactionItem.styles";
7
7
  import type { TransactionItemProps } from "./TransactionItem.types";
8
8
  import { defaultDateFormatter, getReasonLabel, getChangePrefix } from "./transactionItemHelpers";
9
9
 
10
- export const TransactionItem: React.FC<TransactionItemProps> = ({
10
+ export const TransactionItem: React.FC<TransactionItemProps> = React.memo(({
11
11
  transaction,
12
12
  translations,
13
13
  dateFormatter = defaultDateFormatter,
@@ -44,4 +44,4 @@ export const TransactionItem: React.FC<TransactionItemProps> = ({
44
44
  </AtomicText>
45
45
  </View>
46
46
  );
47
- };
47
+ });
@@ -1,5 +1,5 @@
1
- import React from "react";
2
- import { View, ScrollView } from "react-native";
1
+ import React, { useCallback, useMemo } from "react";
2
+ import { View, FlatList } from "react-native";
3
3
  import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
4
4
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
5
5
  import { TransactionItem } from "./TransactionItem";
@@ -7,10 +7,11 @@ import { transactionListStyles } from "./TransactionList.styles";
7
7
  import { LoadingState, EmptyState } from "./TransactionListStates";
8
8
  import { DEFAULT_TRANSACTION_LIST_MAX_HEIGHT } from "./TransactionList.constants";
9
9
  import type { TransactionListTranslations, TransactionListProps } from "./TransactionList.types";
10
+ import { isEmptyArray } from "../../../../shared/utils/arrayUtils";
10
11
 
11
12
  export type { TransactionListTranslations };
12
13
 
13
- export const TransactionList: React.FC<TransactionListProps> = ({
14
+ export const TransactionList: React.FC<TransactionListProps> = React.memo(({
14
15
  transactions,
15
16
  loading,
16
17
  translations,
@@ -19,6 +20,13 @@ export const TransactionList: React.FC<TransactionListProps> = ({
19
20
  }) => {
20
21
  const tokens = useAppDesignTokens();
21
22
 
23
+ const keyExtractor = useCallback((item: typeof transactions[0]) => item.id, []);
24
+ const renderItem = useCallback(({ item }: { item: typeof transactions[0] }) => (
25
+ <TransactionItem transaction={item} translations={translations} dateFormatter={dateFormatter} />
26
+ ), [translations, dateFormatter]);
27
+
28
+ const listStyle = useMemo(() => [transactionListStyles.scrollView, { maxHeight }], [maxHeight]);
29
+
22
30
  return (
23
31
  <View style={transactionListStyles.container}>
24
32
  <View style={transactionListStyles.header}>
@@ -30,19 +38,22 @@ export const TransactionList: React.FC<TransactionListProps> = ({
30
38
 
31
39
  {loading ? (
32
40
  <LoadingState message={translations.loading} />
33
- ) : transactions.length === 0 ? (
41
+ ) : isEmptyArray(transactions) ? (
34
42
  <EmptyState message={translations.empty} />
35
43
  ) : (
36
- <ScrollView
37
- style={[transactionListStyles.scrollView, { maxHeight }]}
44
+ <FlatList
45
+ data={transactions}
46
+ renderItem={renderItem}
47
+ keyExtractor={keyExtractor}
48
+ style={listStyle}
38
49
  contentContainerStyle={transactionListStyles.scrollContent}
39
50
  showsVerticalScrollIndicator={false}
40
- >
41
- {transactions.map((transaction) => (
42
- <TransactionItem key={transaction.id} transaction={transaction} translations={translations} dateFormatter={dateFormatter} />
43
- ))}
44
- </ScrollView>
51
+ removeClippedSubviews={true}
52
+ maxToRenderPerBatch={10}
53
+ windowSize={5}
54
+ initialNumToRender={10}
55
+ />
45
56
  )}
46
57
  </View>
47
58
  );
48
- };
59
+ });
@@ -8,15 +8,15 @@ interface LoadingStateProps {
8
8
  message: string;
9
9
  }
10
10
 
11
- export const LoadingState: React.FC<LoadingStateProps> = ({ message }) => (
11
+ export const LoadingState: React.FC<LoadingStateProps> = React.memo(({ message }) => (
12
12
  <AtomicSpinner size="lg" color="primary" text={message} style={transactionListStyles.stateContainer} />
13
- );
13
+ ));
14
14
 
15
15
  interface EmptyStateProps {
16
16
  message: string;
17
17
  }
18
18
 
19
- export const EmptyState: React.FC<EmptyStateProps> = ({ message }) => {
19
+ export const EmptyState: React.FC<EmptyStateProps> = React.memo(({ message }) => {
20
20
  const tokens = useAppDesignTokens();
21
21
  return (
22
22
  <View style={transactionListStyles.stateContainer}>
@@ -26,4 +26,4 @@ export const EmptyState: React.FC<EmptyStateProps> = ({ message }) => {
26
26
  </AtomicText>
27
27
  </View>
28
28
  );
29
- };
29
+ });
@@ -10,7 +10,7 @@ import { BalanceCard } from "../components/BalanceCard";
10
10
  import { TransactionList } from "../components/TransactionList";
11
11
  import { WalletScreenProps } from "./WalletScreen.types";
12
12
 
13
- export const WalletScreen: React.FC<WalletScreenProps> = ({ translations, onBack, dateFormatter, footer }) => {
13
+ export const WalletScreen: React.FC<WalletScreenProps> = React.memo(({ translations, onBack, dateFormatter, footer }) => {
14
14
  const tokens = useAppDesignTokens();
15
15
  const navigation = useNavigation();
16
16
  const config = getWalletConfig();
@@ -57,7 +57,7 @@ export const WalletScreen: React.FC<WalletScreenProps> = ({ translations, onBack
57
57
  />
58
58
  </ScreenLayout>
59
59
  );
60
- };
60
+ });
61
61
 
62
62
  const styles = StyleSheet.create({
63
63
  content: { paddingBottom: 24 },
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Array validation and utility functions
3
+ */
4
+
5
+ /**
6
+ * Check if array has items
7
+ */
8
+ export function hasItems<T>(arr: readonly T[] | T[] | null | undefined): boolean {
9
+ return Array.isArray(arr) && arr.length > 0;
10
+ }
11
+
12
+ /**
13
+ * Check if array is empty
14
+ */
15
+ export function isEmptyArray<T>(arr: readonly T[] | T[] | null | undefined): boolean {
16
+ return !Array.isArray(arr) || arr.length === 0;
17
+ }
18
+
19
+ /**
20
+ * Get array length safely (returns 0 for null/undefined)
21
+ */
22
+ export function getArrayLength<T>(arr: T[] | null | undefined): number {
23
+ return Array.isArray(arr) ? arr.length : 0;
24
+ }
25
+
26
+ /**
27
+ * Find item in array by predicate
28
+ */
29
+ export function findItem<T>(
30
+ arr: T[] | null | undefined,
31
+ predicate: (item: T) => boolean
32
+ ): T | undefined {
33
+ if (!Array.isArray(arr)) return undefined;
34
+ return arr.find(predicate);
35
+ }
36
+
37
+ /**
38
+ * Filter array by predicate
39
+ */
40
+ export function filterItems<T>(
41
+ arr: T[] | null | undefined,
42
+ predicate: (item: T) => boolean
43
+ ): T[] {
44
+ if (!Array.isArray(arr)) return [];
45
+ return arr.filter(predicate);
46
+ }
47
+
48
+ /**
49
+ * Map array safely (returns empty array for null/undefined)
50
+ */
51
+ export function mapItems<T, R>(
52
+ arr: T[] | null | undefined,
53
+ mapper: (item: T) => R
54
+ ): R[] {
55
+ if (!Array.isArray(arr)) return [];
56
+ return arr.map(mapper);
57
+ }
58
+
59
+ /**
60
+ * Check if array contains item
61
+ */
62
+ export function arrayContains<T>(arr: T[] | null | undefined, item: T): boolean {
63
+ if (!Array.isArray(arr)) return false;
64
+ return arr.includes(item);
65
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * String validation and utility functions
3
+ */
4
+
5
+ /**
6
+ * Check if string is not empty
7
+ */
8
+ export function isNonEmptyString(str: string | null | undefined): boolean {
9
+ return typeof str === "string" && str.length > 0;
10
+ }
11
+
12
+ /**
13
+ * Check if string is empty
14
+ */
15
+ export function isEmptyString(str: string | null | undefined): boolean {
16
+ return !isNonEmptyString(str);
17
+ }
18
+
19
+ /**
20
+ * Trim string and check if not empty
21
+ */
22
+ export function isNonEmptyAfterTrim(str: string | null | undefined): boolean {
23
+ if (typeof str !== "string") return false;
24
+ return str.trim().length > 0;
25
+ }
26
+
27
+ /**
28
+ * Get trimmed string or empty string
29
+ */
30
+ export function getTrimmedString(str: string | null | undefined): string {
31
+ if (typeof str !== "string") return "";
32
+ return str.trim();
33
+ }
34
+
35
+ /**
36
+ * Check if string equals one of the values
37
+ */
38
+ export function stringEqualsAny(
39
+ str: string | null | undefined,
40
+ values: string[]
41
+ ): boolean {
42
+ if (typeof str !== "string") return false;
43
+ return values.includes(str);
44
+ }
45
+
46
+ /**
47
+ * Check if string contains substring
48
+ */
49
+ export function stringContains(
50
+ str: string | null | undefined,
51
+ substring: string
52
+ ): boolean {
53
+ if (typeof str !== "string") return false;
54
+ return str.includes(substring);
55
+ }
56
+
57
+ /**
58
+ * Check if string is neither 'undefined' nor 'null' string
59
+ */
60
+ export function isValidString(str: string | null | undefined): boolean {
61
+ if (typeof str !== "string") return false;
62
+ const trimmed = str.trim();
63
+ return trimmed.length > 0 && trimmed !== "undefined" && trimmed !== "null";
64
+ }