jfs-components 0.0.73 → 0.0.77

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 (134) hide show
  1. package/CHANGELOG.md +115 -6
  2. package/lib/commonjs/components/AccountCard/AccountCard.js +247 -0
  3. package/lib/commonjs/components/ActionFooter/ActionFooter.js +147 -82
  4. package/lib/commonjs/components/AppBar/AppBar.js +17 -11
  5. package/lib/commonjs/components/Avatar/Avatar.js +20 -0
  6. package/lib/commonjs/components/Badge/Badge.js +23 -0
  7. package/lib/commonjs/components/Button/Button.js +37 -0
  8. package/lib/commonjs/components/CardBankAccount/CardBankAccount.js +18 -2
  9. package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +40 -25
  10. package/lib/commonjs/components/Dropdown/Dropdown.js +214 -0
  11. package/lib/commonjs/components/DropdownInput/DropdownInput.js +542 -0
  12. package/lib/commonjs/components/FormField/FormField.js +328 -178
  13. package/lib/commonjs/components/IconButton/IconButton.js +20 -0
  14. package/lib/commonjs/components/Image/Image.js +26 -1
  15. package/lib/commonjs/components/LottieIntroBlock/LottieIntroBlock.js +150 -0
  16. package/lib/commonjs/components/LottiePlayer/LottiePlayer.js +116 -0
  17. package/lib/commonjs/components/LottiePlayer/LottiePlayer.web.js +82 -0
  18. package/lib/commonjs/components/LottiePlayer/loadNativeLottieView.js +74 -0
  19. package/lib/commonjs/components/LottiePlayer/loadWebLottieView.js +50 -0
  20. package/lib/commonjs/components/PageHero/PageHero.js +189 -0
  21. package/lib/commonjs/components/PoweredByLabel/PoweredByLabel.js +135 -0
  22. package/lib/commonjs/components/PoweredByLabel/finvu.png +0 -0
  23. package/lib/commonjs/components/RechargeCard/RechargeCard.js +32 -17
  24. package/lib/commonjs/components/Text/Text.js +40 -3
  25. package/lib/commonjs/components/Tooltip/Tooltip.js +34 -27
  26. package/lib/commonjs/components/index.js +67 -0
  27. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  28. package/lib/commonjs/icons/Icon.js +16 -0
  29. package/lib/commonjs/icons/registry.js +1 -1
  30. package/lib/commonjs/index.js +12 -0
  31. package/lib/commonjs/skeleton/Skeleton.js +234 -0
  32. package/lib/commonjs/skeleton/SkeletonGroup.js +140 -0
  33. package/lib/commonjs/skeleton/index.js +58 -0
  34. package/lib/commonjs/skeleton/shimmer-tokens.js +189 -0
  35. package/lib/commonjs/skeleton/useReducedMotion.js +64 -0
  36. package/lib/module/components/AccountCard/AccountCard.js +241 -0
  37. package/lib/module/components/ActionFooter/ActionFooter.js +146 -82
  38. package/lib/module/components/AppBar/AppBar.js +17 -11
  39. package/lib/module/components/Avatar/Avatar.js +19 -0
  40. package/lib/module/components/Badge/Badge.js +23 -0
  41. package/lib/module/components/Button/Button.js +37 -0
  42. package/lib/module/components/CardBankAccount/CardBankAccount.js +17 -2
  43. package/lib/module/components/CheckboxItem/CheckboxItem.js +41 -26
  44. package/lib/module/components/Dropdown/Dropdown.js +206 -0
  45. package/lib/module/components/DropdownInput/DropdownInput.js +536 -0
  46. package/lib/module/components/FormField/FormField.js +330 -180
  47. package/lib/module/components/IconButton/IconButton.js +20 -0
  48. package/lib/module/components/Image/Image.js +25 -1
  49. package/lib/module/components/LottieIntroBlock/LottieIntroBlock.js +144 -0
  50. package/lib/module/components/LottiePlayer/LottiePlayer.js +111 -0
  51. package/lib/module/components/LottiePlayer/LottiePlayer.web.js +77 -0
  52. package/lib/module/components/LottiePlayer/loadNativeLottieView.js +69 -0
  53. package/lib/module/components/LottiePlayer/loadWebLottieView.js +45 -0
  54. package/lib/module/components/PageHero/PageHero.js +183 -0
  55. package/lib/module/components/PoweredByLabel/PoweredByLabel.js +130 -0
  56. package/lib/module/components/PoweredByLabel/finvu.png +0 -0
  57. package/lib/module/components/RechargeCard/RechargeCard.js +33 -17
  58. package/lib/module/components/Text/Text.js +40 -3
  59. package/lib/module/components/Tooltip/Tooltip.js +34 -27
  60. package/lib/module/components/index.js +8 -1
  61. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  62. package/lib/module/icons/Icon.js +16 -0
  63. package/lib/module/icons/registry.js +1 -1
  64. package/lib/module/index.js +2 -1
  65. package/lib/module/skeleton/Skeleton.js +229 -0
  66. package/lib/module/skeleton/SkeletonGroup.js +133 -0
  67. package/lib/module/skeleton/index.js +6 -0
  68. package/lib/module/skeleton/shimmer-tokens.js +181 -0
  69. package/lib/module/skeleton/useReducedMotion.js +61 -0
  70. package/lib/typescript/src/components/AccountCard/AccountCard.d.ts +81 -0
  71. package/lib/typescript/src/components/ActionFooter/ActionFooter.d.ts +26 -21
  72. package/lib/typescript/src/components/Avatar/Avatar.d.ts +7 -1
  73. package/lib/typescript/src/components/Badge/Badge.d.ts +7 -1
  74. package/lib/typescript/src/components/Button/Button.d.ts +8 -1
  75. package/lib/typescript/src/components/CardBankAccount/CardBankAccount.d.ts +9 -2
  76. package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +18 -2
  77. package/lib/typescript/src/components/Dropdown/Dropdown.d.ts +62 -0
  78. package/lib/typescript/src/components/DropdownInput/DropdownInput.d.ts +107 -0
  79. package/lib/typescript/src/components/FormField/FormField.d.ts +76 -19
  80. package/lib/typescript/src/components/IconButton/IconButton.d.ts +7 -1
  81. package/lib/typescript/src/components/Image/Image.d.ts +8 -1
  82. package/lib/typescript/src/components/LottieIntroBlock/LottieIntroBlock.d.ts +58 -0
  83. package/lib/typescript/src/components/LottiePlayer/LottiePlayer.d.ts +85 -0
  84. package/lib/typescript/src/components/LottiePlayer/LottiePlayer.web.d.ts +28 -0
  85. package/lib/typescript/src/components/LottiePlayer/loadNativeLottieView.d.ts +11 -0
  86. package/lib/typescript/src/components/LottiePlayer/loadWebLottieView.d.ts +11 -0
  87. package/lib/typescript/src/components/PageHero/PageHero.d.ts +79 -0
  88. package/lib/typescript/src/components/PoweredByLabel/PoweredByLabel.d.ts +70 -0
  89. package/lib/typescript/src/components/Text/Text.d.ts +31 -2
  90. package/lib/typescript/src/components/Tooltip/Tooltip.d.ts +13 -2
  91. package/lib/typescript/src/components/index.d.ts +8 -1
  92. package/lib/typescript/src/icons/Icon.d.ts +7 -1
  93. package/lib/typescript/src/icons/registry.d.ts +1 -1
  94. package/lib/typescript/src/index.d.ts +1 -0
  95. package/lib/typescript/src/skeleton/Skeleton.d.ts +60 -0
  96. package/lib/typescript/src/skeleton/SkeletonGroup.d.ts +78 -0
  97. package/lib/typescript/src/skeleton/index.d.ts +5 -0
  98. package/lib/typescript/src/skeleton/shimmer-tokens.d.ts +160 -0
  99. package/lib/typescript/src/skeleton/useReducedMotion.d.ts +15 -0
  100. package/package.json +11 -3
  101. package/src/components/AccountCard/AccountCard.tsx +376 -0
  102. package/src/components/ActionFooter/ActionFooter.tsx +152 -86
  103. package/src/components/AppBar/AppBar.tsx +25 -14
  104. package/src/components/Avatar/Avatar.tsx +26 -0
  105. package/src/components/Badge/Badge.tsx +27 -0
  106. package/src/components/Button/Button.tsx +40 -0
  107. package/src/components/CardBankAccount/CardBankAccount.tsx +29 -3
  108. package/src/components/CheckboxItem/CheckboxItem.tsx +65 -30
  109. package/src/components/Dropdown/Dropdown.tsx +331 -0
  110. package/src/components/DropdownInput/DropdownInput.tsx +819 -0
  111. package/src/components/FormField/FormField.tsx +542 -215
  112. package/src/components/IconButton/IconButton.tsx +27 -0
  113. package/src/components/Image/Image.tsx +25 -0
  114. package/src/components/LottieIntroBlock/LottieIntroBlock.tsx +202 -0
  115. package/src/components/LottiePlayer/LottiePlayer.tsx +145 -0
  116. package/src/components/LottiePlayer/LottiePlayer.web.tsx +94 -0
  117. package/src/components/LottiePlayer/loadNativeLottieView.tsx +87 -0
  118. package/src/components/LottiePlayer/loadWebLottieView.tsx +64 -0
  119. package/src/components/PageHero/PageHero.tsx +257 -0
  120. package/src/components/PoweredByLabel/PoweredByLabel.tsx +221 -0
  121. package/src/components/PoweredByLabel/finvu.png +0 -0
  122. package/src/components/RechargeCard/RechargeCard.tsx +32 -24
  123. package/src/components/Text/Text.tsx +78 -3
  124. package/src/components/Tooltip/Tooltip.tsx +50 -25
  125. package/src/components/index.ts +16 -1
  126. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  127. package/src/icons/Icon.tsx +17 -0
  128. package/src/icons/registry.ts +1 -1
  129. package/src/index.ts +1 -0
  130. package/src/skeleton/Skeleton.tsx +298 -0
  131. package/src/skeleton/SkeletonGroup.tsx +193 -0
  132. package/src/skeleton/index.ts +10 -0
  133. package/src/skeleton/shimmer-tokens.ts +221 -0
  134. package/src/skeleton/useReducedMotion.ts +72 -0
