jfs-components 0.0.73 → 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 (63) hide show
  1. package/CHANGELOG.md +23 -6
  2. package/lib/commonjs/components/AccountCard/AccountCard.js +247 -0
  3. package/lib/commonjs/components/AppBar/AppBar.js +17 -11
  4. package/lib/commonjs/components/CardBankAccount/CardBankAccount.js +18 -2
  5. package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +40 -25
  6. package/lib/commonjs/components/Dropdown/Dropdown.js +214 -0
  7. package/lib/commonjs/components/DropdownInput/DropdownInput.js +542 -0
  8. package/lib/commonjs/components/FormField/FormField.js +328 -178
  9. package/lib/commonjs/components/LottieIntroBlock/LottieIntroBlock.js +150 -0
  10. package/lib/commonjs/components/PageHero/PageHero.js +153 -0
  11. package/lib/commonjs/components/PoweredByLabel/PoweredByLabel.js +135 -0
  12. package/lib/commonjs/components/PoweredByLabel/finvu.png +0 -0
  13. package/lib/commonjs/components/Text/Text.js +9 -2
  14. package/lib/commonjs/components/Tooltip/Tooltip.js +34 -27
  15. package/lib/commonjs/components/index.js +60 -0
  16. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  17. package/lib/commonjs/icons/registry.js +1 -1
  18. package/lib/module/components/AccountCard/AccountCard.js +241 -0
  19. package/lib/module/components/AppBar/AppBar.js +17 -11
  20. package/lib/module/components/CardBankAccount/CardBankAccount.js +17 -2
  21. package/lib/module/components/CheckboxItem/CheckboxItem.js +41 -26
  22. package/lib/module/components/Dropdown/Dropdown.js +206 -0
  23. package/lib/module/components/DropdownInput/DropdownInput.js +536 -0
  24. package/lib/module/components/FormField/FormField.js +330 -180
  25. package/lib/module/components/LottieIntroBlock/LottieIntroBlock.js +144 -0
  26. package/lib/module/components/PageHero/PageHero.js +147 -0
  27. package/lib/module/components/PoweredByLabel/PoweredByLabel.js +130 -0
  28. package/lib/module/components/PoweredByLabel/finvu.png +0 -0
  29. package/lib/module/components/Text/Text.js +9 -2
  30. package/lib/module/components/Tooltip/Tooltip.js +34 -27
  31. package/lib/module/components/index.js +7 -1
  32. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  33. package/lib/module/icons/registry.js +1 -1
  34. package/lib/typescript/src/components/AccountCard/AccountCard.d.ts +81 -0
  35. package/lib/typescript/src/components/CardBankAccount/CardBankAccount.d.ts +9 -2
  36. package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +18 -2
  37. package/lib/typescript/src/components/Dropdown/Dropdown.d.ts +62 -0
  38. package/lib/typescript/src/components/DropdownInput/DropdownInput.d.ts +107 -0
  39. package/lib/typescript/src/components/FormField/FormField.d.ts +76 -19
  40. package/lib/typescript/src/components/LottieIntroBlock/LottieIntroBlock.d.ts +58 -0
  41. package/lib/typescript/src/components/PageHero/PageHero.d.ts +53 -0
  42. package/lib/typescript/src/components/PoweredByLabel/PoweredByLabel.d.ts +70 -0
  43. package/lib/typescript/src/components/Text/Text.d.ts +12 -2
  44. package/lib/typescript/src/components/Tooltip/Tooltip.d.ts +13 -2
  45. package/lib/typescript/src/components/index.d.ts +7 -1
  46. package/lib/typescript/src/icons/registry.d.ts +1 -1
  47. package/package.json +1 -3
  48. package/src/components/AccountCard/AccountCard.tsx +376 -0
  49. package/src/components/AppBar/AppBar.tsx +25 -14
  50. package/src/components/CardBankAccount/CardBankAccount.tsx +29 -3
  51. package/src/components/CheckboxItem/CheckboxItem.tsx +65 -30
  52. package/src/components/Dropdown/Dropdown.tsx +331 -0
  53. package/src/components/DropdownInput/DropdownInput.tsx +819 -0
  54. package/src/components/FormField/FormField.tsx +542 -215
  55. package/src/components/LottieIntroBlock/LottieIntroBlock.tsx +202 -0
  56. package/src/components/PageHero/PageHero.tsx +200 -0
  57. package/src/components/PoweredByLabel/PoweredByLabel.tsx +221 -0
  58. package/src/components/PoweredByLabel/finvu.png +0 -0
  59. package/src/components/Text/Text.tsx +24 -3
  60. package/src/components/Tooltip/Tooltip.tsx +50 -25
  61. package/src/components/index.ts +15 -1
  62. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  63. package/src/icons/registry.ts +1 -1
