jfs-components 0.0.72 → 0.0.73

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 (116) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/lib/commonjs/components/AccordionCheckbox/AccordionCheckbox.js +239 -0
  3. package/lib/commonjs/components/BrandChip/BrandChip.js +149 -0
  4. package/lib/commonjs/components/CardBankAccount/CardBankAccount.js +213 -0
  5. package/lib/commonjs/components/CardInsight/CardInsight.js +166 -0
  6. package/lib/commonjs/components/CheckboxGroup/CheckboxGroup.js +67 -0
  7. package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +125 -0
  8. package/lib/commonjs/components/CircularProgressBar/CircularProgressBar.js +56 -9
  9. package/lib/commonjs/components/CoverageBarComparison/CoverageBarComparison.js +272 -0
  10. package/lib/commonjs/components/CoverageRing/CoverageRing.js +141 -0
  11. package/lib/commonjs/components/DonutChart/DonutChart.js +309 -0
  12. package/lib/commonjs/components/DonutChartSummary/DonutChartSummary.js +155 -0
  13. package/lib/commonjs/components/LinearMeter/LinearMeter.js +9 -28
  14. package/lib/commonjs/components/LinearProgress/LinearProgress.js +68 -0
  15. package/lib/commonjs/components/MetricLegendItem/MetricLegendItem.js +95 -0
  16. package/lib/commonjs/components/MonthlyStatusGrid/MonthlyStatusGrid.js +286 -0
  17. package/lib/commonjs/components/OTP/OTP.js +381 -37
  18. package/lib/commonjs/components/ProductOverview/ProductOverview.js +147 -0
  19. package/lib/commonjs/components/RangeTrack/RangeTrack.js +269 -0
  20. package/lib/commonjs/components/SavingsGoalSummary/SavingsGoalSummary.js +181 -0
  21. package/lib/commonjs/components/SegmentedTrack/SegmentedTrack.js +171 -0
  22. package/lib/commonjs/components/StatGroup/StatGroup.js +128 -0
  23. package/lib/commonjs/components/StatItem/StatItem.js +65 -35
  24. package/lib/commonjs/components/StrengthIndicator/StrengthIndicator.js +157 -0
  25. package/lib/commonjs/components/SummaryTile/SummaryTile.js +150 -0
  26. package/lib/commonjs/components/index.js +171 -1
  27. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  28. package/lib/commonjs/icons/registry.js +1 -1
  29. package/lib/commonjs/utils/index.js +7 -0
  30. package/lib/commonjs/utils/number-utils.js +57 -0
  31. package/lib/module/components/AccordionCheckbox/AccordionCheckbox.js +233 -0
  32. package/lib/module/components/BrandChip/BrandChip.js +143 -0
  33. package/lib/module/components/CardBankAccount/CardBankAccount.js +208 -0
  34. package/lib/module/components/CardInsight/CardInsight.js +161 -0
  35. package/lib/module/components/CheckboxGroup/CheckboxGroup.js +62 -0
  36. package/lib/module/components/CheckboxItem/CheckboxItem.js +119 -0
  37. package/lib/module/components/CircularProgressBar/CircularProgressBar.js +56 -9
  38. package/lib/module/components/CoverageBarComparison/CoverageBarComparison.js +266 -0
  39. package/lib/module/components/CoverageRing/CoverageRing.js +136 -0
  40. package/lib/module/components/DonutChart/DonutChart.js +303 -0
  41. package/lib/module/components/DonutChartSummary/DonutChartSummary.js +150 -0
  42. package/lib/module/components/LinearMeter/LinearMeter.js +9 -28
  43. package/lib/module/components/LinearProgress/LinearProgress.js +63 -0
  44. package/lib/module/components/MetricLegendItem/MetricLegendItem.js +90 -0
  45. package/lib/module/components/MonthlyStatusGrid/MonthlyStatusGrid.js +281 -0
  46. package/lib/module/components/OTP/OTP.js +381 -38
  47. package/lib/module/components/ProductOverview/ProductOverview.js +142 -0
  48. package/lib/module/components/RangeTrack/RangeTrack.js +263 -0
  49. package/lib/module/components/SavingsGoalSummary/SavingsGoalSummary.js +175 -0
  50. package/lib/module/components/SegmentedTrack/SegmentedTrack.js +166 -0
  51. package/lib/module/components/StatGroup/StatGroup.js +123 -0
  52. package/lib/module/components/StatItem/StatItem.js +66 -36
  53. package/lib/module/components/StrengthIndicator/StrengthIndicator.js +152 -0
  54. package/lib/module/components/SummaryTile/SummaryTile.js +145 -0
  55. package/lib/module/components/index.js +21 -1
  56. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  57. package/lib/module/icons/registry.js +1 -1
  58. package/lib/module/utils/index.js +2 -1
  59. package/lib/module/utils/number-utils.js +53 -0
  60. package/lib/typescript/src/components/AccordionCheckbox/AccordionCheckbox.d.ts +71 -0
  61. package/lib/typescript/src/components/BrandChip/BrandChip.d.ts +43 -0
  62. package/lib/typescript/src/components/CardBankAccount/CardBankAccount.d.ts +79 -0
  63. package/lib/typescript/src/components/CardInsight/CardInsight.d.ts +48 -0
  64. package/lib/typescript/src/components/CheckboxGroup/CheckboxGroup.d.ts +41 -0
  65. package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +56 -0
  66. package/lib/typescript/src/components/CircularProgressBar/CircularProgressBar.d.ts +11 -1
  67. package/lib/typescript/src/components/CoverageBarComparison/CoverageBarComparison.d.ts +105 -0
  68. package/lib/typescript/src/components/CoverageRing/CoverageRing.d.ts +90 -0
  69. package/lib/typescript/src/components/DonutChart/DonutChart.d.ts +117 -0
  70. package/lib/typescript/src/components/DonutChartSummary/DonutChartSummary.d.ts +103 -0
  71. package/lib/typescript/src/components/LinearProgress/LinearProgress.d.ts +17 -0
  72. package/lib/typescript/src/components/MetricLegendItem/MetricLegendItem.d.ts +37 -0
  73. package/lib/typescript/src/components/MonthlyStatusGrid/MonthlyStatusGrid.d.ts +119 -0
  74. package/lib/typescript/src/components/OTP/OTP.d.ts +88 -2
  75. package/lib/typescript/src/components/ProductOverview/ProductOverview.d.ts +39 -0
  76. package/lib/typescript/src/components/RangeTrack/RangeTrack.d.ts +173 -0
  77. package/lib/typescript/src/components/SavingsGoalSummary/SavingsGoalSummary.d.ts +95 -0
  78. package/lib/typescript/src/components/SegmentedTrack/SegmentedTrack.d.ts +108 -0
  79. package/lib/typescript/src/components/StatGroup/StatGroup.d.ts +45 -0
  80. package/lib/typescript/src/components/StatItem/StatItem.d.ts +24 -7
  81. package/lib/typescript/src/components/StrengthIndicator/StrengthIndicator.d.ts +58 -0
  82. package/lib/typescript/src/components/SummaryTile/SummaryTile.d.ts +60 -0
  83. package/lib/typescript/src/components/index.d.ts +22 -2
  84. package/lib/typescript/src/icons/registry.d.ts +1 -1
  85. package/lib/typescript/src/utils/index.d.ts +1 -0
  86. package/lib/typescript/src/utils/number-utils.d.ts +29 -0
  87. package/package.json +1 -1
  88. package/src/components/AccordionCheckbox/AccordionCheckbox.tsx +323 -0
  89. package/src/components/BrandChip/BrandChip.tsx +235 -0
  90. package/src/components/CardBankAccount/CardBankAccount.tsx +295 -0
  91. package/src/components/CardInsight/CardInsight.tsx +239 -0
  92. package/src/components/CheckboxGroup/CheckboxGroup.tsx +86 -0
  93. package/src/components/CheckboxItem/CheckboxItem.tsx +174 -0
  94. package/src/components/CircularProgressBar/CircularProgressBar.tsx +74 -9
  95. package/src/components/CoverageBarComparison/CoverageBarComparison.tsx +378 -0
  96. package/src/components/CoverageRing/CoverageRing.tsx +225 -0
  97. package/src/components/DonutChart/DonutChart.tsx +503 -0
  98. package/src/components/DonutChartSummary/DonutChartSummary.tsx +256 -0
  99. package/src/components/LinearMeter/LinearMeter.tsx +9 -39
  100. package/src/components/LinearProgress/LinearProgress.tsx +92 -0
  101. package/src/components/MetricLegendItem/MetricLegendItem.tsx +167 -0
  102. package/src/components/MonthlyStatusGrid/MonthlyStatusGrid.tsx +438 -0
  103. package/src/components/OTP/OTP.tsx +476 -29
  104. package/src/components/ProductOverview/ProductOverview.tsx +236 -0
  105. package/src/components/RangeTrack/RangeTrack.tsx +394 -0
  106. package/src/components/SavingsGoalSummary/SavingsGoalSummary.tsx +269 -0
  107. package/src/components/SegmentedTrack/SegmentedTrack.tsx +268 -0
  108. package/src/components/StatGroup/StatGroup.tsx +169 -0
  109. package/src/components/StatItem/StatItem.tsx +117 -40
  110. package/src/components/StrengthIndicator/StrengthIndicator.tsx +205 -0
  111. package/src/components/SummaryTile/SummaryTile.tsx +251 -0
  112. package/src/components/index.ts +32 -2
  113. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  114. package/src/icons/registry.ts +1 -1
  115. package/src/utils/index.ts +1 -0
  116. package/src/utils/number-utils.ts +60 -0