@@ -0,0 +1,64 @@
1
+ import React from 'react'
2
+ import type { CSSProperties } from 'react'
3
+
4
+ /** Props we forward to the underlying web Lottie view. */
5
+ export type WebLottieViewProps = {
6
+ animationData: Record<string, unknown>
7
+ autoplay?: boolean
8
+ loop?: boolean
9
+ style?: CSSProperties
10
+ }
11
+
12
+ const INSTALL_HINT =
13
+ 'LottiePlayer requires lottie-react in your app.\n' +
14
+ ' npm install lottie-react'
15
+
16
+ function resolveWebLottieModuleName() {
17
+ return ['lottie', '-react'].join('')
18
+ }
19
+
20
+ function LottieUnavailable(props: WebLottieViewProps) {
21
+ React.useEffect(() => {
22
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
23
+ console.warn(`[jfs-components/LottiePlayer] ${INSTALL_HINT}`)
24
+ }
25
+ }, [])
26
+
27
+ return (
28
+ <div
29
+ style={{
30
+ ...props.style,
31
+ display: 'flex',
32
+ alignItems: 'center',
33
+ justifyContent: 'center',
34
+ backgroundColor: 'rgba(255, 196, 0, 0.12)',
35
+ border: '1px solid rgba(255, 196, 0, 0.45)',
36
+ borderRadius: 8,
37
+ padding: 8,
38
+ color: '#8a6d00',
39
+ fontSize: 11,
40
+ textAlign: 'center',
41
+ lineHeight: '15px',
42
+ }}
43
+ >
44
+ {typeof __DEV__ !== 'undefined' && __DEV__ ? INSTALL_HINT : null}
45
+ </div>
46
+ )
47
+ }
48
+
49
+ let cachedView: React.ComponentType<WebLottieViewProps> | undefined
50
+
51
+ export function getWebLottieView(): React.ComponentType<WebLottieViewProps> {
52
+ if (cachedView !== undefined) return cachedView
53
+
54
+ try {
55
+ const mod = require(resolveWebLottieModuleName()) as {
56
+ default?: React.ComponentType<WebLottieViewProps>
57
+ }
58
+ cachedView = mod.default ?? LottieUnavailable
59
+ } catch {
60
+ cachedView = LottieUnavailable
61
+ }
62
+
63
+ return cachedView
64
+ }
@@ -0,0 +1,257 @@
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
+ /**
36
+ * Optional media element shown above the text block (eyebrow + headline).
37
+ *
38
+ * Intentionally typed as `React.ReactNode` so the consumer can bring any
39
+ * renderer they like — `<Image />`, `<IconCapsule />`, a Lottie player
40
+ * (`lottie-react-native` / `@lottiefiles/dotlottie-react`), an `<SvgXml />`
41
+ * from `react-native-svg`, a `<Video />` from `react-native-video`, a
42
+ * gradient view, or a custom illustration. The library deliberately does
43
+ * NOT wrap Lottie / video runtimes (they require native modules + pod
44
+ * autolinking on every consumer's app), so PageHero just allocates a
45
+ * token-sized container and lets the slot render whatever it wants.
46
+ *
47
+ * The slot is rendered inside a `width × height` box driven by
48
+ * `media/width` and `media/height` tokens (default 117×117). `modes` are
49
+ * automatically cascaded into the slot via `cloneChildrenWithModes`.
50
+ */
51
+ media?: React.ReactNode
52
+ /** Mode configuration for design-token theming. */
53
+ modes?: Record<string, any>
54
+ /** Style overrides applied to the outer container. */
55
+ style?: StyleProp<ViewStyle>
56
+ testID?: string
57
+ }
58
+
59
+ /**
60
+ * PageHero displays a centered hero block typically used at the top of a page
61
+ * or feature screen. It contains an optional media slot (illustration / image
62
+ * / Lottie / SVG / video — consumer's choice), an eyebrow line, a large
63
+ * headline, an optional supporting line (e.g. price / timeline), and an
64
+ * optional action button.
65
+ *
66
+ * All visual values are resolved from Figma design tokens via
67
+ * `getVariableByName`. Slots cascade the active `modes` to their children
68
+ * through `cloneChildrenWithModes`.
69
+ *
70
+ * @component
71
+ * @example
72
+ * ```tsx
73
+ * <PageHero
74
+ * eyebrow="Upgrade to JioFinance+"
75
+ * headline="Resume earning cashback, extra points, and 1% gold"
76
+ * supportingText="₹999/year · ₹0 until 2027"
77
+ * buttonLabel="Renew for free"
78
+ * onButtonPress={() => navigate('Upgrade')}
79
+ * media={
80
+ * <Image
81
+ * imageSource={require('./assets/upgrade.png')}
82
+ * width={117}
83
+ * height={117}
84
+ * />
85
+ * }
86
+ * />
87
+ * ```
88
+ */
89
+ function PageHero({
90
+ eyebrow = 'Upgrade to JioFinance+',
91
+ headline = 'Resume earning cashback, extra points, and 1% gold',
92
+ supportingText = '₹999/year · ₹0 until 2027',
93
+ showSupportingText = true,
94
+ buttonLabel = 'Renew for free',
95
+ onButtonPress,
96
+ showButton = true,
97
+ buttonSlot,
98
+ media,
99
+ modes: propModes = EMPTY_MODES,
100
+ style,
101
+ testID,
102
+ }: PageHeroProps) {
103
+ const { modes: globalModes } = useTokens()
104
+ const modes = useMemo(
105
+ () => ({ ...globalModes, ...propModes }),
106
+ [globalModes, propModes]
107
+ )
108
+
109
+ const gap = Number(getVariableByName('PageHero/gap', modes)) || 16
110
+ const paddingHorizontal =
111
+ Number(getVariableByName('PageHero/padding/horizontal', modes)) || 0
112
+
113
+ const textWrapGap =
114
+ Number(getVariableByName('PageHero/textWrap/gap', modes)) || 8
115
+
116
+ // Media slot box — matches the 117×117 frame in Figma (node 4540:7845).
117
+ // Tokens fall back to 117 when not defined in the variables collection,
118
+ // so the layout stays stable on consumers that haven't tokenized this yet.
119
+ const mediaWidth = Number(getVariableByName('media/width', modes)) || 117
120
+ const mediaHeight = Number(getVariableByName('media/height', modes)) || 117
121
+
122
+ const eyebrowColor =
123
+ getVariableByName('PageHero/eyebrow/color', modes) || '#ffffff'
124
+ const eyebrowFontFamily =
125
+ getVariableByName('PageHero/eyebrow/fontFamily', modes) || 'System'
126
+ const eyebrowFontSize =
127
+ Number(getVariableByName('PageHero/eyebrow/fontSize', modes)) || 18
128
+ const eyebrowFontWeight =
129
+ getVariableByName('PageHero/eyebrow/fontWeight', modes) || 700
130
+ const eyebrowLineHeight =
131
+ Number(getVariableByName('PageHero/eyebrow/lineHeight', modes)) || 20
132
+
133
+ const headlineColor =
134
+ getVariableByName('PageHero/headline/color', modes) || '#ffffff'
135
+ const headlineFontFamily =
136
+ getVariableByName('PageHero/headline/fontFamily', modes) || 'System'
137
+ const headlineFontSize =
138
+ Number(getVariableByName('PageHero/headline/fontSize', modes)) || 29
139
+ const headlineFontWeight =
140
+ getVariableByName('PageHero/headline/fontWeight', modes) || 900
141
+ const headlineLineHeight =
142
+ Number(getVariableByName('PageHero/headline/lineHeight', modes)) || 29
143
+
144
+ // Only `lineHeight` is tokenized for the supporting text in the Figma source.
145
+ // Color, font size and weight are inline literals in the design (12px medium
146
+ // white) — we mirror that here so the visual stays faithful when no token
147
+ // exists.
148
+ const supportingTextLineHeight =
149
+ Number(getVariableByName('PageHero/supportingText/lineHeight', modes)) || 16
150
+
151
+ const containerStyle: ViewStyle = {
152
+ flexDirection: 'column',
153
+ alignItems: 'center',
154
+ paddingHorizontal,
155
+ gap,
156
+ width: '100%',
157
+ }
158
+
159
+ const textWrapStyle: ViewStyle = {
160
+ flexDirection: 'column',
161
+ alignItems: 'center',
162
+ gap: textWrapGap,
163
+ width: '100%',
164
+ }
165
+
166
+ const eyebrowStyle: TextStyle = {
167
+ color: eyebrowColor as string,
168
+ fontFamily: eyebrowFontFamily as string,
169
+ fontSize: eyebrowFontSize,
170
+ fontWeight: String(eyebrowFontWeight) as TextStyle['fontWeight'],
171
+ lineHeight: eyebrowLineHeight,
172
+ textAlign: 'center',
173
+ }
174
+
175
+ const headlineStyle: TextStyle = {
176
+ color: headlineColor as string,
177
+ fontFamily: headlineFontFamily as string,
178
+ fontSize: headlineFontSize,
179
+ fontWeight: String(headlineFontWeight) as TextStyle['fontWeight'],
180
+ lineHeight: headlineLineHeight,
181
+ textAlign: 'center',
182
+ width: '100%',
183
+ }
184
+
185
+ const supportingTextStyle: TextStyle = {
186
+ color: '#ffffff',
187
+ fontFamily: 'System',
188
+ fontSize: 12,
189
+ fontWeight: '500',
190
+ lineHeight: supportingTextLineHeight,
191
+ textAlign: 'center',
192
+ }
193
+
194
+ const buttonWrapStyle: ViewStyle = {
195
+ width: '100%',
196
+ }
197
+
198
+ const buttonContent = useMemo<React.ReactNode>(() => {
199
+ if (buttonSlot !== undefined && buttonSlot !== null) {
200
+ return cloneChildrenWithModes(buttonSlot, modes)
201
+ }
202
+ if (!showButton) {
203
+ return null
204
+ }
205
+ return (
206
+ <Button
207
+ label={buttonLabel}
208
+ onPress={onButtonPress}
209
+ modes={modes}
210
+ style={buttonWrapStyle}
211
+ />
212
+ )
213
+ // buttonWrapStyle is a literal object created above; it is intentionally
214
+ // omitted from deps because its identity changes on every render but its
215
+ // shape never does.
216
+ // eslint-disable-next-line react-hooks/exhaustive-deps
217
+ }, [buttonSlot, showButton, buttonLabel, onButtonPress, modes])
218
+
219
+ // Sized container for the media slot. Always rendered when `media` is
220
+ // provided, so the slot has a predictable box (matches Figma frame
221
+ // 4540:7845 — 117×117 by default) even if the inner element omits its
222
+ // own width/height. `overflow: 'hidden'` mirrors the Figma frame's
223
+ // `clipsContent` so a slightly oversized illustration doesn't break
224
+ // the centered layout.
225
+ const mediaContent = useMemo<React.ReactNode>(() => {
226
+ if (media === undefined || media === null) return null
227
+ return (
228
+ <View
229
+ style={{
230
+ width: mediaWidth,
231
+ height: mediaHeight,
232
+ alignItems: 'center',
233
+ justifyContent: 'center',
234
+ overflow: 'hidden',
235
+ }}
236
+ >
237
+ {cloneChildrenWithModes(media, modes)}
238
+ </View>
239
+ )
240
+ }, [media, mediaWidth, mediaHeight, modes])
241
+
242
+ return (
243
+ <View style={[containerStyle, style]} testID={testID}>
244
+ {mediaContent}
245
+ <View style={textWrapStyle}>
246
+ {eyebrow ? <Text style={eyebrowStyle}>{eyebrow}</Text> : null}
247
+ {headline ? <Text style={headlineStyle}>{headline}</Text> : null}
248
+ </View>
249
+ {showSupportingText && supportingText ? (
250
+ <Text style={supportingTextStyle}>{supportingText}</Text>
251
+ ) : null}
252
+ {buttonContent}
253
+ </View>
254
+ )
255
+ }
256
+
257
+ 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
@@ -1,11 +1,26 @@
1
- import React, { isValidElement, cloneElement } from 'react';
2
- import { View, Text, StyleSheet, type ViewStyle } from 'react-native';
1
+ import React from 'react';
2
+ import { View, Text, type ViewStyle } from 'react-native';
3
3
  import ButtonGroup from '../ButtonGroup/ButtonGroup';
