jfs-components 0.0.72 → 0.0.74

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 (158) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/lib/commonjs/components/AccordionCheckbox/AccordionCheckbox.js +239 -0
  3. package/lib/commonjs/components/AccountCard/AccountCard.js +247 -0
  4. package/lib/commonjs/components/AppBar/AppBar.js +17 -11
  5. package/lib/commonjs/components/BrandChip/BrandChip.js +149 -0
  6. package/lib/commonjs/components/CardBankAccount/CardBankAccount.js +229 -0
  7. package/lib/commonjs/components/CardInsight/CardInsight.js +166 -0
  8. package/lib/commonjs/components/CheckboxGroup/CheckboxGroup.js +67 -0
  9. package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +140 -0
  10. package/lib/commonjs/components/CircularProgressBar/CircularProgressBar.js +56 -9
  11. package/lib/commonjs/components/CoverageBarComparison/CoverageBarComparison.js +272 -0
  12. package/lib/commonjs/components/CoverageRing/CoverageRing.js +141 -0
  13. package/lib/commonjs/components/DonutChart/DonutChart.js +309 -0
  14. package/lib/commonjs/components/DonutChartSummary/DonutChartSummary.js +155 -0
  15. package/lib/commonjs/components/Dropdown/Dropdown.js +214 -0
  16. package/lib/commonjs/components/DropdownInput/DropdownInput.js +542 -0
  17. package/lib/commonjs/components/FormField/FormField.js +328 -178
  18. package/lib/commonjs/components/LinearMeter/LinearMeter.js +9 -28
  19. package/lib/commonjs/components/LinearProgress/LinearProgress.js +68 -0
  20. package/lib/commonjs/components/LottieIntroBlock/LottieIntroBlock.js +150 -0
  21. package/lib/commonjs/components/MetricLegendItem/MetricLegendItem.js +95 -0
  22. package/lib/commonjs/components/MonthlyStatusGrid/MonthlyStatusGrid.js +286 -0
  23. package/lib/commonjs/components/OTP/OTP.js +381 -37
  24. package/lib/commonjs/components/PageHero/PageHero.js +153 -0
  25. package/lib/commonjs/components/PoweredByLabel/PoweredByLabel.js +135 -0
  26. package/lib/commonjs/components/PoweredByLabel/finvu.png +0 -0
  27. package/lib/commonjs/components/ProductOverview/ProductOverview.js +147 -0
  28. package/lib/commonjs/components/RangeTrack/RangeTrack.js +269 -0
  29. package/lib/commonjs/components/SavingsGoalSummary/SavingsGoalSummary.js +181 -0
  30. package/lib/commonjs/components/SegmentedTrack/SegmentedTrack.js +171 -0
  31. package/lib/commonjs/components/StatGroup/StatGroup.js +128 -0
  32. package/lib/commonjs/components/StatItem/StatItem.js +65 -35
  33. package/lib/commonjs/components/StrengthIndicator/StrengthIndicator.js +157 -0
  34. package/lib/commonjs/components/SummaryTile/SummaryTile.js +150 -0
  35. package/lib/commonjs/components/Text/Text.js +9 -2
  36. package/lib/commonjs/components/Tooltip/Tooltip.js +34 -27
  37. package/lib/commonjs/components/index.js +231 -1
  38. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  39. package/lib/commonjs/icons/registry.js +1 -1
  40. package/lib/commonjs/utils/index.js +7 -0
  41. package/lib/commonjs/utils/number-utils.js +57 -0
  42. package/lib/module/components/AccordionCheckbox/AccordionCheckbox.js +233 -0
  43. package/lib/module/components/AccountCard/AccountCard.js +241 -0
  44. package/lib/module/components/AppBar/AppBar.js +17 -11
  45. package/lib/module/components/BrandChip/BrandChip.js +143 -0
  46. package/lib/module/components/CardBankAccount/CardBankAccount.js +223 -0
  47. package/lib/module/components/CardInsight/CardInsight.js +161 -0
  48. package/lib/module/components/CheckboxGroup/CheckboxGroup.js +62 -0
  49. package/lib/module/components/CheckboxItem/CheckboxItem.js +134 -0
  50. package/lib/module/components/CircularProgressBar/CircularProgressBar.js +56 -9
  51. package/lib/module/components/CoverageBarComparison/CoverageBarComparison.js +266 -0
  52. package/lib/module/components/CoverageRing/CoverageRing.js +136 -0
  53. package/lib/module/components/DonutChart/DonutChart.js +303 -0
  54. package/lib/module/components/DonutChartSummary/DonutChartSummary.js +150 -0
  55. package/lib/module/components/Dropdown/Dropdown.js +206 -0
  56. package/lib/module/components/DropdownInput/DropdownInput.js +536 -0
  57. package/lib/module/components/FormField/FormField.js +330 -180
  58. package/lib/module/components/LinearMeter/LinearMeter.js +9 -28
  59. package/lib/module/components/LinearProgress/LinearProgress.js +63 -0
  60. package/lib/module/components/LottieIntroBlock/LottieIntroBlock.js +144 -0
  61. package/lib/module/components/MetricLegendItem/MetricLegendItem.js +90 -0
  62. package/lib/module/components/MonthlyStatusGrid/MonthlyStatusGrid.js +281 -0
  63. package/lib/module/components/OTP/OTP.js +381 -38
  64. package/lib/module/components/PageHero/PageHero.js +147 -0
  65. package/lib/module/components/PoweredByLabel/PoweredByLabel.js +130 -0
  66. package/lib/module/components/PoweredByLabel/finvu.png +0 -0
  67. package/lib/module/components/ProductOverview/ProductOverview.js +142 -0
  68. package/lib/module/components/RangeTrack/RangeTrack.js +263 -0
  69. package/lib/module/components/SavingsGoalSummary/SavingsGoalSummary.js +175 -0
  70. package/lib/module/components/SegmentedTrack/SegmentedTrack.js +166 -0
  71. package/lib/module/components/StatGroup/StatGroup.js +123 -0
  72. package/lib/module/components/StatItem/StatItem.js +66 -36
  73. package/lib/module/components/StrengthIndicator/StrengthIndicator.js +152 -0
  74. package/lib/module/components/SummaryTile/SummaryTile.js +145 -0
  75. package/lib/module/components/Text/Text.js +9 -2
  76. package/lib/module/components/Tooltip/Tooltip.js +34 -27
  77. package/lib/module/components/index.js +28 -2
  78. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  79. package/lib/module/icons/registry.js +1 -1
  80. package/lib/module/utils/index.js +2 -1
  81. package/lib/module/utils/number-utils.js +53 -0
  82. package/lib/typescript/src/components/AccordionCheckbox/AccordionCheckbox.d.ts +71 -0
  83. package/lib/typescript/src/components/AccountCard/AccountCard.d.ts +81 -0
  84. package/lib/typescript/src/components/BrandChip/BrandChip.d.ts +43 -0
  85. package/lib/typescript/src/components/CardBankAccount/CardBankAccount.d.ts +86 -0
  86. package/lib/typescript/src/components/CardInsight/CardInsight.d.ts +48 -0
  87. package/lib/typescript/src/components/CheckboxGroup/CheckboxGroup.d.ts +41 -0
  88. package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +72 -0
  89. package/lib/typescript/src/components/CircularProgressBar/CircularProgressBar.d.ts +11 -1
  90. package/lib/typescript/src/components/CoverageBarComparison/CoverageBarComparison.d.ts +105 -0
  91. package/lib/typescript/src/components/CoverageRing/CoverageRing.d.ts +90 -0
  92. package/lib/typescript/src/components/DonutChart/DonutChart.d.ts +117 -0
  93. package/lib/typescript/src/components/DonutChartSummary/DonutChartSummary.d.ts +103 -0
  94. package/lib/typescript/src/components/Dropdown/Dropdown.d.ts +62 -0
  95. package/lib/typescript/src/components/DropdownInput/DropdownInput.d.ts +107 -0
  96. package/lib/typescript/src/components/FormField/FormField.d.ts +76 -19
  97. package/lib/typescript/src/components/LinearProgress/LinearProgress.d.ts +17 -0
  98. package/lib/typescript/src/components/LottieIntroBlock/LottieIntroBlock.d.ts +58 -0
  99. package/lib/typescript/src/components/MetricLegendItem/MetricLegendItem.d.ts +37 -0
  100. package/lib/typescript/src/components/MonthlyStatusGrid/MonthlyStatusGrid.d.ts +119 -0
  101. package/lib/typescript/src/components/OTP/OTP.d.ts +88 -2
  102. package/lib/typescript/src/components/PageHero/PageHero.d.ts +53 -0
  103. package/lib/typescript/src/components/PoweredByLabel/PoweredByLabel.d.ts +70 -0
  104. package/lib/typescript/src/components/ProductOverview/ProductOverview.d.ts +39 -0
  105. package/lib/typescript/src/components/RangeTrack/RangeTrack.d.ts +173 -0
  106. package/lib/typescript/src/components/SavingsGoalSummary/SavingsGoalSummary.d.ts +95 -0
  107. package/lib/typescript/src/components/SegmentedTrack/SegmentedTrack.d.ts +108 -0
  108. package/lib/typescript/src/components/StatGroup/StatGroup.d.ts +45 -0
  109. package/lib/typescript/src/components/StatItem/StatItem.d.ts +24 -7
  110. package/lib/typescript/src/components/StrengthIndicator/StrengthIndicator.d.ts +58 -0
  111. package/lib/typescript/src/components/SummaryTile/SummaryTile.d.ts +60 -0
  112. package/lib/typescript/src/components/Text/Text.d.ts +12 -2
  113. package/lib/typescript/src/components/Tooltip/Tooltip.d.ts +13 -2
  114. package/lib/typescript/src/components/index.d.ts +29 -3
  115. package/lib/typescript/src/icons/registry.d.ts +1 -1
  116. package/lib/typescript/src/utils/index.d.ts +1 -0
  117. package/lib/typescript/src/utils/number-utils.d.ts +29 -0
  118. package/package.json +1 -3
  119. package/src/components/AccordionCheckbox/AccordionCheckbox.tsx +323 -0
  120. package/src/components/AccountCard/AccountCard.tsx +376 -0
  121. package/src/components/AppBar/AppBar.tsx +25 -14
  122. package/src/components/BrandChip/BrandChip.tsx +235 -0
  123. package/src/components/CardBankAccount/CardBankAccount.tsx +321 -0
  124. package/src/components/CardInsight/CardInsight.tsx +239 -0
  125. package/src/components/CheckboxGroup/CheckboxGroup.tsx +86 -0
  126. package/src/components/CheckboxItem/CheckboxItem.tsx +209 -0
  127. package/src/components/CircularProgressBar/CircularProgressBar.tsx +74 -9
  128. package/src/components/CoverageBarComparison/CoverageBarComparison.tsx +378 -0
  129. package/src/components/CoverageRing/CoverageRing.tsx +225 -0
  130. package/src/components/DonutChart/DonutChart.tsx +503 -0
  131. package/src/components/DonutChartSummary/DonutChartSummary.tsx +256 -0
  132. package/src/components/Dropdown/Dropdown.tsx +331 -0
  133. package/src/components/DropdownInput/DropdownInput.tsx +819 -0
  134. package/src/components/FormField/FormField.tsx +542 -215
  135. package/src/components/LinearMeter/LinearMeter.tsx +9 -39
  136. package/src/components/LinearProgress/LinearProgress.tsx +92 -0
  137. package/src/components/LottieIntroBlock/LottieIntroBlock.tsx +202 -0
  138. package/src/components/MetricLegendItem/MetricLegendItem.tsx +167 -0
  139. package/src/components/MonthlyStatusGrid/MonthlyStatusGrid.tsx +438 -0
  140. package/src/components/OTP/OTP.tsx +476 -29
  141. package/src/components/PageHero/PageHero.tsx +200 -0
  142. package/src/components/PoweredByLabel/PoweredByLabel.tsx +221 -0
  143. package/src/components/PoweredByLabel/finvu.png +0 -0
  144. package/src/components/ProductOverview/ProductOverview.tsx +236 -0
  145. package/src/components/RangeTrack/RangeTrack.tsx +394 -0
  146. package/src/components/SavingsGoalSummary/SavingsGoalSummary.tsx +269 -0
  147. package/src/components/SegmentedTrack/SegmentedTrack.tsx +268 -0
  148. package/src/components/StatGroup/StatGroup.tsx +169 -0
  149. package/src/components/StatItem/StatItem.tsx +117 -40
  150. package/src/components/StrengthIndicator/StrengthIndicator.tsx +205 -0
  151. package/src/components/SummaryTile/SummaryTile.tsx +251 -0
  152. package/src/components/Text/Text.tsx +24 -3
  153. package/src/components/Tooltip/Tooltip.tsx +50 -25
  154. package/src/components/index.ts +47 -3
  155. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  156. package/src/icons/registry.ts +1 -1
  157. package/src/utils/index.ts +1 -0
  158. package/src/utils/number-utils.ts +60 -0