@@ -0,0 +1,202 @@
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
+ const DEFAULT_MEDIA_SIZE = 117
15
+
16
+ export type LottieIntroBlockProps = {
17
+ /** Headline text shown below the media area. */
18
+ title?: string
19
+ /** Whether to render the supportive paragraph below the title. */
20
+ showSupportText?: boolean
21
+ /** Body/supportive text shown below the title. */
22
+ supportText?: string
23
+ /** Whether to render the action button at the bottom. */
24
+ showButton?: boolean
25
+ /** Label for the default action button. Ignored when `buttonSlot` is provided. */
26
+ buttonLabel?: string
27
+ /** Press handler for the default action button. Ignored when `buttonSlot` is provided. */
28
+ onButtonPress?: () => void
29
+ /**
30
+ * Custom slot for the media area (Lottie animation, illustration, or image).
31
+ * Should render at the design size of 117x117. If omitted, a neutral
32
+ * placeholder of the same size is rendered so the layout stays stable.
33
+ * `modes` are automatically cascaded into this slot.
34
+ */
35
+ media?: React.ReactNode
36
+ /**
37
+ * Optional slot to fully override the action button.
38
+ * When provided, `showButton`, `buttonLabel`, and `onButtonPress` are ignored.
39
+ * `modes` are automatically cascaded into this slot.
40
+ */
41
+ buttonSlot?: React.ReactNode
42
+ /** Mode configuration for design-token theming. */
43
+ modes?: Record<string, any>
44
+ /** Style overrides applied to the outer container. */
45
+ style?: StyleProp<ViewStyle>
46
+ testID?: string
47
+ }
48
+
49
+ /**
50
+ * LottieIntroBlock displays a centered onboarding/intro block composed of a
51
+ * media slot (typically a Lottie animation or illustration) above a title,
52
+ * an optional supportive paragraph, and an optional action button.
53
+ *
54
+ * All visual values are resolved from Figma design tokens via
55
+ * `getVariableByName`. Slots cascade the active `modes` to their children
56
+ * through `cloneChildrenWithModes`.
57
+ *
58
+ * @component
59
+ * @example
60
+ * ```tsx
61
+ * <LottieIntroBlock
62
+ * title="Let's get to know how your financial health is doing"
63
+ * supportText="From assets to taxes, stay on top of everything in one simple view."
64
+ * buttonLabel="Get started"
65
+ * onButtonPress={() => navigate('NextScreen')}
66
+ * media={<MyLottiePlayer source={animationSource} />}
67
+ * />
68
+ * ```
69
+ */
70
+ function LottieIntroBlock({
71
+ title = "Let's get to know how your financial health is doing",
72
+ showSupportText = true,
73
+ supportText = 'From assets to taxes, stay on top of everything in one simple view.',
74
+ showButton = true,
75
+ buttonLabel = 'Button',
76
+ onButtonPress,
77
+ media,
78
+ buttonSlot,
79
+ modes: propModes = EMPTY_MODES,
80
+ style,
81
+ testID,
82
+ }: LottieIntroBlockProps) {
83
+ const { modes: globalModes } = useTokens()
84
+ const modes = useMemo(
85
+ () => ({ ...globalModes, ...propModes }),
86
+ [globalModes, propModes]
87
+ )
88
+
89
+ // Container
90
+ const gap = Number(getVariableByName('lottieIntroBlock/gap', modes)) || 36
91
+ const paddingHorizontal =
92
+ Number(getVariableByName('lottieIntroBlock/padding/horizontal', modes)) || 0
93
+ const paddingVertical =
94
+ Number(getVariableByName('lottieIntroBlock/padding/vertical', modes)) || 16
95
+
96
+ // Text wrap
97
+ const textWrapGap =
98
+ Number(getVariableByName('lottieIntroBlock/textWrap/gap', modes)) || 16
99
+
100
+ // Title
101
+ const titleColor =
102
+ getVariableByName('lottieIntroBlock/title/foreground', modes) || '#0d0d0f'
103
+ const titleFontSize =
104
+ Number(getVariableByName('lottieIntroBlock/title/fontSize', modes)) || 23
105
+ const titleFontFamily =
106
+ getVariableByName('lottieIntroBlock/title/fontFamily', modes) || 'System'
107
+ const titleLineHeight =
108
+ Number(getVariableByName('lottieIntroBlock/title/lineHeight', modes)) || 23
109
+ const titleFontWeight =
110
+ getVariableByName('lottieIntroBlock/title/fontWeight', modes) || 900
111
+
112
+ // Support text
113
+ const supportColor =
114
+ getVariableByName('lottieIntroBlock/supportText/foreground', modes) ||
115
+ '#0d0d0f'
116
+ const supportFontSize =
117
+ Number(getVariableByName('lottieIntroBlock/supportText/fontSize', modes)) ||
118
+ 14
119
+ const supportFontFamily =
120
+ getVariableByName('lottieIntroBlock/supportText/fontFamily', modes) ||
121
+ 'System'
122
+ const supportLineHeight =
123
+ Number(
124
+ getVariableByName('lottieIntroBlock/supportText/lineHeight', modes)
125
+ ) || 18
126
+ const supportFontWeight =
127
+ getVariableByName('lottieIntroBlock/supportText/fontWeight', modes) || 400
128
+
129
+ const containerStyle: ViewStyle = {
130
+ flexDirection: 'column',
131
+ alignItems: 'center',
132
+ paddingHorizontal,
133
+ paddingVertical,
134
+ gap,
135
+ }
136
+
137
+ const textWrapStyle: ViewStyle = {
138
+ flexDirection: 'column',
139
+ alignItems: 'center',
140
+ gap: textWrapGap,
141
+ width: '100%',
142
+ }
143
+
144
+ const titleStyle: TextStyle = {
145
+ color: titleColor,
146
+ fontSize: titleFontSize,
147
+ fontFamily: titleFontFamily,
148
+ lineHeight: titleLineHeight,
149
+ fontWeight: String(titleFontWeight) as TextStyle['fontWeight'],
150
+ textAlign: 'center',
151
+ }
152
+
153
+ const supportTextStyle: TextStyle = {
154
+ color: supportColor,
155
+ fontSize: supportFontSize,
156
+ fontFamily: supportFontFamily,
157
+ lineHeight: supportLineHeight,
158
+ fontWeight: String(supportFontWeight) as TextStyle['fontWeight'],
159
+ textAlign: 'center',
160
+ }
161
+
162
+ const mediaContent = useMemo(() => {
163
+ if (media === undefined || media === null) {
164
+ return (
165
+ <View
166
+ style={{
167
+ width: DEFAULT_MEDIA_SIZE,
168
+ height: DEFAULT_MEDIA_SIZE,
169
+ }}
170
+ accessibilityElementsHidden
171
+ importantForAccessibility="no-hide-descendants"
172
+ />
173
+ )
174
+ }
175
+ return cloneChildrenWithModes(media, modes)
176
+ }, [media, modes])
177
+
178
+ const buttonContent = useMemo(() => {
179
+ if (buttonSlot !== undefined && buttonSlot !== null) {
180
+ return cloneChildrenWithModes(buttonSlot, modes)
181
+ }
182
+ if (!showButton) {
183
+ return null
184
+ }
185
+ return <Button label={buttonLabel} onPress={onButtonPress} modes={modes} />
186
+ }, [buttonSlot, showButton, buttonLabel, onButtonPress, modes])
187
+
188
+ return (
189
+ <View style={[containerStyle, style]} testID={testID}>
190
+ {mediaContent}
191
+ <View style={textWrapStyle}>
192
+ <Text style={titleStyle}>{title}</Text>
193
+ {showSupportText && supportText ? (
194
+ <Text style={supportTextStyle}>{supportText}</Text>
195
+ ) : null}
196
+ {buttonContent}
197
+ </View>
198
+ </View>
199
+ )
200
+ }
201
+
202
+ export default LottieIntroBlock
@@ -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
@@ -4,8 +4,17 @@ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
4
4
  import { EMPTY_MODES } from '../../utils/react-utils'
