Skip to content

Akmal / fix: enhance demo account handling in AuthWrapper and CallbackPage#661

Merged
nijil-deriv merged 4 commits into
masterfrom
fix-reset-to-demo
Apr 16, 2026
Merged

Akmal / fix: enhance demo account handling in AuthWrapper and CallbackPage#661
nijil-deriv merged 4 commits into
masterfrom
fix-reset-to-demo

Conversation

@akmal-deriv

@akmal-deriv akmal-deriv commented Apr 16, 2026

Copy link
Copy Markdown
Collaborator
  • fix: enhance demo account handling in AuthWrapper and CallbackPage

@github-actions

github-actions Bot commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

OpenSSF Scorecard

PackageVersionScoreDetails

Scanned Manifest Files

package-lock.json
  • @tootallnate/once@2.0.0

@github-actions

github-actions Bot commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Claude finished @akmal-deriv's task in 2m 9s —— View job


PR Review: fix: enhance demo account handling in AuthWrapper and CallbackPage

Tasks:

  • Gather context and read changed files
  • Analyze auth flow changes in AuthWrapper.tsx
  • Analyze callback page changes in callback-page.tsx
  • Analyze analytics utils changes
  • Write review summary

Summary

This PR improves demo account selection during authentication by introducing a shared isDemoAccount utility and using URL/session state to pick the correct account. The intent is correct and the selected_currency move-before-API-call in callback-page.tsx is a genuine fix. However, there are several issues ranging from high-severity correctness bugs to missing tests.


High Priority

1. DEM/DOT prefixes are unverified and potentially wrongsrc/analytics/utils.ts:56-57

loginid.startsWith('DEM') ||
loginid.startsWith('DOT')

Neither DEM nor DOT appear anywhere else in the codebase as account prefixes, and there is no documentation that Deriv uses these for demo accounts. DOT is also the ticker symbol for the Polkadot cryptocurrency. If Deriv ever issues real accounts with DOT as a prefix (e.g., DOT12345), those accounts would be silently treated as demo — blocking users from trading real funds.

The only virtual account prefixes confirmed in the codebase are VRTC (classic demo) and VRW (wallet demo). These two additions need explicit verification against Deriv's official account system before shipping.

Fix this →


2. wantsDemo fallback inconsistent with isDemoAccountsrc/app/AuthWrapper.tsx:84

const wantsDemo =
    accountParam.toLowerCase() === 'demo' || defaultActiveAccount?.loginid?.startsWith('VR');

The fallback check only uses .startsWith('VR'), but isDemoAccount also checks DEM and DOT. If those prefixes are valid, this check is incomplete. If they aren't valid (see issue #1), then isDemoAccount should be trimmed to match. Either way, these two detection paths should be consistent — use isDemoAccount(defaultActiveAccount?.loginid ?? '') here.

Fix this →


Medium Priority

3. isDemoAccount placed in the wrong modulesrc/analytics/utils.ts

This function is purely about account identity/auth logic, not analytics. It is imported by AuthWrapper.tsx and callback-page.tsx, neither of which do analytics. Placing it in src/analytics/utils.ts creates surprising coupling — a future dev looking for auth helpers would not look there. It belongs in src/utils/auth-utils.ts.


4. No unit tests for isDemoAccount

The function has branching logic and edge cases (empty string, falsy values, overlapping prefixes like VRT vs VRTC). It is used in two critical auth flows. It should have dedicated tests, e.g.:

it('identifies VRTC as demo', () => expect(isDemoAccount('VRTC1234567')).toBe(true));
it('identifies VRW as demo',  () => expect(isDemoAccount('VRW1000001')).toBe(true));
it('identifies real account as non-demo', () => expect(isDemoAccount('CR1234567')).toBe(false));
it('handles empty string', () => expect(isDemoAccount('')).toBe(false));

5. wantsDemo=true with no matching demo account silently falls to wrong accountsrc/app/AuthWrapper.tsx:95-97

// Fallback to first account from authorize response
if (!targetAccount) {
    const firstId = authorize?.account_list[0]?.loginid;
    targetAccount = loginInfo.find(account => account.loginid === firstId);
}

If the user explicitly requested a demo account but none exists in loginInfo, the fallback silently picks a real account. The user is left with an unexpected active account. Consider logging a warning or explicitly not applying the fallback when wantsDemo was the intent.