4
4
  import AvatarGroup from '../AvatarGroup/AvatarGroup';
5
5
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
6
  import { EMPTY_MODES } from '../../utils/react-utils';
7
7
  import MoneyValue from '../MoneyValue/MoneyValue';
8
- import Button from '../Button/Button';
8
+
9
+ // Defaults applied to the inner ButtonGroup so the card matches Figma out of
10
+ // the box. They are spread *before* the caller's `modes` so any consumer can
11
+ // override an individual key (e.g. swap the size to "M").
12
+ const DEFAULT_BUTTON_GROUP_MODES: Record<string, string> = {
13
+ AppearanceBrand: 'Secondary',
14
+ 'Button / Size': 'S',
15
+ Emphasis: 'High',
16
+ };
17
+
18
+ // Defaults applied to the inner MoneyValue so the price renders at the
19
+ // 36 px / 900-weight scale defined for cards in Figma. Same merge rule as
20
+ // the button group — consumer `modes` override these.
21
+ const DEFAULT_MONEY_VALUE_MODES: Record<string, string> = {
22
+ Context3: 'Balance & Cards',
23
+ };
9
24
 
10
25
 
11
26
  export type RechargeCardProps = {
@@ -66,17 +81,19 @@ export default function RechargeCard({
66
81
  modes = EMPTY_MODES,
67
82
  style,
68
83
  }: RechargeCardProps) {
69
- // Container Tokens
70
- const backgroundColor = getVariableByName('rechargeCard/background', modes) || '#f6f3ff';
84
+ // Container Tokens (defaults mirror Figma node 2235:937).
85
+ const backgroundColor = getVariableByName('rechargeCard/background', modes) || '#ffffff';
71
86
  const paddingHorizontal = parseInt(getVariableByName('rechargeCard/padding/horizontal', modes) || 16, 10);
72
87
  const paddingVertical = parseInt(getVariableByName('rechargeCard/padding/vertical', modes) || 20, 10);
73
88
  const gap = parseInt(getVariableByName('rechargeCard/gap', modes) || 20, 10);
74
89
  const radius = parseInt(getVariableByName('rechargeCard/radius', modes) || 20, 10);
75
- const minWidth = parseInt(getVariableByName('rechargeCard/minWidth', modes) || 328, 10);
90
+ const minWidth = parseInt(getVariableByName('rechargeCard/minWidth', modes) || 312, 10);
91
+ const strokeWidth = parseInt(getVariableByName('rechargeCard/strokeWidth', modes) || 1, 10);
92
+ const strokeColor = getVariableByName('rechargeCard/stroke/color', modes) || '#ebebed';
76
93
 
77
94
  // Header Tokens
78
95
  const headerGap = parseInt(getVariableByName('rechargeCard/header/gap', modes) || 4, 10);
79
- const titleColor = getVariableByName('rechargeCard/title/color', modes) || '#13002d';
96
+ const titleColor = getVariableByName('rechargeCard/title/color', modes) || '#000000';
80
97
  const titleFontSize = parseInt(getVariableByName('rechargeCard/title/fontSize', modes) || 12, 10);
81
98
  const titleFontFamily = getVariableByName('rechargeCard/title/fontFamily', modes) || 'JioType Var';
82
99
  const titleLineHeight = parseInt(getVariableByName('rechargeCard/title/lineHeight', modes) || 14, 10);
@@ -87,30 +104,26 @@ export default function RechargeCard({
87
104
  const specItemGap = parseInt(getVariableByName('rechargeCard/specItem/gap', modes) || 4, 10);
88
105
 
89
106
  // Spec Label Tokens
90
- const specLabelColor = getVariableByName('rechargeCard/specItem/label/color', modes) || '#13002d';
107
+ const specLabelColor = getVariableByName('rechargeCard/specItem/label/color', modes) || '#000000';
91
108
  const specLabelFontSize = parseInt(getVariableByName('rechargeCard/specItem/label/fontSize', modes) || 12, 10);
92
109
  const specLabelFontFamily = getVariableByName('rechargeCard/specItem/label/fontFamily', modes) || 'JioType Var';
93
110
  const specLabelLineHeight = parseInt(getVariableByName('rechargeCard/specItem/label/lineHeight', modes) || 14, 10);
94
111
  const specLabelFontWeight = getVariableByName('rechargeCard/specItem/label/fontWeight', modes) || '500';
95
112
 
96
113
  // Spec Value Tokens
97
- const specValueColor = getVariableByName('rechargeCard/specItem/value/color', modes) || '#310064';
114
+ const specValueColor = getVariableByName('rechargeCard/specItem/value/color', modes) || '#000000';
98
115
  const specValueFontSize = parseInt(getVariableByName('rechargeCard/specItem/value/fontSize', modes) || 14, 10);
99
116
  const specValueFontFamily = getVariableByName('rechargeCard/specItem/value/fontFamily', modes) || 'JioType Var';
100
117
  const specValueLineHeight = parseInt(getVariableByName('rechargeCard/specItem/value/lineHeight', modes) || 17, 10);
101
118
  const specValueFontWeight = getVariableByName('rechargeCard/specItem/value/fontWeight', modes) || '500';
102
119
 
103
120
  // Disclaimer Tokens
104
- const disclaimerColor = getVariableByName('rechargeCard/disclaimer/color', modes) || '#22004a';
121
+ const disclaimerColor = getVariableByName('rechargeCard/disclaimer/color', modes) || '#000000';
105
122
  const disclaimerFontSize = parseInt(getVariableByName('rechargeCard/disclaimer/fontSize', modes) || 10, 10);
106
123
  const disclaimerFontFamily = getVariableByName('rechargeCard/disclaimer/fontFamily', modes) || 'JioType Var';
107
124
  const disclaimerLineHeight = parseInt(getVariableByName('rechargeCard/disclaimer/lineHeight', modes) || 13, 10);
108
125
  const disclaimerFontWeight = getVariableByName('rechargeCard/disclaimer/fontWeight', modes) || '400';
109
126
 
110
- // Button Group Tokens
111
- // Handled by ButtonGroup component directly
112
-
113
- // Helpers
114
127
  const resolveFontWeight = (weight: string | number) => typeof weight === 'number' ? weight.toString() : weight;
115
128
 
116
129
  // Pass modes to subscription children (e.g. AvatarGroup)
@@ -125,6 +138,8 @@ export default function RechargeCard({
125
138
  paddingVertical,
126
139
  gap,
127
140
  borderRadius: radius,
141
+ borderWidth: strokeWidth,
142
+ borderColor: strokeColor,
128
143
  minWidth,
129
144
  alignItems: 'flex-start',
130
145
  }, style]}>
@@ -142,7 +157,7 @@ export default function RechargeCard({
142
157
  <MoneyValue
143
158
  value={price}
144
159
  currency="₹"
145
- modes={modes}
160
+ modes={{ ...DEFAULT_MONEY_VALUE_MODES, ...modes }}
146
161
  />
147
162
  </View>
148
163
 
@@ -224,15 +239,8 @@ export default function RechargeCard({
224
239
  {disclaimer}
225
240
  </Text>
226
241
 
227
- {/* Button Group */}
228
- <ButtonGroup
229
- modes={{
230
- ...modes,
231
- "Appearance.Brand": "Secondary",
232
- "Button / Size": "S",
233
- "Emphasis": "High"
234
- }}
235
- >
242
+ {/* Button Group: defaults are overridable via the consumer's `modes` */}
243
+ <ButtonGroup modes={{ ...DEFAULT_BUTTON_GROUP_MODES, ...modes }}>
236
244
  {actions}
237
245
  </ButtonGroup>
238
246
  </View>