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