@@ -0,0 +1,200 @@
1
+ import React, { useMemo } from 'react'
2
+ import {
3
+ View,
4
+ Text,
5
+ type StyleProp,
6
+ type ViewStyle,
7
+ type TextStyle,
8
+ } from 'react-native'
9
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
10
+ import { useTokens } from '../../design-tokens/JFSThemeProvider'
11
+ import Button from '../Button/Button'
12
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
13
+
14
+ export type PageHeroProps = {
15
+ /** Small eyebrow text shown above the headline. */
16
+ eyebrow?: string
17
+ /** Main headline text. Centered and bold. */
18
+ headline?: string
19
+ /** Optional supporting text shown below the headline. */
20
+ supportingText?: string
21
+ /** Whether to render the supporting text. */
22
+ showSupportingText?: boolean
23
+ /** Label for the default action button. Ignored when `buttonSlot` is provided. */
24
+ buttonLabel?: string
25
+ /** Press handler for the default action button. Ignored when `buttonSlot` is provided. */
26
+ onButtonPress?: () => void
27
+ /** Whether to render the default action button. Ignored when `buttonSlot` is provided. */
28
+ showButton?: boolean
29
+ /**
30
+ * Optional slot to fully override the action button.
31
+ * When provided, `showButton`, `buttonLabel`, and `onButtonPress` are ignored.
32
+ * `modes` are automatically cascaded into this slot.
33
+ */
34
+ buttonSlot?: React.ReactNode
35
+ /** Mode configuration for design-token theming. */
36
+ modes?: Record<string, any>
37
+ /** Style overrides applied to the outer container. */
38
+ style?: StyleProp<ViewStyle>
39
+ testID?: string
40
+ }
41
+
42
+ /**
43
+ * PageHero displays a centered hero block typically used at the top of a page
44
+ * or feature screen. It contains an eyebrow line, a large headline, an optional
45
+ * supporting line (e.g. price/timeline), and an optional action button.
46
+ *
47
+ * All visual values are resolved from Figma design tokens via
48
+ * `getVariableByName`. The button slot cascades the active `modes` to its
49
+ * children through `cloneChildrenWithModes`.
50
+ *
51
+ * @component
52
+ * @example
53
+ * ```tsx
54
+ * <PageHero
55
+ * eyebrow="Upgrade to JioFinance+"
56
+ * headline="Resume earning cashback, extra points, and 1% gold"
57
+ * supportingText="₹999/year · ₹0 until 2027"
58
+ * buttonLabel="Renew for free"
59
+ * onButtonPress={() => navigate('Upgrade')}
60
+ * />
61
+ * ```
62
+ */
63
+ function PageHero({
64
+ eyebrow = 'Upgrade to JioFinance+',
65
+ headline = 'Resume earning cashback, extra points, and 1% gold',
66
+ supportingText = '₹999/year · ₹0 until 2027',
67
+ showSupportingText = true,
68
+ buttonLabel = 'Renew for free',
69
+ onButtonPress,
70
+ showButton = true,
71
+ buttonSlot,
72
+ modes: propModes = EMPTY_MODES,
73
+ style,
74
+ testID,
75
+ }: PageHeroProps) {
76
+ const { modes: globalModes } = useTokens()
77
+ const modes = useMemo(
78
+ () => ({ ...globalModes, ...propModes }),
79
+ [globalModes, propModes]
80
+ )
81
+
82
+ const gap = Number(getVariableByName('PageHero/gap', modes)) || 16
83
+ const paddingHorizontal =
84
+ Number(getVariableByName('PageHero/padding/horizontal', modes)) || 0
85
+
86
+ const textWrapGap =
87
+ Number(getVariableByName('PageHero/textWrap/gap', modes)) || 8
88
+
89
+ const eyebrowColor =
90
+ getVariableByName('PageHero/eyebrow/color', modes) || '#ffffff'
91
+ const eyebrowFontFamily =
92
+ getVariableByName('PageHero/eyebrow/fontFamily', modes) || 'System'
93
+ const eyebrowFontSize =
94
+ Number(getVariableByName('PageHero/eyebrow/fontSize', modes)) || 18
95
+ const eyebrowFontWeight =
96
+ getVariableByName('PageHero/eyebrow/fontWeight', modes) || 700
97
+ const eyebrowLineHeight =
98
+ Number(getVariableByName('PageHero/eyebrow/lineHeight', modes)) || 20
99
+
100
+ const headlineColor =
101
+ getVariableByName('PageHero/headline/color', modes) || '#ffffff'
102
+ const headlineFontFamily =
103
+ getVariableByName('PageHero/headline/fontFamily', modes) || 'System'
104
+ const headlineFontSize =
105
+ Number(getVariableByName('PageHero/headline/fontSize', modes)) || 29
106
+ const headlineFontWeight =
107
+ getVariableByName('PageHero/headline/fontWeight', modes) || 900
108
+ const headlineLineHeight =
109
+ Number(getVariableByName('PageHero/headline/lineHeight', modes)) || 29
110
+
111
+ // Only `lineHeight` is tokenized for the supporting text in the Figma source.
112
+ // Color, font size and weight are inline literals in the design (12px medium
113
+ // white) — we mirror that here so the visual stays faithful when no token
114
+ // exists.
115
+ const supportingTextLineHeight =
116
+ Number(getVariableByName('PageHero/supportingText/lineHeight', modes)) || 16
117
+
118
+ const containerStyle: ViewStyle = {
119
+ flexDirection: 'column',
120
+ alignItems: 'center',
121
+ paddingHorizontal,
122
+ gap,
123
+ width: '100%',
124
+ }
125
+
126
+ const textWrapStyle: ViewStyle = {
127
+ flexDirection: 'column',
128
+ alignItems: 'center',
129
+ gap: textWrapGap,
130
+ width: '100%',
131
+ }
132
+
133
+ const eyebrowStyle: TextStyle = {
134
+ color: eyebrowColor as string,
135
+ fontFamily: eyebrowFontFamily as string,
136
+ fontSize: eyebrowFontSize,
137
+ fontWeight: String(eyebrowFontWeight) as TextStyle['fontWeight'],
138
+ lineHeight: eyebrowLineHeight,
139
+ textAlign: 'center',
140
+ }
141
+
142
+ const headlineStyle: TextStyle = {
143
+ color: headlineColor as string,
144
+ fontFamily: headlineFontFamily as string,
145
+ fontSize: headlineFontSize,
146
+ fontWeight: String(headlineFontWeight) as TextStyle['fontWeight'],
147
+ lineHeight: headlineLineHeight,
148
+ textAlign: 'center',
149
+ width: '100%',
150
+ }
151
+
152
+ const supportingTextStyle: TextStyle = {
153
+ color: '#ffffff',
154
+ fontFamily: 'System',
155
+ fontSize: 12,
156
+ fontWeight: '500',
157
+ lineHeight: supportingTextLineHeight,
158
+ textAlign: 'center',
159
+ }
160
+
161
+ const buttonWrapStyle: ViewStyle = {
162
+ width: '100%',
163
+ }
164
+
165
+ const buttonContent = useMemo<React.ReactNode>(() => {
166
+ if (buttonSlot !== undefined && buttonSlot !== null) {
167
+ return cloneChildrenWithModes(buttonSlot, modes)
168
+ }
169
+ if (!showButton) {
170
+ return null
171
+ }
172
+ return (
173
+ <Button
174
+ label={buttonLabel}
175
+ onPress={onButtonPress}
176
+ modes={modes}
177
+ style={buttonWrapStyle}
178
+ />
179
+ )
180
+ // buttonWrapStyle is a literal object created above; it is intentionally
181
+ // omitted from deps because its identity changes on every render but its
182
+ // shape never does.
183
+ // eslint-disable-next-line react-hooks/exhaustive-deps
184
+ }, [buttonSlot, showButton, buttonLabel, onButtonPress, modes])
185
+
186
+ return (
187
+ <View style={[containerStyle, style]} testID={testID}>
188
+ <View style={textWrapStyle}>
189
+ {eyebrow ? <Text style={eyebrowStyle}>{eyebrow}</Text> : null}
190
+ {headline ? <Text style={headlineStyle}>{headline}</Text> : null}
191
+ </View>
192
+ {showSupportingText && supportingText ? (
193
+ <Text style={supportingTextStyle}>{supportingText}</Text>
194
+ ) : null}
195
+ {buttonContent}
196
+ </View>
197
+ )
198
+ }
199
+
200
+ export default PageHero
@@ -0,0 +1,221 @@
1
+ import React, { useMemo } from 'react'
2
+ import {
3
+ Text,
4
+ View,
5
+ type ImageStyle,
6
+ type StyleProp,
7
+ type TextStyle,
8
+ type ViewStyle,
9
+ } from 'react-native'
10
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
11
+ import { useTokens } from '../../design-tokens/JFSThemeProvider'
12
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
13
+ import MediaSource, { type UnifiedSource } from '../../utils/MediaSource'
14
+
15
+ // Default bundled FINVU brand logo, matching the Figma reference so the
16
+ // component renders correctly out of the box without any image prop.
17
+ const DEFAULT_LOGO: UnifiedSource = require('./finvu.png')
18
+
19
+ const DEFAULT_LABEL = 'Powered by RBI-regulated account aggregator'
20
+ const DEFAULT_IMAGE_WIDTH = 33
21
+ const DEFAULT_IMAGE_HEIGHT = 12
22
+
23
+ export type PoweredByLabelProps = {
24
+ /**
25
+ * Label copy. Defaults to "Powered by RBI-regulated account aggregator"
26
+ * to match the Figma reference.
27
+ */
28
+ label?: string
29
+ /**
30
+ * Brand logo source. Accepts any {@link UnifiedSource} — remote URI, SVG
31
+ * XML string, `require()` asset, SVG React component, or React element.
32
+ * Defaults to the bundled FINVU logo so the component renders correctly
33
+ * without any caller-provided image.
34
+ *
35
+ * Ignored when `imageSlot` is provided.
36
+ */
37
+ imageSource?: UnifiedSource
38
+ /**
39
+ * Rendered width of the logo in px. Defaults to `33` to match Figma.
40
+ */
41
+ imageWidth?: number
42
+ /**
43
+ * Rendered height of the logo in px. Defaults to `12` to match Figma.
44
+ */
45
+ imageHeight?: number
46
+ /**
47
+ * Replace the default `Image` entirely (e.g. with a vector logo,
48
+ * `BrandChip`, or custom layout). Receives `modes` recursively.
49
+ * Overrides `imageSource`.
50
+ */
51
+ imageSlot?: React.ReactNode
52
+ /**
53
+ * Design token modes for theming (e.g. `{ 'Color Mode': 'Dark' }`).
54
+ */
55
+ modes?: Record<string, any>
56
+ /** Container style override. */
57
+ style?: StyleProp<ViewStyle>
58
+ /** Label text style override. */
59
+ textStyle?: StyleProp<TextStyle>
60
+ /** Logo image style override (size/resize behaviour, etc.). */
61
+ imageStyle?: StyleProp<ImageStyle>
62
+ /**
63
+ * Accessibility label. Defaults to the visible `label` so the brand image
64
+ * (which is decorative) does not need to be announced separately.
65
+ */
66
+ accessibilityLabel?: string
67
+ }
68
+
69
+ const toNumber = (value: unknown, fallback: number): number => {
70
+ if (typeof value === 'number') return Number.isFinite(value) ? value : fallback
71
+ if (typeof value === 'string') {
72
+ const parsed = Number(value)
73
+ return Number.isFinite(parsed) ? parsed : fallback
74
+ }
75
+ return fallback
76
+ }
77
+
78
+ const toFontWeight = (
79
+ value: unknown,
80
+ fallback: TextStyle['fontWeight']
81
+ ): TextStyle['fontWeight'] => {
82
+ if (typeof value === 'number') return String(value) as TextStyle['fontWeight']
83
+ if (typeof value === 'string') return value as TextStyle['fontWeight']
84
+ return fallback
85
+ }
86
+
87
+ /**
88
+ * `PoweredByLabel` renders the small "Powered by RBI-regulated account
89
+ * aggregator" badge with a trailing brand logo, used to attribute the
90
+ * underlying account-aggregator partner in flows such as bank-account
91
+ * linking and consent screens.
92
+ *
93
+ * The component is composed of:
94
+ *
95
+ * 1. A token-styled pill container (`poweredByLabel/background`,
96
+ * `poweredByLabel/padding/*`).
97
+ * 2. The disclosure copy rendered through the `poweredByLabel/*` typography
98
+ * tokens.
99
+ * 3. A configurable brand logo slot. Defaults to the bundled FINVU mark, but
100
+ * callers can pass any image via `imageSource` or fully replace the slot
101
+ * via `imageSlot`.
102
+ *
103
+ * @component
104
+ * @param {PoweredByLabelProps} props
105
+ */
106
+ function PoweredByLabel({
107
+ label = DEFAULT_LABEL,
108
+ imageSource,
109
+ imageWidth = DEFAULT_IMAGE_WIDTH,
110
+ imageHeight = DEFAULT_IMAGE_HEIGHT,
111
+ imageSlot,
112
+ modes: propModes = EMPTY_MODES,
113
+ style,
114
+ textStyle,
115
+ imageStyle,
116
+ accessibilityLabel,
117
+ }: PoweredByLabelProps) {
118
+ const { modes: globalModes } = useTokens()
119
+ const modes = useMemo(
120
+ () =>
121
+ globalModes === EMPTY_MODES && propModes === EMPTY_MODES
122
+ ? EMPTY_MODES
123
+ : { ...globalModes, ...propModes },
124
+ [globalModes, propModes]
125
+ )
126
+
127
+ const background =
128
+ (getVariableByName('poweredByLabel/background', modes) as string | null) ??
129
+ '#f5f5f5'
130
+ const foreground =
131
+ (getVariableByName('poweredByLabel/foreground', modes) as string | null) ??
132
+ '#191b1e'
133
+ const fontFamily =
134
+ (getVariableByName('poweredByLabel/fontFamily', modes) as string | null) ??
135
+ 'JioType Var'
136
+ const fontSize = toNumber(getVariableByName('poweredByLabel/fontSize', modes), 10)
137
+ const lineHeight = toNumber(
138
+ getVariableByName('poweredByLabel/lineHeight', modes),
139
+ 12
140
+ )
141
+ const fontWeight = toFontWeight(
142
+ getVariableByName('poweredByLabel/fontWeight', modes),
143
+ '400'
144
+ )
145
+ const gap = toNumber(getVariableByName('poweredByLabel/gap', modes), 10)
146
+ const paddingHorizontal = toNumber(
147
+ getVariableByName('poweredByLabel/padding/horizontal', modes),
148
+ 16
149
+ )
150
+ const paddingVertical = toNumber(
151
+ getVariableByName('poweredByLabel/padding/vertical', modes),
152
+ 6
153
+ )
154
+
155
+ const containerStyle: ViewStyle = {
156
+ flexDirection: 'row',
157
+ alignItems: 'center',
158
+ justifyContent: 'center',
159
+ backgroundColor: background,
160
+ paddingHorizontal,
161
+ paddingVertical,
162
+ gap,
163
+ // Hug content horizontally so the pill does not stretch to fill the
164
+ // parent (matches Badge, BrandChip, etc.). Override via `style` if
165
+ // you want it full-width (e.g. inside a card footer).
166
+ alignSelf: 'flex-start',
167
+ }
168
+
169
+ const labelTextStyle: TextStyle = {
170
+ color: foreground,
171
+ fontFamily,
172
+ fontSize,
173
+ lineHeight,
174
+ fontWeight,
175
+ textAlign: 'center',
176
+ flexShrink: 1,
177
+ }
178
+
179
+ const renderImage = (): React.ReactNode => {
180
+ if (imageSlot !== undefined && imageSlot !== null) {
181
+ const processed = cloneChildrenWithModes(imageSlot, modes)
182
+ if (processed.length === 0) return null
183
+ return processed.length === 1 ? processed[0] : processed
184
+ }
185
+
186
+ const resolvedSource: UnifiedSource =
187
+ (imageSource as UnifiedSource | undefined) ?? DEFAULT_LOGO
188
+
189
+ return (
190
+ <MediaSource
191
+ source={resolvedSource}
192
+ width={imageWidth}
193
+ height={imageHeight}
194
+ resizeMode="contain"
195
+ style={imageStyle}
196
+ accessibilityElementsHidden={true}
197
+ importantForAccessibility="no"
198
+ />
199
+ )
200
+ }
201
+
202
+ return (
203
+ <View
204
+ accessibilityRole="text"
205
+ accessibilityLabel={accessibilityLabel ?? label}
206
+ style={[containerStyle, style]}
207
+ >
208
+ <Text
209
+ style={[labelTextStyle, textStyle]}
210
+ accessibilityElementsHidden={true}
211
+ importantForAccessibility="no"
212
+ numberOfLines={1}
213
+ >
214
+ {label}
215
+ </Text>
216
+ {renderImage()}
217
+ </View>
218
+ )
219
+ }
220
+
221
+ export default PoweredByLabel
@@ -0,0 +1,236 @@
1
+ import React from 'react'
2
+ import {
3
+ View,
4
+ Text,
5
+ type StyleProp,
6
+ type ViewStyle,
7
+ type TextStyle,
8
+ type ImageSourcePropType,
9
+ } from 'react-native'
10
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
11
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
12
+ import Image from '../Image/Image'
13
+ import ProductLabel from '../ProductLabel/ProductLabel'
14
+
15
+ export type ProductOverviewStat = {
16
+ /** The large prominent value shown on top, e.g. "995", "3%". */
17
+ value: string
18
+ /** The descriptive label rendered beneath the value, e.g. "Purity". */
19
+ label: string
20
+ }
21
+
22
+ export type ProductOverviewProps = {
23
+ /** Hero image source rendered at the top of the card. */
24
+ imageSource?: ImageSourcePropType | string
25
+ /** Aspect ratio for the hero image. Defaults to the Figma frame (288:170). */
26
+ imageRatio?: number
27
+ /** Avatar source for the inline ProductLabel chip. */
28
+ labelImageSource?: ImageSourcePropType | string
29
+ /** Text shown next to the avatar in the ProductLabel chip. */
30
+ label?: string
31
+ /** The large product name, e.g. "0.5g Gold Coin". */
32
+ productName?: string
33
+ /** The short multi-line description below the product name. */
34
+ description?: string
35
+ /**
36
+ * Stats rendered in the bottom row. Each stat shows a large value above a
37
+ * smaller label. Pass an empty array (or `null`) to hide the row.
38
+ */
39
+ stats?: ProductOverviewStat[]
40
+ /** Design token modes for theming (e.g. `{ 'Color Mode': 'Light' }`). */
41
+ modes?: Record<string, any>
42
+ /** Container style override. */
43
+ style?: StyleProp<ViewStyle>
44
+ /**
45
+ * Custom slot rendered between the description and the stats row, useful
46
+ * for badges, callouts, etc. Receives the same `modes` as the parent.
47
+ */
48
+ children?: React.ReactNode
49
+ }
50
+
51
+ const DEFAULT_STATS: ProductOverviewStat[] = [
52
+ { value: '995', label: 'Purity' },
53
+ { value: '3%', label: 'GST' },
54
+ ]
55
+
56
+ const ProductOverview = ({
57
+ imageSource,
58
+ imageRatio = 288 / 170,
59
+ labelImageSource,
60
+ label = 'Gold',
61
+ productName = '0.5g Gold Coin',
62
+ description = 'Your gold is insured from our vault to you. If lost or damaged, we’ll replace it.',
63
+ stats = DEFAULT_STATS,
64
+ modes = EMPTY_MODES,
65
+ style,
66
+ children,
67
+ }: ProductOverviewProps) => {
68
+ const padding = (getVariableByName('productOverview/padding', modes) as number | null) ?? 24
69
+ const gap = (getVariableByName('productOverview/gap', modes) as number | null) ?? 12
70
+ const background =
71
+ (getVariableByName('productOverview/background', modes) as string | null) ?? '#ffffff'
72
+
73
+ const productNameColor =
74
+ (getVariableByName('productOverview/productName/color', modes) as string | null) ??
75
+ '#0d0d0f'
76
+ const productNameFontFamily =
77
+ (getVariableByName('productOverview/productName/fontFamily', modes) as string | null) ??
78
+ 'JioType Var'
79
+ const productNameFontSize =
80
+ (getVariableByName('productOverview/productName/fontSize', modes) as number | null) ?? 26
81
+ const productNameFontWeight =
82
+ getVariableByName('productOverview/productName/fontWeight', modes) ?? 900
83
+ const productNameLineHeight =
84
+ (getVariableByName('productOverview/productName/lineHeight', modes) as number | null) ?? 26
85
+
86
+ const descriptionColor =
87
+ (getVariableByName('productOverview/description/color', modes) as string | null) ??
88
+ '#1a1c1f'
89
+ const descriptionFontFamily =
90
+ (getVariableByName('productOverview/description/fontFamily', modes) as string | null) ??
91
+ 'JioType Var'
92
+ const descriptionFontSize =
93
+ (getVariableByName('productOverview/description/fontSize', modes) as number | null) ?? 14
94
+ const descriptionFontWeight =
95
+ getVariableByName('productOverview/description/fontWeight', modes) ?? 500
96
+ const descriptionLineHeight =
97
+ (getVariableByName('productOverview/description/lineHeight', modes) as number | null) ??
98
+ 18.2
99
+
100
+ const statGap = (getVariableByName('productOverview/stat/gap', modes) as number | null) ?? 2
101
+ const statValueColor =
102
+ (getVariableByName('productOverview/stat/value/color', modes) as string | null) ??
103
+ '#141414'
104
+ const statValueFontFamily =
105
+ (getVariableByName('productOverview/stat/value/fontFamily', modes) as string | null) ??
106
+ 'JioType Var'
107
+ const statValueFontSize =
108
+ (getVariableByName('productOverview/stat/value/fontSize', modes) as number | null) ?? 20
109
+ const statValueFontWeight =
110
+ getVariableByName('productOverview/stat/value/fontWeight', modes) ?? 900
111
+ const statValueLineHeight =
112
+ (getVariableByName('productOverview/stat/value/lineHeight', modes) as number | null) ?? 20
113
+
114
+ const statLabelColor = productNameColor
115
+ const statLabelFontFamily =
116
+ (getVariableByName('productOverview/stat/label/fontFamily', modes) as string | null) ??
117
+ 'JioType Var'
118
+ const statLabelFontSize =
119
+ (getVariableByName('productOverview/stat/label/fontSize', modes) as number | null) ?? 12
120
+ const statLabelFontWeight =
121
+ getVariableByName('productOverview/stat/label/fontWeight', modes) ?? 400
122
+ const statLabelLineHeight =
123
+ (getVariableByName('productOverview/stat/label/lineHeight', modes) as number | null) ??
124
+ 15.6
125
+
126
+ const productNameStyle: TextStyle = {
127
+ color: productNameColor,
128
+ fontFamily: productNameFontFamily,
129
+ fontSize: productNameFontSize,
130
+ fontWeight: String(productNameFontWeight) as TextStyle['fontWeight'],
131
+ lineHeight: productNameLineHeight,
132
+ textAlign: 'center',
133
+ }
134
+
135
+ const descriptionStyle: TextStyle = {
136
+ color: descriptionColor,
137
+ fontFamily: descriptionFontFamily,
138
+ fontSize: descriptionFontSize,
139
+ fontWeight: String(descriptionFontWeight) as TextStyle['fontWeight'],
140
+ lineHeight: descriptionLineHeight,
141
+ textAlign: 'center',
142
+ }
143
+
144
+ const statValueStyle: TextStyle = {
145
+ color: statValueColor,
146
+ fontFamily: statValueFontFamily,
147
+ fontSize: statValueFontSize,
148
+ fontWeight: String(statValueFontWeight) as TextStyle['fontWeight'],
149
+ lineHeight: statValueLineHeight,
150
+ }
151
+
152
+ const statLabelStyle: TextStyle = {
153
+ color: statLabelColor,
154
+ fontFamily: statLabelFontFamily,
155
+ fontSize: statLabelFontSize,
156
+ fontWeight: String(statLabelFontWeight) as TextStyle['fontWeight'],
157
+ lineHeight: statLabelLineHeight,
158
+ textAlign: 'center',
159
+ }
160
+
161
+ const showStats = Array.isArray(stats) && stats.length > 0
162
+
163
+ return (
164
+ <View
165
+ style={[
166
+ {
167
+ backgroundColor: background,
168
+ padding,
169
+ gap,
170
+ alignItems: 'center',
171
+ width: '100%',
172
+ },
173
+ style,
174
+ ]}
175
+ >
176
+ {imageSource != null && (
177
+ <Image
178
+ imageSource={imageSource}
179
+ ratio={imageRatio}
180
+ resizeMode="contain"
181
+ accessibilityElementsHidden
182
+ importantForAccessibility="no"
183
+ />
184
+ )}
185
+
186
+ <ProductLabel
187
+ label={label}
188
+ {...(labelImageSource != null && { imageSource: labelImageSource })}
189
+ modes={modes}
190
+ />
191
+
192
+ {productName ? (
193
+ <Text style={productNameStyle} accessibilityRole="header">
194
+ {productName}
195
+ </Text>
196
+ ) : null}
197
+
198
+ {description ? <Text style={descriptionStyle}>{description}</Text> : null}
199
+
200
+ {children ? <>{cloneChildrenWithModes(children, modes)}</> : null}
201
+
202
+ {showStats && (
203
+ <View
204
+ style={{
205
+ flexDirection: 'row',
206
+ alignItems: 'center',
207
+ justifyContent: 'space-between',
208
+ width: '100%',
209
+ }}
210
+ >
211
+ {stats.map((stat, index) => (
212
+ <View
213
+ key={`${stat.label}-${index}`}
214
+ style={{
215
+ flex: 1,
216
+ minWidth: 0,
217
+ alignItems: 'center',
218
+ gap: statGap,
219
+ overflow: 'hidden',
220
+ }}
221
+ >
222
+ <Text style={statValueStyle} numberOfLines={1}>
223
+ {stat.value}
224
+ </Text>
225
+ <Text style={statLabelStyle} numberOfLines={1}>
226
+ {stat.label}
227
+ </Text>
228
+ </View>
229
+ ))}
230
+ </View>
231
+ )}
232
+ </View>
233
+ )
234
+ }
235
+
236
+ export default ProductOverview