Low Priority

6. window.location.search read inside setLocalStorageTokensrc/app/AuthWrapper.tsx:80

Reading URL params inside a utility function is a hidden side effect. The urlParams value would be cleaner passed as a parameter or read at the call site (the useEffect in AuthWrapper) and forwarded down.


7. selected_currency moved before API call — good fix, but worth a note

The move from after the API block to before it (callback-page.tsx:65) is the correct fix for ensuring the redirect URL is always populated. The clientAccounts object is fully populated (with currencies from cur* keys) before this line, so getSelectedCurrency will receive complete data. This change is correct.


Pre-existing issue (not introduced by this PR, but worth flagging)

useTMB() called inside async callbacksrc/pages/callback/callback-page.tsx:78

const { is_tmb_enabled = false } = useTMB();

This call is inside onSignInSuccess, an async callback — not at the top level of a React component. This violates Rules of Hooks and will produce incorrect behavior in strict mode. Not introduced by this PR but should be fixed separately.


Quick-reference checklist

Priority File Issue
🔴 High analytics/utils.ts:56-57 DEM/DOT prefixes unverified — may classify real accounts as demo
🔴 High AuthWrapper.tsx:84 wantsDemo uses .startsWith('VR') instead of isDemoAccount
🟡 Medium analytics/utils.ts Function belongs in auth-utils.ts, not analytics
🟡 Medium No unit tests for isDemoAccount
🟡 Medium AuthWrapper.tsx:95-97 Silent fallback to real account when demo was requested
🟢 Low AuthWrapper.tsx:80 Side-effectful URL read inside utility function

@github-actions

github-actions Bot commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Claude finished @akmal-deriv's task in 1m 45s —— View job


PR Review: fix: enhance demo account handling in AuthWrapper and CallbackPage

Tasks:

  • Gather context
  • Read changed files
  • Analyze auth flow changes in AuthWrapper.tsx
  • Analyze callback page changes in callback-page.tsx
  • Analyze analytics utils changes
  • Write review summary

Summary

This PR introduces isDemoAccount as a shared utility to avoid duplicated demo-detection logic across AuthWrapper.tsx and callback-page.tsx, and moves selected_currency computation before the API call in callback-page.tsx. The intent is correct and the selected_currency move is a genuine fix, but the implementation has consistency gaps that undermine the utility it introduces.


High Priority

1. DEM/DOT prefixes have no precedent in the codebasesrc/analytics/utils.ts:56-57

loginid.startsWith('DEM') ||
loginid.startsWith('DOT')

Searching the entire codebase, every other demo-account check uses .startsWith('VR'):

  • src/app/App.tsx:100key.startsWith('VR') to find demo accounts
  • src/app/App.tsx:113!loginid.startsWith('VR') to identify real accounts

Neither DEM nor DOT appear anywhere else as demo account prefixes. DOT is the Polkadot cryptocurrency ticker and a valid currency code that could feasibly be assigned to a real account. These two entries need explicit verification against Deriv's account system before shipping — or should be removed until confirmed.

Fix this →


2. Three separate demo-detection paths remain after this PRcallback-page.tsx:29, AuthWrapper.tsx:84, analytics/utils.ts:54-58

The whole point of introducing isDemoAccount was to consolidate detection. But getSelectedCurrency and the wantsDemo fallback were not updated:

// callback-page.tsx:29 — still uses startsWith('VR'), not isDemoAccount
if (tokens.acct1?.startsWith('VR') || currency === 'demo') return 'demo';

// AuthWrapper.tsx:84 — falls back to startsWith('VR'), not isDemoAccount
const wantsDemo =
    accountParam.toLowerCase() === 'demo' || defaultActiveAccount?.loginid?.startsWith('VR');

