Skip to content

feat: finish mobile region coverage and harden extension sync#81

Draft
dhairyashiil wants to merge 1 commit intodevin/1776355396-eu-region-supportfrom
devin/1776757080-eu-region-followup
Draft

feat: finish mobile region coverage and harden extension sync#81
dhairyashiil wants to merge 1 commit intodevin/1776355396-eu-region-supportfrom
devin/1776757080-eu-region-followup

Conversation

@dhairyashiil
Copy link
Copy Markdown
Member

Summary

Stacked on top of #80. Finishes the mechanical EU region coverage on mobile and hardens the extension token-sync path.

§1 Mobile hardcoded URLs → region-aware helpers

  • apps/mobile/utils/defaultLocations.ts: exported constant → getDefaultLocations() getter that calls getCalAppUrl() at call time so icon URLs follow the current region.
  • apps/mobile/utils/locationHelpers.ts: all hardcoded https://app.cal.com/*.svg icon URLs now built from getCalAppUrl(); buildLocationOptions consumes getDefaultLocations().
  • apps/mobile/utils/getAppIconUrl.ts: app-store icon URL prefixed with getCalAppUrl().
  • apps/mobile/utils/getAvatarUrl.ts: hardcoded CAL_URL = "https://cal.com" replaced with per-call getCalWebUrl().
  • apps/mobile/utils/index.ts: re-export updated accordingly.
  • apps/mobile/app/(tabs)/(more)/index.ios.tsx: Delete Account URL routed through getCalAppUrl().
  • apps/mobile/components/event-type-detail/tabs/BasicsTab.tsx: booking-URL prefix fallback derives the hostname from getCalWebUrl() instead of hard-coding cal.com/.

§A Clear cal_region on mobile logout

  • New clearRegion() helper in apps/mobile/utils/region.ts (resets in-memory cache + removes the persisted key from generalStorage and the localStorage mirror).
  • AuthContext.logout() now calls it so the next user is prompted via the login-screen picker rather than silently inheriting the previous region, mirroring the extension's existing storageAPI.local.remove([..., "cal_region"]) on logout.

§B Drop oauthService double-init

  • AuthContext no longer constructs CalComOAuthService inside the useState initializer and again in the effect after preloadRegion(). Initial state is null; the effect builds it once the region has been preloaded and rebuilds on region changes.

§C Validate (and persist) region on write in extension sync-oauth-tokens

  • Background handler now reads message.region, rejects anything that isn't "us" | "eu" with an Invalid region error, and persists it alongside the tokens in chrome.storage.local. Previously the iframe-supplied region was forwarded but never written; the extension could only read a region that had been set out of band.

