diff --git a/apps/bff/src/integrations/salesforce/services/salesforce-account.service.ts b/apps/bff/src/integrations/salesforce/services/salesforce-account.service.ts
index 598943c7..954972aa 100644
--- a/apps/bff/src/integrations/salesforce/services/salesforce-account.service.ts
+++ b/apps/bff/src/integrations/salesforce/services/salesforce-account.service.ts
@@ -159,10 +159,11 @@ export class SalesforceAccountService {
try {
// Search for Contact with matching email and get the associated Account + Contact details
const result = (await this.connection.query(
- `SELECT Account.Id, Account.SF_Account_No__c, FirstName, LastName, MailingStreet, MailingCity, MailingState, MailingPostalCode, MailingCountry FROM Contact WHERE Email = '${this.safeSoql(email)}' LIMIT 1`,
+ `SELECT Account.Id, Account.SF_Account_No__c, Account.${this.portalSourceField}, FirstName, LastName, MailingStreet, MailingCity, MailingState, MailingPostalCode, MailingCountry FROM Contact WHERE Email = '${this.safeSoql(email)}' LIMIT 1`,
{ label: "checkout:findAccountByEmail" }
)) as SalesforceResponse<{
- Account: { Id: string; SF_Account_No__c: string };
+ // Index signature needed: portalSourceField is env-configurable
+ Account: Record & { Id: string; SF_Account_No__c: string };
FirstName?: string | null;
LastName?: string | null;
MailingStreet?: string | null;
@@ -174,12 +175,14 @@ export class SalesforceAccountService {
if (result.totalSize > 0 && result.records[0]?.Account) {
const record = result.records[0];
+ const portalSource = (record.Account[this.portalSourceField] as string | null) ?? undefined;
const hasAddress = record.MailingCity || record.MailingState || record.MailingPostalCode;
return {
id: record.Account.Id,
accountNumber: record.Account.SF_Account_No__c,
...(record.FirstName && { firstName: record.FirstName }),
...(record.LastName && { lastName: record.LastName }),
+ ...(portalSource && { portalSource }),
...(hasAddress && {
address: {
address1: record.MailingStreet || "",
@@ -560,6 +563,7 @@ export interface FindByEmailResult {
accountNumber: string;
firstName?: string;
lastName?: string;
+ portalSource?: string;
address?: {
address1: string;
city: string;
diff --git a/apps/bff/src/modules/auth/infra/workflows/verification-workflow.service.ts b/apps/bff/src/modules/auth/infra/workflows/verification-workflow.service.ts
index ff01135d..f02f9a07 100644
--- a/apps/bff/src/modules/auth/infra/workflows/verification-workflow.service.ts
+++ b/apps/bff/src/modules/auth/infra/workflows/verification-workflow.service.ts
@@ -11,6 +11,8 @@ import {
} from "@customer-portal/domain/get-started";
import { STREET_ADDRESS_PATTERN } from "@customer-portal/domain/address";
+import { PORTAL_SOURCE_INTERNET_ELIGIBILITY } from "@bff/modules/auth/constants/portal.constants.js";
+
import { UsersService } from "@bff/modules/users/application/users.service.js";
import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
import { SalesforceAccountService } from "@bff/integrations/salesforce/services/salesforce-account.service.js";
@@ -215,7 +217,12 @@ export class VerificationWorkflowService {
if (mapping) {
return { status: ACCOUNT_STATUS.PORTAL_EXISTS };
}
- const sfAddress = sfAccount.address ? this.parseSfAddress(sfAccount.address) : undefined;
+ // Only parse address for accounts created via internet eligibility —
+ // those have a known address format (townJa + chome-banchi-go).
+ // All other SF accounts get a blank address form.
+ const isFromEligibility = sfAccount.portalSource === PORTAL_SOURCE_INTERNET_ELIGIBILITY;
+ const sfAddress =
+ isFromEligibility && sfAccount.address ? this.parseSfAddress(sfAccount.address) : undefined;
return {
status: ACCOUNT_STATUS.SF_UNMAPPED,
sfAccountId: sfAccount.id,
diff --git a/apps/portal/src/components/molecules/OtpInput/OtpExpiryDisplay.tsx b/apps/portal/src/components/molecules/OtpInput/OtpExpiryDisplay.tsx
new file mode 100644
index 00000000..70da32d7
--- /dev/null
+++ b/apps/portal/src/components/molecules/OtpInput/OtpExpiryDisplay.tsx
@@ -0,0 +1,34 @@
+"use client";
+
+import { Clock } from "lucide-react";
+
+export function formatTimeRemaining(seconds: number): string {
+ const mins = Math.floor(seconds / 60);
+ const secs = seconds % 60;
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
+}
+
+export function OtpExpiryDisplay({
+ timeRemaining,
+ isExpired,
+}: {
+ timeRemaining: number | null;
+ isExpired: boolean;
+}) {
+ if (isExpired) {
+ return (
+
+ Code expired. Please request a new one.
+
+ );
+ }
+ if (timeRemaining !== null) {
+ return (
+
+
+ Code expires in {formatTimeRemaining(timeRemaining)}
+
+ );
+ }
+ return null;
+}
diff --git a/apps/portal/src/components/molecules/OtpInput/index.ts b/apps/portal/src/components/molecules/OtpInput/index.ts
index 053decd4..992288e9 100644
--- a/apps/portal/src/components/molecules/OtpInput/index.ts
+++ b/apps/portal/src/components/molecules/OtpInput/index.ts
@@ -1 +1,2 @@
export { OtpInput } from "./OtpInput";
+export { OtpExpiryDisplay, formatTimeRemaining } from "./OtpExpiryDisplay";
diff --git a/apps/portal/src/components/molecules/index.ts b/apps/portal/src/components/molecules/index.ts
index 86b11e91..5959afac 100644
--- a/apps/portal/src/components/molecules/index.ts
+++ b/apps/portal/src/components/molecules/index.ts
@@ -10,7 +10,7 @@ export type { DataTableProps, Column } from "./DataTable/DataTable";
// Form components
export { FormField } from "./FormField/FormField";
export type { FormFieldProps } from "./FormField/FormField";
-export { OtpInput } from "./OtpInput";
+export { OtpInput, OtpExpiryDisplay, formatTimeRemaining } from "./OtpInput";
export { SearchFilterBar } from "./SearchFilterBar/SearchFilterBar";
export type { SearchFilterBarProps, FilterOption } from "./SearchFilterBar/SearchFilterBar";
diff --git a/apps/portal/src/features/auth/components/LoginOtpStep/LoginOtpStep.tsx b/apps/portal/src/features/auth/components/LoginOtpStep/LoginOtpStep.tsx
index c96a2071..373fe0b5 100644
--- a/apps/portal/src/features/auth/components/LoginOtpStep/LoginOtpStep.tsx
+++ b/apps/portal/src/features/auth/components/LoginOtpStep/LoginOtpStep.tsx
@@ -9,7 +9,7 @@
import { useState, useCallback, useEffect } from "react";
import { Button, ErrorMessage } from "@/components/atoms";
-import { OtpInput } from "@/components/molecules";
+import { OtpInput, formatTimeRemaining } from "@/components/molecules";
import { ArrowLeft, Mail, Clock } from "lucide-react";
interface LoginOtpStepProps {
@@ -23,15 +23,6 @@ interface LoginOtpStepProps {
error?: string | null;
}
-/**
- * Format remaining time as MM:SS
- */
-function formatTimeRemaining(seconds: number): string {
- const mins = Math.floor(seconds / 60);
- const secs = seconds % 60;
- return `${mins}:${secs.toString().padStart(2, "0")}`;
-}
-
export function LoginOtpStep({
sessionToken: _sessionToken,
maskedEmail,
diff --git a/apps/portal/src/features/get-started/components/GetStartedForm/steps/CompleteAccountStep.tsx b/apps/portal/src/features/get-started/components/GetStartedForm/steps/CompleteAccountStep.tsx
index bc626e6d..c46f2576 100644
--- a/apps/portal/src/features/get-started/components/GetStartedForm/steps/CompleteAccountStep.tsx
+++ b/apps/portal/src/features/get-started/components/GetStartedForm/steps/CompleteAccountStep.tsx
@@ -99,8 +99,6 @@ export function CompleteAccountStep() {
{isSfUnmappedIncomplete && (
diff --git a/apps/portal/src/features/get-started/components/GetStartedForm/steps/VerificationStep.tsx b/apps/portal/src/features/get-started/components/GetStartedForm/steps/VerificationStep.tsx
index 5dbab91e..7ae19573 100644
--- a/apps/portal/src/features/get-started/components/GetStartedForm/steps/VerificationStep.tsx
+++ b/apps/portal/src/features/get-started/components/GetStartedForm/steps/VerificationStep.tsx
@@ -11,50 +11,41 @@
"use client";
-import { useState, useEffect, useCallback, useRef } from "react";
+import { useState, useEffect, useCallback } from "react";
import { Button } from "@/components/atoms";
-import { OtpInput } from "@/components/molecules";
-import { Clock } from "lucide-react";
+import { OtpInput, OtpExpiryDisplay } from "@/components/molecules";
import { useGetStartedMachine } from "../../../hooks/useGetStartedMachine";
const RESEND_COOLDOWN_SECONDS = 60;
const MAX_RESENDS = 3;
-function formatTimeRemaining(seconds: number): string {
- const mins = Math.floor(seconds / 60);
- const secs = seconds % 60;
- return `${mins}:${secs.toString().padStart(2, "0")}`;
-}
-
function useResendCooldown() {
const [countdown, setCountdown] = useState(RESEND_COOLDOWN_SECONDS);
const [resendCount, setResendCount] = useState(0);
- const timerRef = useRef | null>(null);
+ // Re-run timer whenever resendCount changes (initial mount + each resend)
useEffect(() => {
if (countdown <= 0) return;
- timerRef.current = setInterval(() => {
+ const id = setInterval(() => {
setCountdown(prev => {
if (prev <= 1) {
- if (timerRef.current) clearInterval(timerRef.current);
+ clearInterval(id);
return 0;
}
return prev - 1;
});
}, 1000);
- return () => {
- if (timerRef.current) clearInterval(timerRef.current);
- };
- }, [countdown > 0]); // eslint-disable-line react-hooks/exhaustive-deps
+ return () => clearInterval(id);
+ }, [resendCount]); // eslint-disable-line react-hooks/exhaustive-deps
const triggerCooldown = useCallback(() => {
setResendCount(prev => prev + 1);
setCountdown(RESEND_COOLDOWN_SECONDS);
}, []);
- return { countdown, resendCount, triggerCooldown, maxedOut: resendCount >= MAX_RESENDS };
+ return { countdown, triggerCooldown, maxedOut: resendCount >= MAX_RESENDS };
}
function useExpiryTimer(codeExpiresAt: string | null) {
@@ -83,31 +74,6 @@ function useExpiryTimer(codeExpiresAt: string | null) {
return { timeRemaining, isExpired };
}
-function ExpiryDisplay({
- timeRemaining,
- isExpired,
-}: {
- timeRemaining: number | null;
- isExpired: boolean;
-}) {
- if (isExpired) {
- return (
-
- Code expired. Please request a new one.
-
- );
- }
- if (timeRemaining !== null) {
- return (
-
-
- Code expires in {formatTimeRemaining(timeRemaining)}
-
- );
- }
- return null;
-}
-
function ResendButton({
disabled,
maxedOut,
@@ -218,7 +184,7 @@ export function VerificationStep() {
)}
-
+