If DEM/DOT are valid (issue #1), these paths miss them. If DEM/DOT are not valid, then isDemoAccount should be trimmed to just VR. Either way, these three paths are currently inconsistent with each other.

Fix this →


Medium Priority

3. useTMB() called inside an async callbacksrc/pages/callback/callback-page.tsx:78

const { is_tmb_enabled = false } = useTMB();

This is called inside onSignInSuccess, an async function — not at the top level of a React component. This violates the Rules of Hooks and will produce incorrect behavior in strict mode. This is pre-existing but worth fixing separately.


4. isDemoAccount placed in the wrong modulesrc/analytics/utils.ts

The function is about account identity, not analytics. It is already imported by AuthWrapper.tsx and callback-page.tsx, neither of which does analytics work. It belongs in src/utils/auth-utils.ts alongside clearAuthData. The jest config already maps @/utils, so no config changes would be needed — and the @/analytics mapping just added to jest.config.ts would become unnecessary.


5. No unit tests for isDemoAccount

The function has branching logic (multiple prefixes), is used in two critical auth flows, and has an edge case for empty string. Tests should cover at minimum:

it('identifies VRTC as demo',   () => expect(isDemoAccount('VRTC1234567')).toBe(true));
it('identifies VRW as demo',    () => expect(isDemoAccount('VRW1000001')).toBe(true));
it('identifies CR as non-demo', () => expect(isDemoAccount('CR1234567')).toBe(false));
it('handles empty string',      () => expect(isDemoAccount('')).toBe(false));

6. Silent fallback to real account when demo was explicitly requestedsrc/app/AuthWrapper.tsx:95-97

if (!targetAccount) {
    const firstId = authorize?.account_list[0]?.loginid;
    targetAccount = loginInfo.find(account => account.loginid === firstId);
}

If the user had account=demo in the URL but no demo account exists in loginInfo, this silently activates a real account. The user is left trading real money when they intended demo. At minimum, add a console.warn to surface this during debugging.


Low Priority

7. selected_currency move before API call is correctsrc/pages/callback/callback-page.tsx:65

Moving getSelectedCurrency before the api.authorize call ensures the redirect URL is always populated even when the API call fails. This is the correct fix.


Quick-reference checklist

Priority File Issue
🔴 High analytics/utils.ts:56-57 DEM/DOT prefixes unverified — absent from rest of codebase
🔴 High callback-page.tsx:29, AuthWrapper.tsx:84 Still use .startsWith('VR') instead of isDemoAccount
🟡 Medium callback-page.tsx:78 useTMB() called inside async callback (hooks violation)
🟡 Medium analytics/utils.ts isDemoAccount belongs in src/utils/auth-utils.ts
🟡 Medium No unit tests for isDemoAccount
🟡 Medium AuthWrapper.tsx:95-97 Silent fallback to real account when demo was requested
🟢 Low callback-page.tsx:65 selected_currency move is a correct fix ✓

@github-actions

github-actions Bot commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Claude finished @akmal-deriv's task in 2m 6s —— View job


PR Review: fix: enhance demo account handling in AuthWrapper and CallbackPage

Tasks:

  • Gather context
  • Read changed files
  • Analyze auth flow changes in AuthWrapper.tsx
  • Analyze callback page changes in callback-page.tsx
  • Analyze analytics utils changes
  • Write review summary

Summary

This PR introduces isDemoAccount as a shared utility and moves selected_currency computation before the API call in callback-page.tsx. The selected_currency move is a genuine correctness fix. However, the core goal — consolidating demo-account detection into a single source of truth — is incomplete, and the new DEM/DOT prefixes are unverified and potentially dangerous.


High Priority

1. DEM/DOT prefixes have no codebase precedent and are unverifiedsrc/analytics/utils.ts:56-57

loginid.startsWith('DEM') ||
loginid.startsWith('DOT')

A search across the entire codebase finds zero other references to DEM or DOT as demo account prefixes. Every other demo check in the app uses .startsWith('VR'):

  • App.tsx:100key.startsWith('VR') to find demo accounts
  • App.tsx:113!loginid.startsWith('VR') to identify real accounts
  • account-switcher-footer.tsx:31loginid?.startsWith('VRTC')
  • callback-page.tsx:29tokens.acct1?.startsWith('VR')

DOT is also the Polkadot cryptocurrency ticker and a feasible real account prefix. If Deriv ever issues real accounts with these prefixes, they would be silently classified as demo — blocking users from trading real funds without any visible error.

These entries need explicit confirmation against Deriv's account system documentation, or should be removed until confirmed.

Fix this →


2. isDemoAccount is not actually used consistently — two call sites still use startsWith('VR')callback-page.tsx:29, AuthWrapper.tsx:84

The stated purpose of isDemoAccount is to consolidate demo detection. But neither of the two pre-existing checks was updated:

// callback-page.tsx:29 — unchanged, still uses startsWith('VR')
if (tokens.acct1?.startsWith('VR') || currency === 'demo') return 'demo';

// AuthWrapper.tsx:84 — fallback still uses startsWith('VR'), not isDemoAccount
const wantsDemo =
    accountParam.toLowerCase() === 'demo' || defaultActiveAccount?.loginid?.startsWith('VR');

If DEM/DOT are valid, these two paths will miss accounts with those prefixes (a VRW wallet demo account would also be missed in the startsWith('VR') check in callback-page.tsx). If they aren't valid (see issue #1), isDemoAccount should be trimmed to stay consistent. Either way the three detection paths now diverge from each other.

Fix this →


Medium Priority

3. isDemoAccount is placed in the wrong modulesrc/analytics/utils.ts

This function is about account identity, not analytics. It is already imported by AuthWrapper.tsx and callback-page.tsx, neither of which performs analytics. Placing it here creates surprising coupling — a developer looking for auth helpers would not look in src/analytics/.

It belongs alongside clearAuthData in src/utils/auth-utils.ts. The jest config already maps @/utils, so no jest config changes would be needed — and the new @/analytics mapping added to jest.config.ts in this PR becomes unnecessary.


4. No unit tests for isDemoAccount

The function has branching logic (4 prefix checks), is called from two critical auth flows, and handles an edge case (empty string → false). Minimum test coverage:

it('identifies VRTC as demo',   () => expect(isDemoAccount('VRTC1234567')).toBe(true));
it('identifies VRW as demo',    () => expect(isDemoAccount('VRW1000001')).toBe(true));
it('identifies CR as non-demo', () => expect(isDemoAccount('CR1234567')).toBe(false));
it('handles empty string',      () => expect(isDemoAccount('')).toBe(false));

5. Silent fallback to real account when demo was explicitly requestedsrc/app/AuthWrapper.tsx:95-97

if (!targetAccount) {
    const firstId = authorize?.account_list[0]?.loginid;
    targetAccount = loginInfo.find(account => account.loginid === firstId);
}

If wantsDemo is true but no demo account exists in loginInfo, this fallback silently activates a real account. The user intended to be on demo but is now trading real funds. At minimum, add a console.warn so this is surfaced during debugging. The same pattern exists in callback-page.tsx:103-106.


6. Pre-existing: useTMB() called inside an async callbacksrc/pages/callback/callback-page.tsx:78

const { is_tmb_enabled = false } = useTMB();

This is called inside onSignInSuccess, an async function — not at the top level of a React component. This violates the Rules of Hooks and will silently produce stale/incorrect values in strict mode. Not introduced by this PR, but should be tracked as a separate fix.


Low Priority

7. window.location.search read inside setLocalStorageTokensrc/app/AuthWrapper.tsx:80

Reading URL params inside a utility function called deep in the auth flow is a hidden side effect. By the time this line runs, URLUtils.filterSearchParams(paramsToDelete) has already run (line 42), which modifies the URL. While account is not in paramsToDelete, this fragile ordering becomes a maintenance hazard. The account param value would be cleaner read at the useEffect call site and passed as an argument.


8. selected_currency move before API call — correct fix ✓src/pages/callback/callback-page.tsx:65

Moving getSelectedCurrency before api.authorize() ensures the redirect URL is always populated, even if the API call fails. The cur* keys are fully populated in clientAccounts before this line, so getSelectedCurrency will receive complete data. This change is correct.


Quick-reference checklist

Priority File Issue
🔴 High analytics/utils.ts:56-57 DEM/DOT prefixes unverified — absent from rest of codebase, DOT conflicts with crypto ticker
🔴 High callback-page.tsx:29, AuthWrapper.tsx:84 Still use .startsWith('VR')isDemoAccount not applied consistently
🟡 Medium analytics/utils.ts isDemoAccount belongs in src/utils/auth-utils.ts, not analytics
🟡 Medium No unit tests for isDemoAccount
🟡 Medium AuthWrapper.tsx:95-97, callback-page.tsx:103-106 Silent fallback to real account when demo was explicitly requested
🟡 Medium callback-page.tsx:78 useTMB() called inside async callback — hooks violation (pre-existing)
🟢 Low AuthWrapper.tsx:80 window.location.search read inside utility function — hidden side effect
✅ Good callback-page.tsx:65 selected_currency moved before API call — correct fix

@github-actions

github-actions Bot commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Claude finished @akmal-deriv's task in 1m 50s —— View job


PR Review: fix: enhance demo account handling in AuthWrapper and CallbackPage

Tasks:

  • Gather context
  • Read changed files (AuthWrapper.tsx, callback-page.tsx, auth-utils.ts, auth-utils.spec.ts)
  • Analyze changes vs previous review feedback
  • Write review summary

Summary

This iteration resolves all of the high-priority findings from previous reviews. isDemoAccount is now correctly located in src/utils/auth-utils.ts, the DEM/DOT prefixes have been dropped (only VRTC/VRW remain), unit tests are in place, and all three demo-detection call sites now use the shared utility consistently. One pre-existing hooks violation remains and is the only item that should block merge.


Must Fix (Blocks Merge)

useTMB() called inside an async callbacksrc/pages/callback/callback-page.tsx:77

// Inside onSignInSuccess, an async function passed as a prop — NOT a component
const { is_tmb_enabled = false } = useTMB();

This violates the Rules of Hooks. Hooks must be called at the top level of a React component or custom hook. Calling one inside an async callback silently produces stale/incorrect values in React strict mode, and will generate warnings in dev. The fix is to hoist the hook to the CallbackPage component level and close over the value:

const CallbackPage = () => {
    const { is_tmb_enabled = false } = useTMB();   // ← move here

    return (
        <Callback
            onSignInSuccess={async (tokens, rawState) => {
                // ... use is_tmb_enabled from closure ...
            }}
        />
    );
};

This is pre-existing code, but callback-page.tsx is being actively modified in this PR, making it the right place to fix it.

Fix this →


Good to Have

Missing test: null / non-string inputsrc/utils/__tests__/auth-utils.spec.ts

The five existing tests are exactly the ones recommended in previous reviews — good. One additional case worth adding: undefined input (callers in callback-page.tsx use tokens.acct1 ?? '' to guard, but future callers might not):

it('handles null-ish via the guard', () => {
    // TypeScript enforces string, but guard handles falsy at runtime
    expect(isDemoAccount('')).toBe(false);  // already tested
});

Not a blocker, just a suggestion.


Pre-existing issue, not introduced here (track separately)

authorize.toString() stores [object Object]callback-page.tsx:88

localStorage.setItem('callback_token', authorize.toString());

authorize is the API response object. .toString() on a plain object returns [object Object]. Whatever reads callback_token from localStorage will get that string — worth fixing in a separate PR.


What was addressed from previous reviews ✅

Previous finding Status
DEM/DOT unverified prefixes in isDemoAccount ✅ Removed — only VRTC/VRW remain
isDemoAccount in wrong module (analytics/utils.ts) ✅ Moved to src/utils/auth-utils.ts
No unit tests for isDemoAccount ✅ 5 tests added in auth-utils.spec.ts
wantsDemo using .startsWith('VR') instead of isDemoAccount ✅ Fixed — AuthWrapper.tsx:83
callback-page.tsx:29 using .startsWith('VR') instead of isDemoAccount ✅ Fixed — callback-page.tsx:28
selected_currency computed after API call ✅ Fixed — moved to line 64
Silent fallback to real account when demo requested console.warn added in both files

Quick-reference checklist

Priority File Issue
🔴 Must fix callback-page.tsx:77 useTMB() called inside async callback — hooks violation
🟡 Nice to have auth-utils.spec.ts Consider adding a test for null/falsy-coerced inputs beyond empty string
🟢 Pre-existing callback-page.tsx:88 authorize.toString() yields [object Object] in localStorage

@github-actions

Copy link
Copy Markdown
Contributor

Preview Link: https://fix-reset-to-demo.bot-65f.pages.dev

Name Result
Build status Completed ✅
Preview URL Visit Preview
Action URL Visit Action

@nijil-deriv
nijil-deriv merged commit e24d1fd into master Apr 16, 2026
8 of 9 checks passed
@nijil-deriv
nijil-deriv deleted the fix-reset-to-demo branch April 16, 2026 07:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants