feat: finish mobile region coverage and harden extension sync#81
feat: finish mobile region coverage and harden extension sync#81dhairyashiil wants to merge 1 commit intodevin/1776355396-eu-region-supportfrom
Conversation
- 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 EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
Deployment failed with the following error: View Documentation: https://vercel.com/docs/accounts/team-members-and-roles |
There was a problem hiding this comment.
🔴 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).
Was this helpful? React with 👍 or 👎 to provide feedback.
| return null; | ||
| } | ||
| }); | ||
| const [oauthService, setOauthService] = useState<CalComOAuthService | null>(null); |
There was a problem hiding this comment.
🟡 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
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 callsgetCalAppUrl()at call time so icon URLs follow the current region.apps/mobile/utils/locationHelpers.ts: all hardcodedhttps://app.cal.com/*.svgicon URLs now built fromgetCalAppUrl();buildLocationOptionsconsumesgetDefaultLocations().apps/mobile/utils/getAppIconUrl.ts: app-store icon URL prefixed withgetCalAppUrl().apps/mobile/utils/getAvatarUrl.ts: hardcodedCAL_URL = "https://cal.com"replaced with per-callgetCalWebUrl().apps/mobile/utils/index.ts: re-export updated accordingly.apps/mobile/app/(tabs)/(more)/index.ios.tsx: Delete Account URL routed throughgetCalAppUrl().apps/mobile/components/event-type-detail/tabs/BasicsTab.tsx: booking-URL prefix fallback derives the hostname fromgetCalWebUrl()instead of hard-codingcal.com/.§A Clear
cal_regionon mobile logoutclearRegion()helper inapps/mobile/utils/region.ts(resets in-memory cache + removes the persisted key fromgeneralStorageand thelocalStoragemirror).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 existingstorageAPI.local.remove([..., "cal_region"])on logout.§B Drop
oauthServicedouble-initAuthContextno longer constructsCalComOAuthServiceinside theuseStateinitializer and again in the effect afterpreloadRegion(). Initial state isnull; the effect builds it once the region has been preloaded and rebuilds on region changes.§C Validate (and persist)
regionon write in extensionsync-oauth-tokensmessage.region, rejects anything that isn't"us" | "eu"with anInvalid regionerror, and persists it alongside the tokens inchrome.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/*URLsAdvancedTab.tsx/RecurringTab.tsxalone and added a short JSDocTODO(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)
content.tsregion-awareness (13+ call sites; needs a region reader in the content-script context).Review & Testing Checklist for Human
api.cal.eu/v2/*, avatar + app-store icons served fromapp.cal.eu, booking-URL prefix in BasicsTab showscal.eu/<user>/whenbookingUrlis absent.app.cal.eu/settings/my-account/profileopens in the in-app browser.cal_regionis 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 onapi.cal.com.chrome.storage.localhascal_region: "eu"andcal_oauth_tokenswritten together; send async-oauth-tokensmessage withregion: "xx"(via devtools) and confirm the background rejects it withInvalid region.Notes
AdvancedTab.tsx/RecurringTab.tsxstill hitcal.com/help/*. TODO comments point this out; intentional until the team confirms whether help docs get an EU mirror.EventTypeListItemParts.tsxalready derives its display string fromnew URL(bookingUrl).hostname, so it's region-correct as long as the server returns a region-correctbookingUrl. No code change needed there.clearRegion()and thesync-oauth-tokensregion validation if reviewers want it.Link to Devin session: https://app.devin.ai/sessions/ccbfee87cc954cba9a475b12845c6b5d
Requested by: @dhairyashiil