@@ -1,13 +1,252 @@
1
1
  "use strict";
2
2
 
3
- import React, { useState, useRef, useCallback, useEffect } from 'react';
3
+ import React, { useState, useRef, useCallback, useEffect, useMemo } from 'react';
4
4
  import { View, Text, TextInput as RNTextInput, Pressable, Animated } from 'react-native';
5
5
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
6
  import { useTokens } from '../../design-tokens/JFSThemeProvider';
7
7
  import SupportText from '../SupportText/SupportText';
8
+ import Button from '../Button/Button';
8
9
  import { cloneChildrenWithModes, EMPTY_MODES } from '../../utils/react-utils';
10
+
11
+ // Default mode overrides for the resend Button. Per design: a small,
12
+ // low-emphasis, neutral-appearance button. Consumers can override any of
13
+ // these via OTPResendConfig.resendButtonModes.
9
14
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
15
+ const DEFAULT_RESEND_BUTTON_MODES = {
16
+ AppearanceBrand: 'Neutral',
17
+ 'Button / Size': 'S',
18
+ Emphasis: 'Low'
19
+ };
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // useOtpResend — headless state machine for the resend countdown.
23
+ //
24
+ // State machine: counting -> ready -> sending -> counting -> ...
25
+ //
26
+ // counting : timer is ticking down; resend button disabled.
27
+ // ready : timer elapsed; resend button enabled.
28
+ // sending : an in-flight onResend() promise is pending; button shows
29
+ // a transient "sending" label, prevents double-fire, and
30
+ // restarts the countdown only on resolve.
31
+ //
32
+ // Designed as a hook so consumers can render any UI they want and still
33
+ // reuse the timing/lifecycle logic. The OTPResend component below is the
34
+ // opinionated default.
35
+ // ---------------------------------------------------------------------------
36
+
37
+ /**
38
+ * Headless hook that drives an OTP resend countdown.
39
+ *
40
+ * The hook is intentionally UI-agnostic: it returns just enough state
41
+ * (`state`, `secondsLeft`, plus `resend()` / `restart()` / `skip()`) so
42
+ * consumers can render their own button, support text, etc. The bundled
43
+ * `OTPResend` component is the canonical UI on top of this hook.
44
+ */
45
+ export function useOtpResend({
46
+ durationSeconds = 30,
47
+ onResend,
48
+ autoStart = true
49
+ } = {}) {
50
+ // Initial state: if we're auto-starting, we begin in 'counting' with
51
+ // the full duration; otherwise we go straight to 'ready' (handy for
52
+ // flows where a previous screen already triggered the SMS and we
53
+ // mount with the timer already elapsed).
54
+ const [state, setState] = useState(autoStart ? 'counting' : 'ready');
55
+ const [secondsLeft, setSecondsLeft] = useState(autoStart ? durationSeconds : 0);
56
+
57
+ // Keep a ref to the latest onResend so the resend() callback is stable
58
+ // even if consumers pass an inline arrow function on every render.
59
+ const onResendRef = useRef(onResend);
60
+ useEffect(() => {
61
+ onResendRef.current = onResend;
62
+ }, [onResend]);
63
+ const intervalRef = useRef(null);
64
+ const stopTicker = useCallback(() => {
65
+ if (intervalRef.current !== null) {
66
+ clearInterval(intervalRef.current);
67
+ intervalRef.current = null;
68
+ }
69
+ }, []);
70
+ const startTicker = useCallback(seconds => {
71
+ stopTicker();
72
+ const initial = Math.max(0, Math.floor(seconds));
73
+ if (initial === 0) {
74
+ setState('ready');
75
+ setSecondsLeft(0);
76
+ return;
77
+ }
78
+ setState('counting');
79
+ setSecondsLeft(initial);
80
+ intervalRef.current = setInterval(() => {
81
+ setSecondsLeft(prev => {
82
+ if (prev <= 1) {
83
+ stopTicker();
84
+ setState('ready');
85
+ return 0;
86
+ }
87
+ return prev - 1;
88
+ });
89
+ }, 1000);
90
+ }, [stopTicker]);
91
+
92
+ // Auto-start once on mount if requested. We deliberately omit
93
+ // `durationSeconds` from the dependency array so changing the prop
94
+ // mid-flight does not silently reset the countdown — consumers should
95
+ // call `restart()` explicitly when they want a new duration.
96
+ useEffect(() => {
97
+ if (autoStart) startTicker(durationSeconds);
98
+ return stopTicker;
99
+ // eslint-disable-next-line react-hooks/exhaustive-deps
100
+ }, []);
101
+ const resend = useCallback(async () => {
102
+ // Guard against double-fire and out-of-state taps (e.g. an
103
+ // accessibility re-tap while we're already in 'sending').
104
+ if (state !== 'ready') return;
105
+ try {
106
+ setState('sending');
107
+ const result = onResendRef.current?.();
108
+ if (result && typeof result.then === 'function') {
109
+ await result;
110
+ }
111
+ startTicker(durationSeconds);
112
+ } catch (e) {
113
+ // Surface the failure — the consumer can decide whether to
114
+ // show a toast, retry, etc. We move back to 'ready' so the
115
+ // user can try again immediately.
116
+ setState('ready');
117
+ throw e;
118
+ }
119
+ }, [state, durationSeconds, startTicker]);
120
+ const restart = useCallback(next => {
121
+ startTicker(next ?? durationSeconds);
122
+ }, [durationSeconds, startTicker]);
123
+ const skip = useCallback(() => {
124
+ stopTicker();
125
+ setState('ready');
126
+ setSecondsLeft(0);
127
+ }, [stopTicker]);
128
+ return {
129
+ state,
130
+ secondsLeft,
131
+ canResend: state === 'ready',
132
+ isSending: state === 'sending',
133
+ resend,
134
+ restart,
135
+ skip
136
+ };
137
+ }
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // OTPResend — opinionated UI built on useOtpResend.
141
+ //
142
+ // Renders a SupportText line that shows "Resend OTP in {n}s" while the
143
+ // countdown is active, and turns into a tappable "Resend" button once the
144
+ // countdown elapses. Re-tapping is debounced via the hook's state machine.
145
+ //
146
+ // Exported as a standalone so consumers can position it anywhere in their
147
+ // layout (it does not need to live inside <OTP />). When passed via
148
+ // <OTP resend={...} />, OTP renders this internally in the support area.
149
+ // ---------------------------------------------------------------------------
150
+
151
+ const defaultFormatCountdown = s => `Resend OTP in ${s}s`;
152
+ export function OTPResend({
153
+ durationSeconds = 30,
154
+ onResend,
155
+ autoStart = true,
156
+ formatCountdown,
157
+ sendingLabel = 'Sending…',
158
+ resendLabel = 'Resend',
159
+ countdownStatus = 'Loading',
160
+ resendButtonModes,
161
+ modes: propModes = EMPTY_MODES,
162
+ style
163
+ }) {
164
+ const {
165
+ modes: globalModes
166
+ } = useTokens();
167
+ const modes = useMemo(() => ({
168
+ ...globalModes,
169
+ ...propModes
170
+ }), [globalModes, propModes]);
171
+
172
+ // The Button gets the consumer's modes layered first, then the
173
+ // component-default brand/size/emphasis trio, and finally any caller
174
+ // overrides via `resendButtonModes`. Spreading in this order lets
175
+ // consumers (a) keep theming like Color Mode propagating through,
176
+ // (b) rely on the small/low/neutral defaults out of the box, and
177
+ // (c) selectively override e.g. `Button / Size: 'M'` without losing
178
+ // the other defaults.
179
+ const resolvedButtonModes = useMemo(() => ({
180
+ ...modes,
181
+ ...DEFAULT_RESEND_BUTTON_MODES,
182
+ ...resendButtonModes
183
+ }), [modes, resendButtonModes]);
184
+ const {
185
+ state,
186
+ secondsLeft,
187
+ canResend,
188
+ resend
189
+ } = useOtpResend({
190
+ durationSeconds,
191
+ onResend,
192
+ autoStart
193
+ });
194
+ const formatter = formatCountdown ?? defaultFormatCountdown;
195
+
196
+ // counting → static SupportText. Not a Pressable: tapping the
197
+ // countdown should not do anything (it's an inert status line).
198
+ if (state === 'counting') {
199
+ return /*#__PURE__*/_jsx(SupportText, {
200
+ label: formatter(secondsLeft),
201
+ status: countdownStatus,
202
+ modes: modes,
203
+ style: style
204
+ });
205
+ }
206
+
207
+ // sending → static SupportText with a transient "Sending…" label so
208
+ // the user has clear feedback that their tap was received and we
209
+ // don't accept another tap until the resend completes.
210
+ if (state === 'sending') {
211
+ return /*#__PURE__*/_jsx(SupportText, {
212
+ label: sendingLabel,
213
+ status: countdownStatus,
214
+ modes: modes,
215
+ style: style
216
+ });
217
+ }
218
+
219
+ // ready → render a real Button (no leading/trailing icon — design
220
+ // calls for a clean text-only pill once the countdown elapses). The
221
+ // Button handles its own pressed/hover states from tokens, so we
222
+ // don't need to layer extra opacity etc. on top.
223
+ return /*#__PURE__*/_jsx(Button, {
224
+ label: resendLabel,
225
+ modes: resolvedButtonModes,
226
+ disabled: !canResend,
227
+ onPress: () => {
228
+ // Swallow rejections here — the hook re-throws so callers
229
+ // wiring useOtpResend directly can react, but at the
230
+ // component boundary we never want an unhandled promise.
231
+ resend().catch(() => {});
232
+ },
233
+ accessibilityLabel: resendLabel,
234
+ style: style
235
+ });
236
+ }
237
+
238
+ // ---------------------------------------------------------------------------
239
+ // OTP
240
+ // ---------------------------------------------------------------------------
241
+
10
242
  const DIGITS_ONLY = /^\d*$/;
243
+
244
+ // How long the underline takes to fade in when a slot becomes
245
+ // "highlighted" (filled or actively focused), and to fade out when a slot
246
+ // reverts to idle (e.g. on backspace). The asymmetric durations make the
247
+ // "fades back" cue intentional without being sluggish.
248
+ const SLOT_FADE_IN_MS = 120;
249
+ const SLOT_FADE_OUT_MS = 220;
11
250
  function OTP({
12
251
  length = 6,
13
252
  value: controlledValue,
@@ -22,20 +261,25 @@ function OTP({
22
261
  modes: propModes = EMPTY_MODES,
23
262
  style,
24
263
  supportText,
25
- supportTextStatus
264
+ supportTextStatus,
265
+ errorMessage,
266
+ resend,
267
+ enableSmsAutofill = true
26
268
  }) {
27
269
  const {
28
270
  modes: globalModes
29
271
  } = useTokens();
30
- const modes = {
272
+ const modes = useMemo(() => ({
31
273
  ...globalModes,
32
274
  ...propModes
33
- };
275
+ }), [globalModes, propModes]);
34
276
  const isControlled = controlledValue !== undefined;
35
277
  const [internalValue, setInternalValue] = useState(defaultValue);
36
278
  const currentValue = isControlled ? controlledValue : internalValue;
37
279
  const inputRef = useRef(null);
38
280
  const [isFocused, setIsFocused] = useState(false);
281
+
282
+ // --- Caret blink (unchanged) ---
39
283
  const caretAnim = useRef(new Animated.Value(1)).current;
40
284
  useEffect(() => {
41
285
  if (!isFocused) return;
@@ -81,7 +325,6 @@ function OTP({
81
325
  const otpPaddingV = Number(getVariableByName('otp/padding/vertical', modes)) || 8;
82
326
  const slotWidth = Number(getVariableByName('pinSlot/width', modes)) || 48;
83
327
  const slotGap = Number(getVariableByName('pinSlot/gap', modes)) || 8;
84
- // digit/color has no state variants in Figma — resolved once from the Output collection
85
328
  const digitColor = getVariableByName('pinSlot/digit/color', modes) || '#000000';
86
329
  const digitFontSize = Number(getVariableByName('pinSlot/digit/fontSize', modes)) || 24;
87
330
  const digitFontFamily = getVariableByName('pinSlot/digit/fontFamily', modes) || 'JioType Var';
@@ -90,25 +333,68 @@ function OTP({
90
333
  const underlineHeight = Number(getVariableByName('pinSlot/underline/height', modes)) || 2;
91
334
  const underlineRadius = Number(getVariableByName('pinSlot/underline/radius', modes)) || 1;
92
335
 
93
- // --- State-driven slot modes ---
94
- // Collection name in Figma is "Input/PINSlot States" (double space before States).
95
- // Only PinSlot/underline/color (capital P/S) lives in this collection with Idle/Active/Error modes.
96
- // isInvalid takes priority over active focus; the component maps semantic state → token mode
97
- // internally so consumers never need to know the collection key name.
98
- const getSlotModes = isActiveSlot => {
99
- if (isInvalid) return {
100
- ...modes,
101
- 'Input/PINSlot States': 'Error'
102
- };
103
- if (isActiveSlot && isFocused) return {
104
- ...modes,
105
- 'Input/PINSlot States': 'Active'
106
- };
107
- return {
108
- ...modes,
109
- 'Input/PINSlot States': 'Idle'
110
- };
111
- };
336
+ // --- Resolve the three underline colors ONCE per render. We then
337
+ // animate between idle active per-slot using interpolation; the
338
+ // error color is applied directly (no animation) when isInvalid.
339
+ const idleUnderlineColor = useMemo(() => getVariableByName('PinSlot/underline/color', {
340
+ ...modes,
341
+ 'Input/PINSlot States': 'Idle'
342
+ }) || '#303338', [modes]);
343
+ const activeUnderlineColor = useMemo(() => getVariableByName('PinSlot/underline/color', {
344
+ ...modes,
345
+ 'Input/PINSlot States': 'Active'
346
+ }) || '#5d00b5', [modes]);
347
+ const errorUnderlineColor = useMemo(() => getVariableByName('PinSlot/underline/color', {
348
+ ...modes,
349
+ 'Input/PINSlot States': 'Error'
350
+ }) || '#d92d20', [modes]);
351
+
352
+ // --- Per-slot underline highlight animations ---
353
+ //
354
+ // Each slot owns one Animated.Value in [0, 1]. 0 = Idle color, 1 =
355
+ // Active color. We re-target on every value/focus change:
356
+ //
357
+ // highlighted = isFilled || (isActiveSlot && isFocused)
358
+ //
359
+ // Filled slots stay lit, so adding a digit instantly recruits its
360
+ // slot into the lit cohort. Deleting a digit transitions that slot
361
+ // (which is no longer the active slot — the cursor moved back) from
362
+ // 1 → 0, producing the "fades back" cue the design calls for.
363
+ //
364
+ // We use useNativeDriver:false because backgroundColor cannot run on
365
+ // the native driver (it's a JS-thread layout property). The overhead
366
+ // is fine here: at most `length` (≤ ~8) values transitioning briefly
367
+ // on each keystroke.
368
+ const slotAnimsRef = useRef([]);
369
+ if (slotAnimsRef.current.length !== length) {
370
+ const next = [];
371
+ for (let i = 0; i < length; i++) {
372
+ const existing = slotAnimsRef.current[i];
373
+ // Initialize fresh slots to match their *current* highlight
374
+ // target. This avoids a flash on first mount when consumers
375
+ // pass a non-empty defaultValue (slots would otherwise fade
376
+ // in from 0 even though they're already filled).
377
+ const initial = i < currentValue.length ? 1 : 0;
378
+ next.push(existing ?? new Animated.Value(initial));
379
+ }
380
+ slotAnimsRef.current = next;
381
+ }
382
+ useEffect(() => {
383
+ const anims = slotAnimsRef.current;
384
+ const filledLen = currentValue.length;
385
+ for (let i = 0; i < length; i++) {
386
+ const slotAnim = anims[i];
387
+ if (!slotAnim) continue;
388
+ const isFilled = i < filledLen;
389
+ const isActiveSlot = i === filledLen && filledLen < length && isFocused;
390
+ const target = isFilled || isActiveSlot ? 1 : 0;
391
+ Animated.timing(slotAnim, {
392
+ toValue: target,
393
+ duration: target === 1 ? SLOT_FADE_IN_MS : SLOT_FADE_OUT_MS,
394
+ useNativeDriver: false
395
+ }).start();
396
+ }
397
+ }, [currentValue, isFocused, length]);
112
398
 
113
399
  // --- Styles ---
114
400
  const containerStyle = {
@@ -128,12 +414,6 @@ function OTP({
128
414
  const char = currentValue[index];
129
415
  const isActiveSlot = index === currentValue.length && currentValue.length < length;
130
416
  const isFilled = char !== undefined;
131
-
132
- // Underline color is the only state-sensitive token (lives in "Input/PINSlot States" collection).
133
- // Note: token name is "PinSlot/underline/color" (capital P/S) — different from the static
134
- // "pinSlot/underline/color" in the Output collection.
135
- const slotModes = getSlotModes(isActiveSlot);
136
- const underlineColor = getVariableByName('PinSlot/underline/color', slotModes) || '#303338';
137
417
  const slotStyle = {
138
418
  width: slotWidth,
139
419
  flexDirection: 'column',
@@ -150,11 +430,22 @@ function OTP({
150
430
  textAlign: 'center',
151
431
  minWidth: '100%'
152
432
  };
433
+
434
+ // Pull the per-slot animated value (always exists by this point —
435
+ // we resync the ref array above before render).
436
+ const slotAnim = slotAnimsRef.current[index] ?? new Animated.Value(0);
437
+ const interpolatedColor = slotAnim.interpolate({
438
+ inputRange: [0, 1],
439
+ outputRange: [idleUnderlineColor, activeUnderlineColor]
440
+ });
153
441
  const underlineStyle = {
154
442
  width: slotWidth,
155
443
  height: underlineHeight,
156
444
  borderRadius: underlineRadius,
157
- backgroundColor: underlineColor
445
+ // Error state takes precedence and snaps directly to the
446
+ // error color — animating *to* an error feels less urgent
447
+ // than the snap, and the design uses an instant transition.
448
+ backgroundColor: isInvalid ? errorUnderlineColor : interpolatedColor
158
449
  };
159
450
  return /*#__PURE__*/_jsxs(View, {
160
451
  style: slotStyle,
@@ -181,24 +472,67 @@ function OTP({
181
472
  }],
182
473
  children: '\u00A0'
183
474
  })
184
- }), /*#__PURE__*/_jsx(View, {
475
+ }), /*#__PURE__*/_jsx(Animated.View, {
185
476
  style: underlineStyle
186
477
  })]
187
478
  }, index);
188
479
  };
189
- const renderSupportText = () => {
480
+
481
+ // --- Support area rendering ---
482
+ //
483
+ // Priority:
484
+ // 1. isInvalid → errorMessage (with Error status). Falls back to
485
+ // `supportText` if errorMessage isn't provided, promoting its
486
+ // status to Error.
487
+ // 2. resend (and !isInvalid) → managed countdown / button.
488
+ // 3. supportText → user's static support text.
489
+ // 4. nothing.
490
+ //
491
+ // This split keeps validation a parent concern (the component never
492
+ // tries to "know" what valid means) while still giving consumers a
493
+ // turnkey error UI when they flip `isInvalid`.
494
+ //
495
+ // While `isInvalid` is true we also inject `Status: 'Error'` into the
496
+ // mode set forwarded to the support area. This lets the SupportText
497
+ // (and any nested icon) resolve error-themed Figma tokens — foreground
498
+ // color, icon color, etc. — without consumers having to thread the
499
+ // collection mode in by hand.
500
+ const supportModes = useMemo(() => isInvalid ? {
501
+ ...modes,
502
+ Status: 'Error'
503
+ } : modes, [modes, isInvalid]);
504
+ const renderStaticSupportText = overrideStatus => {
190
505
  if (!supportText) return null;
191
506
  if (typeof supportText === 'string') {
192
507
  return /*#__PURE__*/_jsx(SupportText, {
193
508
  label: supportText,
194
- status: supportTextStatus ?? (isInvalid ? 'Error' : 'Neutral'),
195
- modes: modes
509
+ status: overrideStatus ?? supportTextStatus ?? 'Neutral',
510
+ modes: supportModes
196
511
  });
197
512
  }
198
513
  return /*#__PURE__*/_jsx(_Fragment, {
199
- children: cloneChildrenWithModes(React.Children.toArray(supportText), modes)
514
+ children: cloneChildrenWithModes(React.Children.toArray(supportText), supportModes)
200
515
  });
201
516
  };
517
+ const renderSupportArea = () => {
518
+ if (isInvalid) {
519
+ if (errorMessage) {
520
+ return /*#__PURE__*/_jsx(SupportText, {
521
+ label: errorMessage,
522
+ status: "Error",
523
+ modes: supportModes
524
+ });
525
+ }
526
+ return renderStaticSupportText('Error');
527
+ }
528
+ if (resend) {
529
+ return /*#__PURE__*/_jsx(OTPResend, {
530
+ ...resend,
531
+ modes: supportModes
532
+ });
533
+ }
534
+ return renderStaticSupportText();
535
+ };
202
536
  return /*#__PURE__*/_jsxs(Pressable, {
203
537
  style: [containerStyle, isDisabled && {
204
538
  opacity: 0.4
@@ -216,7 +550,16 @@ function OTP({
216
550
  editable: !isDisabled,
217
551
  onFocus: () => setIsFocused(true),
218
552
  onBlur: () => setIsFocused(false),
219
- caretHidden: true,
553
+ caretHidden: true
554
+ // Cross-platform native one-time-code autofill. iOS reads
555
+ // `textContentType="oneTimeCode"` to surface SMS codes in
556
+ // the QuickType bar (no library needed). Android reads
557
+ // `autoComplete="one-time-code"` (the canonical RN value;
558
+ // also accepted as a hint by the SMS Retriever / SMS User
559
+ // Consent APIs that the host app wires up natively).
560
+ ,
561
+ textContentType: enableSmsAutofill ? 'oneTimeCode' : 'none',
562
+ autoComplete: enableSmsAutofill ? 'one-time-code' : 'off',
220
563
  style: {
221
564
  position: 'absolute',
222
565
  width: 1,
@@ -230,7 +573,7 @@ function OTP({
230
573
  children: Array.from({
231
574
  length
232
575
  }, (_, i) => renderSlot(i))
233
- }), renderSupportText()]
576
+ }), renderSupportArea()]
234
577
  });
235
578
  }
236
579
  export default OTP;
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+
3
+ import React from 'react';
4
+ import { View, Text } from 'react-native';
5
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
7
+ import Image from '../Image/Image';
8
+ import ProductLabel from '../ProductLabel/ProductLabel';
9
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
10
+ const DEFAULT_STATS = [{
11
+ value: '995',
12
+ label: 'Purity'
13
+ }, {
14
+ value: '3%',
15
+ label: 'GST'
16
+ }];
17
+ const ProductOverview = ({
18
+ imageSource,
19
+ imageRatio = 288 / 170,
20
+ labelImageSource,
21
+ label = 'Gold',
22
+ productName = '0.5g Gold Coin',
23
+ description = 'Your gold is insured from our vault to you. If lost or damaged, we’ll replace it.',
24
+ stats = DEFAULT_STATS,
25
+ modes = EMPTY_MODES,
26
+ style,
27
+ children
28
+ }) => {
29
+ const padding = getVariableByName('productOverview/padding', modes) ?? 24;
30
+ const gap = getVariableByName('productOverview/gap', modes) ?? 12;
31
+ const background = getVariableByName('productOverview/background', modes) ?? '#ffffff';
32
+ const productNameColor = getVariableByName('productOverview/productName/color', modes) ?? '#0d0d0f';
33
+ const productNameFontFamily = getVariableByName('productOverview/productName/fontFamily', modes) ?? 'JioType Var';
34
+ const productNameFontSize = getVariableByName('productOverview/productName/fontSize', modes) ?? 26;
35
+ const productNameFontWeight = getVariableByName('productOverview/productName/fontWeight', modes) ?? 900;
36
+ const productNameLineHeight = getVariableByName('productOverview/productName/lineHeight', modes) ?? 26;
37
+ const descriptionColor = getVariableByName('productOverview/description/color', modes) ?? '#1a1c1f';
38
+ const descriptionFontFamily = getVariableByName('productOverview/description/fontFamily', modes) ?? 'JioType Var';
39
+ const descriptionFontSize = getVariableByName('productOverview/description/fontSize', modes) ?? 14;
40
+ const descriptionFontWeight = getVariableByName('productOverview/description/fontWeight', modes) ?? 500;
41
+ const descriptionLineHeight = getVariableByName('productOverview/description/lineHeight', modes) ?? 18.2;
42
+ const statGap = getVariableByName('productOverview/stat/gap', modes) ?? 2;
43
+ const statValueColor = getVariableByName('productOverview/stat/value/color', modes) ?? '#141414';
44
+ const statValueFontFamily = getVariableByName('productOverview/stat/value/fontFamily', modes) ?? 'JioType Var';
45
+ const statValueFontSize = getVariableByName('productOverview/stat/value/fontSize', modes) ?? 20;
46
+ const statValueFontWeight = getVariableByName('productOverview/stat/value/fontWeight', modes) ?? 900;
47
+ const statValueLineHeight = getVariableByName('productOverview/stat/value/lineHeight', modes) ?? 20;
48
+ const statLabelColor = productNameColor;
49
+ const statLabelFontFamily = getVariableByName('productOverview/stat/label/fontFamily', modes) ?? 'JioType Var';
50
+ const statLabelFontSize = getVariableByName('productOverview/stat/label/fontSize', modes) ?? 12;
51
+ const statLabelFontWeight = getVariableByName('productOverview/stat/label/fontWeight', modes) ?? 400;
52
+ const statLabelLineHeight = getVariableByName('productOverview/stat/label/lineHeight', modes) ?? 15.6;
53
+ const productNameStyle = {
54
+ color: productNameColor,
55
+ fontFamily: productNameFontFamily,
56
+ fontSize: productNameFontSize,
57
+ fontWeight: String(productNameFontWeight),
58
+ lineHeight: productNameLineHeight,
59
+ textAlign: 'center'
60
+ };
61
+ const descriptionStyle = {
62
+ color: descriptionColor,
63
+ fontFamily: descriptionFontFamily,
64
+ fontSize: descriptionFontSize,
65
+ fontWeight: String(descriptionFontWeight),
66
+ lineHeight: descriptionLineHeight,
67
+ textAlign: 'center'
68
+ };
69
+ const statValueStyle = {
70
+ color: statValueColor,
71
+ fontFamily: statValueFontFamily,
72
+ fontSize: statValueFontSize,
73
+ fontWeight: String(statValueFontWeight),
74
+ lineHeight: statValueLineHeight
75
+ };
76
+ const statLabelStyle = {
77
+ color: statLabelColor,
78
+ fontFamily: statLabelFontFamily,
79
+ fontSize: statLabelFontSize,
80
+ fontWeight: String(statLabelFontWeight),
81
+ lineHeight: statLabelLineHeight,
82
+ textAlign: 'center'
83
+ };
84
+ const showStats = Array.isArray(stats) && stats.length > 0;
85
+ return /*#__PURE__*/_jsxs(View, {
86
+ style: [{
87
+ backgroundColor: background,
88
+ padding,
89
+ gap,
90
+ alignItems: 'center',
91
+ width: '100%'
92
+ }, style],
93
+ children: [imageSource != null && /*#__PURE__*/_jsx(Image, {
94
+ imageSource: imageSource,
95
+ ratio: imageRatio,
96
+ resizeMode: "contain",
97
+ accessibilityElementsHidden: true,
98
+ importantForAccessibility: "no"
99
+ }), /*#__PURE__*/_jsx(ProductLabel, {
100
+ label: label,
101
+ ...(labelImageSource != null && {
102
+ imageSource: labelImageSource
103
+ }),
104
+ modes: modes
105
+ }), productName ? /*#__PURE__*/_jsx(Text, {
106
+ style: productNameStyle,
107
+ accessibilityRole: "header",
108
+ children: productName
109
+ }) : null, description ? /*#__PURE__*/_jsx(Text, {
110
+ style: descriptionStyle,
111
+ children: description
112
+ }) : null, children ? /*#__PURE__*/_jsx(_Fragment, {
113
+ children: cloneChildrenWithModes(children, modes)
114
+ }) : null, showStats && /*#__PURE__*/_jsx(View, {
115
+ style: {
116
+ flexDirection: 'row',
117
+ alignItems: 'center',
118
+ justifyContent: 'space-between',
119
+ width: '100%'
120
+ },
121
+ children: stats.map((stat, index) => /*#__PURE__*/_jsxs(View, {
122
+ style: {
123
+ flex: 1,
124
+ minWidth: 0,
125
+ alignItems: 'center',
126
+ gap: statGap,
127
+ overflow: 'hidden'
128
+ },
129
+ children: [/*#__PURE__*/_jsx(Text, {
130
+ style: statValueStyle,
131
+ numberOfLines: 1,
132
+ children: stat.value
133
+ }), /*#__PURE__*/_jsx(Text, {
134
+ style: statLabelStyle,
135
+ numberOfLines: 1,
136
+ children: stat.label
137
+ })]
138
+ }, `${stat.label}-${index}`))
139
+ })]
140
+ });
141
+ };
142
+ export default ProductOverview;