TODO on cal.com/help/* URLs

  • Left the help-doc URLs in AdvancedTab.tsx / RecurringTab.tsx alone and added a short JSDoc TODO(eu-region) at the top of each file explaining the decision is pending an EU help-mirror call.

Out of scope (separate follow-up PRs)

  • §2 extension content.ts region-awareness (13+ call sites; needs a region reader in the content-script context).
  • §3 force logout + query-cache clear on region change while authenticated.
  • §F rollout kill switch.
  • §E in-memory region cache in the extension background (only useful once §2 lands).

Review & Testing Checklist for Human

  • Set mobile region to EU on the login screen, sign in, inspect network: api.cal.eu/v2/*, avatar + app-store icons served from app.cal.eu, booking-URL prefix in BasicsTab shows cal.eu/<user>/ when bookingUrl is absent.
  • In the more tab, tap "Delete Account" while in EU region → app.cal.eu/settings/my-account/profile opens in the in-app browser.
  • Log out from EU, confirm cal_region is gone (e.g. fresh login screen defaults back to US), then log in to a US account without touching the picker and confirm API traffic is on api.cal.com.
  • In the extension, complete an OAuth flow from a Cal.eu origin and confirm chrome.storage.local has cal_region: "eu" and cal_oauth_tokens written together; send a sync-oauth-tokens message with region: "xx" (via devtools) and confirm the background rejects it with Invalid region.
  • Smoke-test a US-region login end-to-end (OAuth + a booking list fetch) to confirm nothing regressed.

Notes

  • AdvancedTab.tsx / RecurringTab.tsx still hit cal.com/help/*. TODO comments point this out; intentional until the team confirms whether help docs get an EU mirror.
  • EventTypeListItemParts.tsx already derives its display string from new URL(bookingUrl).hostname, so it's region-correct as long as the server returns a region-correct bookingUrl. No code change needed there.
  • No new tests added — all changes are mechanical URL rewrites plus small refactors on existing call paths. Happy to add unit coverage for clearRegion() and the sync-oauth-tokens region validation if reviewers want it.

Link to Devin session: https://app.devin.ai/sessions/ccbfee87cc954cba9a475b12845c6b5d
Requested by: @dhairyashiil

- Make mobile asset/icon + external link URLs region-aware via getCalAppUrl/getCalWebUrl
- Clear cal_region on mobile logout to mirror extension behavior
- Drop oauthService double-init in AuthContext
- Validate and persist region on write in extension sync-oauth-tokens handler
- TODO note on cal.com/help/* URLs pending EU mirror decision
@devin-ai-integration
Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 21, 2026

Deployment failed with the following error:

You don't have permission to create a Preview Deployment for this Vercel project: cal-companion-mcp.

View Documentation: https://vercel.com/docs/accounts/team-members-and-roles

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

View 5 additional findings in Devin Review.

Open in Devin Review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Extension validates tokens against wrong region's API when region is changing

When sync-oauth-tokens receives a new region (e.g., "eu"), validateTokens() at line 429 calls getApiBaseUrl()getStoredRegion() which reads the old region from chrome.storage.local (since the new region hasn't been stored yet — that happens at lines 439-444, inside the .then() after validation succeeds). This means EU tokens are validated against https://api.cal.com/v2/me (and vice-versa), which will fail because the tokens are scoped to the other region's API. The validation returns false, and the token sync is silently rejected.

This breaks the extension integration for any user who selects a region different from the one previously stored (or different from the default "us" on first use). The mobile app login itself succeeds, but the extension never receives the tokens.

(Refers to lines 429-435)

Prompt for agents
In apps/extension/entrypoints/background/index.ts, the sync-oauth-tokens handler validates tokens using validateTokens() which internally calls getApiBaseUrl() -> getStoredRegion(). This reads the region from chrome.storage.local, but at this point the new region from the message hasnt been stored yet (it gets stored after validation in the .then() block at lines 439-444).

The fix is to make validateTokens region-aware. Either:
1. Pass the incoming region to validateTokens and have it use that region to determine the API base URL instead of reading from storage: change the signature to validateTokens(tokens, region) and compute the API URL directly from the region parameter.
2. Or store the region before validation (but this is less clean since invalid tokens would still update the region).

Option 1 is cleaner. Update validateTokens to accept an optional region parameter, and in the sync-oauth-tokens handler, pass the region from the message (falling back to the stored region if not provided).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

return null;
}
});
const [oauthService, setOauthService] = useState<CalComOAuthService | null>(null);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 OAuth users see flash of unauthenticated state due to async oauthService initialization

The oauthService state was changed from being synchronously initialized via useState(() => createCalComOAuthService()) to starting as null and being set only after preloadRegion() resolves asynchronously (line 66 and lines 84-86). Since checkAuthState at apps/mobile/contexts/AuthContext.tsx:256 checks oauthService and skips OAuth auth when it's null, the first invocation of checkAuthState (triggered by the effect at line 275) will skip OAuth authentication and set loading=false (line 261). This briefly exposes {loading: false, isAuthenticated: false} state to the UI before oauthService is set and checkAuthState re-runs, potentially flashing the login screen for returning OAuth users.

Prompt for agents
In apps/mobile/contexts/AuthContext.tsx, oauthService was changed from synchronous initialization to null + async initialization. This causes checkAuthState to run once with oauthService=null, set loading=false prematurely, and then run again when oauthService is set.

Possible fixes:
1. Guard setLoading(false) inside checkAuthState: only set loading=false when oauthService is available OR when the auth type is not oauth. This way loading stays true until the OAuth service is ready and auth is properly checked.
2. Alternatively, keep the synchronous initialization as a fallback (initialize with the default region) and update it when preloadRegion completes. This was essentially the old behavior. The synchronous init would use the default 'us' region, but that's fine because the service gets rebuilt when the actual region loads.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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.

1 participant