Skip to content

Commit 32ff733

Browse files
@W-21094171: Fix Resend Code for OTP modal (#3624)
* One click checkout * changes to fix install, tests and lint - needs to be reviewed * revert the test change in pwa-kit-runtime * @W-20892497 Show Phone number in Contact Info summary (#3576) * W-20892497 Show Phone number in Contact Info summary * fix lint * @W-20892592 Remove gift messaging for multi shipment (#3579) * W-20892592 Remove gift messaging for multi shipment * translations * @W-20892530 @W-20892577 Billing Address Validation and Using contact phone for user registration (#3583) * W-20892530 Billing Address Validation * W-20892577 save contact info phone * Fix SDK tests (#3593) * fix sdk tests and app bundle size * fix lint * @ W-20540715 Address 1CC feature branch review comments (#3619) * address first set of comments * address rest of code review comments * reverting default.js changes * fix package versions * shipping options fix * attempt to fix flaky tests * passwordless mode updates * @W-21094171: Fix Resend Code for OTP modal Signed-off-by: d.phan <d.phan@salesforce.com> * add translations Signed-off-by: d.phan <d.phan@salesforce.com> * fix lint Signed-off-by: d.phan <d.phan@salesforce.com> * update component per UX alignment Signed-off-by: d.phan <d.phan@salesforce.com> * no actions during verification Signed-off-by: d.phan <d.phan@salesforce.com> * revert to generic error message as per code review comment * transaltion changes * fix the rebase issue * update isomorphic version * update isomorhic in dev dependencies * Updating another dev dependency for isomorphic version * @W-21094171: Fix Resend Code for OTP modal Signed-off-by: d.phan <d.phan@salesforce.com> * fix package versions --------- Signed-off-by: d.phan <d.phan@salesforce.com> Co-authored-by: Sushma Yadupathi <syadupathi@salesforce.com> Co-authored-by: syadupathi-sf <66088780+syadupathi-sf@users.noreply.github.com>
1 parent 9b5050f commit 32ff733

File tree

10 files changed

+328
-201
lines changed

10 files changed

+328
-201
lines changed

packages/commerce-sdk-react/package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/commerce-sdk-react/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"version": "node ./scripts/version.js"
4141
},
4242
"dependencies": {
43-
"commerce-sdk-isomorphic": "5.0.0-preview.1",
43+
"commerce-sdk-isomorphic": "5.0.0",
4444
"js-cookie": "^3.0.1",
4545
"jwt-decode": "^4.0.0"
4646
},

packages/template-retail-react-app/app/components/otp-auth/index.jsx

Lines changed: 111 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ const OtpAuth = ({
3737
handleOtpVerification,
3838
onCheckoutAsGuest,
3939
isGuestRegistration = false,
40-
hideCheckoutAsGuestButton = false
40+
hideCheckoutAsGuestButton = false,
41+
resendCooldownDuration = 30
4142
}) => {
4243
const {tokenLength} = getConfig().app.login
4344
const parsedLength = Number(tokenLength)
@@ -99,6 +100,8 @@ const OtpAuth = ({
99100
otpInputs.clear()
100101
setError('')
101102
form.setValue('otp', '')
103+
// Start resend cooldown when modal opens
104+
setResendTimer(resendCooldownDuration)
102105

103106
// Track OTP modal view activity
104107
track('/otp-authentication', {
@@ -108,10 +111,10 @@ const OtpAuth = ({
108111

109112
setTimeout(() => otpInputs.inputRefs.current[0]?.focus(), 100)
110113
}
111-
}, [isOpen])
114+
}, [isOpen, resendCooldownDuration])
112115

113116
const handleVerify = async (code = otpInputs.values.join('')) => {
114-
if (code.length !== OTP_LENGTH) return
117+
if (isVerifying || code.length !== OTP_LENGTH) return
115118

116119
setIsVerifying(true)
117120
setError('')
@@ -147,14 +150,17 @@ const OtpAuth = ({
147150
}
148151

149152
const handleResend = async () => {
150-
setResendTimer(5)
153+
// No action while verifying or during cooldown; button stays visible/enabled
154+
if (isVerifying || resendTimer > 0) return
155+
156+
setResendTimer(resendCooldownDuration)
151157
try {
152158
await track('/otp-resend', {
153159
activity: 'otp_code_resent',
154160
context: 'authentication',
155161
resendAttempt: true
156162
})
157-
await handleSendEmailOtp(form.getValues('email'))
163+
await handleSendEmailOtp(form.getValues('email'), true)
158164
} catch (error) {
159165
setResendTimer(0)
160166
await track('/otp-resend-failed', {
@@ -167,6 +173,8 @@ const OtpAuth = ({
167173
}
168174

169175
const handleCheckoutAsGuest = async () => {
176+
if (isVerifying) return
177+
170178
// Track checkout as guest selection
171179
await track('/checkout-as-guest', {
172180
activity: 'checkout_as_guest_selected',
@@ -191,8 +199,6 @@ const OtpAuth = ({
191199
}
192200
}
193201

194-
const isResendDisabled = resendTimer > 0 || isVerifying
195-
196202
return (
197203
<Modal isOpen={isOpen} onClose={onClose} isCentered size="lg" closeOnOverlayClick={false}>
198204
<ModalOverlay />
@@ -213,7 +219,7 @@ const OtpAuth = ({
213219
<ModalCloseButton disabled={isVerifying} />
214220
<ModalBody pb={6}>
215221
<Stack spacing={12} paddingLeft={4} paddingRight={4} alignItems="center">
216-
<Text fontSize="md" maxWidth="300px" textAlign="center">
222+
<Text fontSize="md" maxWidth={80} textAlign="center">
217223
{isGuestRegistration ? (
218224
<FormattedMessage
219225
defaultMessage="We sent a one-time password (OTP) to your email. To create your account and proceed to checkout, enter the {otpLength}-digit code below."
@@ -228,116 +234,111 @@ const OtpAuth = ({
228234
)}
229235
</Text>
230236

231-
{/* OTP Input */}
232-
<SimpleGrid columns={OTP_LENGTH} spacing={3}>
233-
{Array.from({length: OTP_LENGTH}).map((_, index) => (
234-
<Input
235-
key={index}
236-
ref={(el) => (otpInputs.inputRefs.current[index] = el)}
237-
value={otpInputs.values[index]}
238-
onChange={(e) => handleInputChange(index, e.target.value)}
239-
onKeyDown={(e) => otpInputs.handleKeyDown(index, e)}
240-
onPaste={otpInputs.handlePaste}
241-
type="text"
242-
inputMode="numeric"
243-
maxLength={1}
244-
textAlign="center"
245-
fontSize="lg"
246-
fontWeight="bold"
247-
size="lg"
248-
width="48px"
249-
height="56px"
250-
borderRadius="md"
251-
borderColor="gray.300"
252-
borderWidth="2px"
253-
disabled={isVerifying}
254-
_focus={{
255-
borderColor: 'blue.500',
256-
boxShadow: '0 0 0 1px var(--chakra-colors-blue-500)'
257-
}}
258-
_hover={{
259-
borderColor: 'gray.400'
260-
}}
261-
/>
262-
))}
263-
</SimpleGrid>
237+
<Stack spacing={6} width="100%" alignItems="center">
238+
{/* OTP Input */}
239+
<SimpleGrid columns={OTP_LENGTH} spacing={3}>
240+
{Array.from({length: OTP_LENGTH}).map((_, index) => (
241+
<Input
242+
key={index}
243+
ref={(el) => (otpInputs.inputRefs.current[index] = el)}
244+
value={otpInputs.values[index]}
245+
onChange={(e) => handleInputChange(index, e.target.value)}
246+
onKeyDown={(e) => otpInputs.handleKeyDown(index, e)}
247+
onPaste={otpInputs.handlePaste}
248+
type="text"
249+
inputMode="numeric"
250+
maxLength={1}
251+
textAlign="center"
252+
fontSize="lg"
253+
fontWeight="bold"
254+
size="lg"
255+
width={12}
256+
height={14}
257+
borderRadius="md"
258+
borderColor={error ? 'red.500' : 'gray.300'}
259+
borderWidth={2}
260+
disabled={isVerifying}
261+
_focus={{
262+
borderColor: error ? 'red.500' : 'blue.500',
263+
boxShadow: error
264+
? '0 0 0 1px var(--chakra-colors-red-500)'
265+
: '0 0 0 1px var(--chakra-colors-blue-500)'
266+
}}
267+
_hover={{
268+
borderColor: error ? 'red.500' : 'gray.400'
269+
}}
270+
/>
271+
))}
272+
</SimpleGrid>
264273

265-
{/* Loading indicator during verification */}
266-
{isVerifying && (
267-
<Text fontSize="sm" color="blue.500">
268-
<FormattedMessage
269-
defaultMessage="Verifying code..."
270-
id="otp.message.verifying"
271-
/>
272-
</Text>
273-
)}
274+
{/* Error message */}
275+
{error && (
276+
<Text fontSize="sm" color="red.500" textAlign="center">
277+
{error}
278+
</Text>
279+
)}
274280

275-
{/* Error message */}
276-
{error && (
277-
<Text fontSize="sm" color="red.500" textAlign="center">
278-
{error}
279-
</Text>
280-
)}
281+
{/* Countdown message */}
282+
{resendTimer > 0 && (
283+
<Text fontSize="sm" color="gray.600" textAlign="center">
284+
<FormattedMessage
285+
defaultMessage="You can request a new code in {timer} {timer, plural, one {second} other {seconds}}."
286+
id="otp.message.resend_cooldown"
287+
values={{timer: resendTimer}}
288+
/>
289+
</Text>
290+
)}
291+
292+
{/* Buttons */}
293+
<HStack spacing={4} width="100%" justifyContent="flex-end">
294+
{!hideCheckoutAsGuestButton && (
295+
<Button
296+
onClick={handleCheckoutAsGuest}
297+
variant="solid"
298+
size="lg"
299+
minWidth={40}
300+
isDisabled={isVerifying}
301+
bg="gray.50"
302+
color="gray.800"
303+
fontWeight="bold"
304+
border="none"
305+
_hover={{
306+
bg: 'gray.100'
307+
}}
308+
_active={{
309+
bg: 'gray.200'
310+
}}
311+
>
312+
{isGuestRegistration ? (
313+
<FormattedMessage
314+
defaultMessage="Cancel"
315+
id="otp.button.cancel_guest_registration"
316+
/>
317+
) : (
318+
<FormattedMessage
319+
defaultMessage="Checkout as a Guest"
320+
id="otp.button.checkout_as_guest"
321+
/>
322+
)}
323+
</Button>
324+
)}
281325

282-
{/* Buttons */}
283-
<HStack spacing={4} width="100%" justifyContent="center">
284-
{!hideCheckoutAsGuestButton && (
285326
<Button
286-
onClick={handleCheckoutAsGuest}
327+
onClick={handleResend}
287328
variant="solid"
288329
size="lg"
289-
minWidth="160px"
290-
isDisabled={isVerifying}
291-
bg="gray.50"
292-
color="gray.800"
293-
fontWeight="bold"
294-
border="none"
295-
_hover={{
296-
bg: 'gray.100'
297-
}}
298-
_active={{
299-
bg: 'gray.200'
300-
}}
330+
colorScheme="blue"
331+
bg="blue.500"
332+
minWidth={40}
333+
_hover={{bg: 'blue.600'}}
301334
>
302-
{isGuestRegistration ? (
303-
<FormattedMessage
304-
defaultMessage="Cancel"
305-
id="otp.button.cancel_guest_registration"
306-
/>
307-
) : (
308-
<FormattedMessage
309-
defaultMessage="Checkout as a Guest"
310-
id="otp.button.checkout_as_guest"
311-
/>
312-
)}
313-
</Button>
314-
)}
315-
316-
<Button
317-
onClick={handleResend}
318-
variant="solid"
319-
size="lg"
320-
colorScheme={isResendDisabled ? 'gray' : 'blue'}
321-
bg={isResendDisabled ? 'gray.300' : 'blue.500'}
322-
minWidth="160px"
323-
isDisabled={isResendDisabled}
324-
_hover={isResendDisabled ? {} : {bg: 'blue.600'}}
325-
_disabled={{bg: 'gray.300', color: 'gray.600'}}
326-
>
327-
{resendTimer > 0 ? (
328-
<FormattedMessage
329-
defaultMessage="Resend code in {timer} seconds..."
330-
id="otp.button.resend_timer"
331-
values={{timer: resendTimer}}
332-
/>
333-
) : (
334335
<FormattedMessage
335336
defaultMessage="Resend Code"
336337
id="otp.button.resend_code"
337338
/>
338-
)}
339-
</Button>
340-
</HStack>
339+
</Button>
340+
</HStack>
341+
</Stack>
341342
</Stack>
342343
</ModalBody>
343344
</ModalContent>
@@ -353,7 +354,9 @@ OtpAuth.propTypes = {
353354
handleOtpVerification: PropTypes.func.isRequired,
354355
onCheckoutAsGuest: PropTypes.func,
355356
isGuestRegistration: PropTypes.bool,
356-
hideCheckoutAsGuestButton: PropTypes.bool
357+
hideCheckoutAsGuestButton: PropTypes.bool,
358+
/** Resend cooldown (in seconds). Default 30. */
359+
resendCooldownDuration: PropTypes.number
357360
}
358361

359362
export default OtpAuth

0 commit comments

Comments
 (0)