5
5
 
6
6
  export type TextProps = {
7
- /** The text content to display. */
7
+ /**
8
+ * The text content to display. You may also pass content as JSX children
9
+ * (e.g. `<Text>Hello</Text>`) — when both are provided, `children` wins.
10
+ */
8
11
  text?: string
12
+ /**
13
+ * Child nodes (string, number, or nested <Text>/<RNText> elements). This
14
+ * mirrors the standard React Native `<Text>` API so the component is a
15
+ * near-drop-in replacement.
16
+ */
17
+ children?: React.ReactNode
9
18
  /** Horizontal alignment of the text. */
10
19
  textAlign?: 'Left' | 'Center'
11
20
  /** Modes configuration for design token resolution. */
@@ -22,7 +31,8 @@ const TEXT_ALIGN_MAP: Record<NonNullable<TextProps['textAlign']>, TextStyle['tex
22
31
  }
23
32
 
24
33
  function Text({
25
- text = 'Korem ipsum ',
34
+ text,
35
+ children,
26
36
  textAlign = 'Left',
27
37
  modes = EMPTY_MODES,
28
38
  style,
@@ -45,12 +55,23 @@ function Text({
45
55
  textAlign: TEXT_ALIGN_MAP[textAlign],
46
56
  }
47
57
 
58
+ // Prefer JSX children when present, otherwise fall back to the `text` prop.
59
+ // Keep the storybook placeholder as a last resort so the Default story
60
+ // still renders something visible when no content is supplied via either
61
+ // route.
62
+ const content =
63
+ children !== undefined && children !== null && children !== false
64
+ ? children
65
+ : text !== undefined
66
+ ? text
67
+ : 'Korem ipsum '
68
+
48
69
  return (
49
70
  <RNText
50
71
  style={[textStyle, style]}
51
72
  numberOfLines={numberOfLines}
52
73
  >
53
- {text}
74
+ {content}
54
75
  </RNText>
55
76
  )
56
77
  }