From 7fef15042de78fe987668a51fec32ce38e1571ac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 13:34:09 +0000 Subject: [PATCH 1/6] Initial plan From e4cfa5c70e3e58030fbec62c66661e4d46df1d8a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 13:42:21 +0000 Subject: [PATCH 2/6] Add polling mechanism for new organization stripe_info setup Co-authored-by: riderx <4084527+riderx@users.noreply.github.com> --- messages/en.json | 3 ++ src/pages/dashboard.vue | 32 +++++++++++++-- src/stores/organization.ts | 81 +++++++++++++++++++++++++++++++++++++- 3 files changed, 111 insertions(+), 5 deletions(-) diff --git a/messages/en.json b/messages/en.json index 862f298db8..70bd83c849 100644 --- a/messages/en.json +++ b/messages/en.json @@ -776,6 +776,7 @@ "incorrect-app-id": "You wrote the wrong app id.", "info": "Information", "init-capgo-in-your-a": "Install Capgo in your Capacitor app", + "initializing-trial": "We're setting up your free trial", "onboarding-prerequisites-title": "Prerequisites", "onboarding-prerequisites-cli-desc": "This command will configure your Capacitor app to receive live updates from Capgo.", "onboarding-prerequisites-runtime": "Node.js 22+ or Bun installed", @@ -1235,6 +1236,7 @@ "set-bundle": "Set bundle to channel", "set-even-not-compatible": "This bundle is not compatible with the channel. Would you like to set it anyway, check why with (%) ?", "set-expiration-date": "Set expiration date", + "setting-up-account": "Setting up your account", "settings": "Settings", "showing": "Showing", "sign-out": "Sign Out", @@ -1263,6 +1265,7 @@ "thank-you-for-choosi": "Thank you for choosing Capgo !", "thank-you-for-sub": "Thank You for subscribing to Capgo", "this-page-will-self-": "This page will self refresh after you finish onboarding with the CLI", + "this-takes-few-seconds": "This usually takes just a few seconds", "to": "to", "to-open-encrypted-bu": "To open Encrypted bundle use the command:", "today": "today", diff --git a/src/pages/dashboard.vue b/src/pages/dashboard.vue index eb9f1d463b..e2b8ac3554 100644 --- a/src/pages/dashboard.vue +++ b/src/pages/dashboard.vue @@ -16,7 +16,7 @@ const { t } = useI18n() const displayStore = useDisplayStore() const apps = ref([]) -const { currentOrganization } = storeToRefs(organizationStore) +const { currentOrganization, isNewOrganizationLoading } = storeToRefs(organizationStore) // Check if user lacks security compliance (2FA or password) - don't load data in this case const lacksSecurityAccess = computed(() => { @@ -32,15 +32,16 @@ const hasNoApps = computed(() => { && !isLoading.value && !organizationStore.currentOrganizationFailed && !lacksSecurityAccess.value + && !isNewOrganizationLoading.value }) -// Payment failed state (subscription required) +// Payment failed state (subscription required) - but not if we're waiting for new org setup const paymentFailed = computed(() => { - return organizationStore.currentOrganizationFailed && !lacksSecurityAccess.value + return organizationStore.currentOrganizationFailed && !lacksSecurityAccess.value && !isNewOrganizationLoading.value }) // Should blur the content (either no apps OR payment failed) -const shouldBlurContent = computed(() => hasNoApps.value || paymentFailed.value) +const shouldBlurContent = computed(() => hasNoApps.value || paymentFailed.value || isNewOrganizationLoading.value) async function getMyApps() { await organizationStore.awaitInitialLoad() @@ -95,6 +96,29 @@ displayStore.defaultBack = '/app' + +
+
+
+
+ +
+
+

+ {{ t('setting-up-account') }} +

+

+ {{ t('initializing-trial') }} +

+

+ {{ t('this-takes-few-seconds') }} +

+
+
+
{ const currentOrganization = ref(undefined) const currentOrganizationFailed = ref(false) const currentRole = ref(null) + const isNewOrganizationLoading = ref(false) const STORAGE_KEY = 'capgo_current_org_id' + const NEW_ORG_POLL_KEY = 'capgo_new_org_poll' + + // Helper function to check if org appears to be newly created and waiting for stripe_info + const isWaitingForStripeInfo = (org: Organization): boolean => { + return !org.paying && (org.trial_left ?? 0) === 0 + } + + // Poll for stripe_info creation if org appears newly created + let pollTimeout: ReturnType | null = null + const pollForStripeInfo = async (orgId: string) => { + if (pollTimeout) + clearTimeout(pollTimeout) + + // Check if we've been polling for this org + const pollData = localStorage.getItem(NEW_ORG_POLL_KEY) + let pollStart = Date.now() + if (pollData) { + try { + const parsed = JSON.parse(pollData) + if (parsed.orgId === orgId) { + pollStart = parsed.start + // If we've been polling for more than 2 minutes, give up + if (Date.now() - pollStart > 120000) { + localStorage.removeItem(NEW_ORG_POLL_KEY) + isNewOrganizationLoading.value = false + return + } + } + else { + // Different org, reset + pollStart = Date.now() + } + } + catch { + pollStart = Date.now() + } + } + + localStorage.setItem(NEW_ORG_POLL_KEY, JSON.stringify({ orgId, start: pollStart })) + isNewOrganizationLoading.value = true + + // Poll every 3 seconds + pollTimeout = setTimeout(async () => { + await fetchOrganizations() + const org = _organizations.value.get(orgId) + if (org && isWaitingForStripeInfo(org)) { + // Still waiting, poll again + await pollForStripeInfo(orgId) + } + else { + // Stripe info created or org changed + localStorage.removeItem(NEW_ORG_POLL_KEY) + isNewOrganizationLoading.value = false + } + }, 3000) + } watch(currentOrganization, async (currentOrganizationRaw, oldOrganization) => { if (!currentOrganizationRaw) { currentRole.value = null localStorage.removeItem(STORAGE_KEY) + if (pollTimeout) { + clearTimeout(pollTimeout) + pollTimeout = null + } + localStorage.removeItem(NEW_ORG_POLL_KEY) + isNewOrganizationLoading.value = false return } @@ -90,7 +153,22 @@ export const useOrganizationStore = defineStore('organization', () => { currentOrganizationFailed.value = false } else { - currentOrganizationFailed.value = !(!!currentOrganizationRaw.paying || (currentOrganizationRaw.trial_left ?? 0) > 0) + const orgIsWaiting = isWaitingForStripeInfo(currentOrganizationRaw) + currentOrganizationFailed.value = orgIsWaiting ? false : !(!!currentOrganizationRaw.paying || (currentOrganizationRaw.trial_left ?? 0) > 0) + + // If org appears newly created (no trial, not paying), start polling + if (orgIsWaiting && !isNewOrganizationLoading.value) { + await pollForStripeInfo(currentOrganizationRaw.gid) + } + else if (!orgIsWaiting && isNewOrganizationLoading.value) { + // Stripe info was created, stop polling + if (pollTimeout) { + clearTimeout(pollTimeout) + pollTimeout = null + } + localStorage.removeItem(NEW_ORG_POLL_KEY) + isNewOrganizationLoading.value = false + } } // Clear caches when org changes to prevent showing stale data from other orgs @@ -374,6 +452,7 @@ export const useOrganizationStore = defineStore('organization', () => { currentOrganization, currentOrganizationFailed, currentRole, + isNewOrganizationLoading, setCurrentOrganization, setCurrentOrganizationToMain, setCurrentOrganizationToFirst, From 9686ef4513642a7b2778c1eaa421f92b7e0c893d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 13:43:55 +0000 Subject: [PATCH 3/6] Improve new org detection with app_count check and hide banner during setup Co-authored-by: riderx <4084527+riderx@users.noreply.github.com> --- src/components/Banner.vue | 4 ++++ src/stores/organization.ts | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/Banner.vue b/src/components/Banner.vue index d9a06e555e..bf2ea70752 100644 --- a/src/components/Banner.vue +++ b/src/components/Banner.vue @@ -71,6 +71,10 @@ const bannerText = computed(() => { if (lacksSecurityAccess.value) return null + // Don't show banner while we're waiting for new org setup + if (organizationStore.isNewOrganizationLoading) + return null + if (organizationStore.currentOrganizationFailed) return t('subscription-required') diff --git a/src/stores/organization.ts b/src/stores/organization.ts index cdf8c03dba..94cbe49f2f 100644 --- a/src/stores/organization.ts +++ b/src/stores/organization.ts @@ -77,8 +77,9 @@ export const useOrganizationStore = defineStore('organization', () => { const NEW_ORG_POLL_KEY = 'capgo_new_org_poll' // Helper function to check if org appears to be newly created and waiting for stripe_info + // Only treat as "waiting" if trial_left is 0, not paying, AND has 0 apps (likely brand new) const isWaitingForStripeInfo = (org: Organization): boolean => { - return !org.paying && (org.trial_left ?? 0) === 0 + return !org.paying && (org.trial_left ?? 0) === 0 && (org.app_count ?? 0) === 0 } // Poll for stripe_info creation if org appears newly created From 7fac45aad8a87c481b5683a389b7cb3bf22b07a7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 13:46:16 +0000 Subject: [PATCH 4/6] Address code review feedback: extract constants and use reactive ref for timeout Co-authored-by: riderx <4084527+riderx@users.noreply.github.com> --- src/stores/organization.ts | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/stores/organization.ts b/src/stores/organization.ts index 94cbe49f2f..a53de8123f 100644 --- a/src/stores/organization.ts +++ b/src/stores/organization.ts @@ -75,6 +75,8 @@ export const useOrganizationStore = defineStore('organization', () => { const STORAGE_KEY = 'capgo_current_org_id' const NEW_ORG_POLL_KEY = 'capgo_new_org_poll' + const POLL_INTERVAL_MS = 3000 // Poll every 3 seconds + const POLL_TIMEOUT_MS = 120000 // Give up after 2 minutes // Helper function to check if org appears to be newly created and waiting for stripe_info // Only treat as "waiting" if trial_left is 0, not paying, AND has 0 apps (likely brand new) @@ -82,11 +84,13 @@ export const useOrganizationStore = defineStore('organization', () => { return !org.paying && (org.trial_left ?? 0) === 0 && (org.app_count ?? 0) === 0 } + // Poll timeout ref to track the polling timer + const pollTimeout = ref | null>(null) + // Poll for stripe_info creation if org appears newly created - let pollTimeout: ReturnType | null = null const pollForStripeInfo = async (orgId: string) => { - if (pollTimeout) - clearTimeout(pollTimeout) + if (pollTimeout.value) + clearTimeout(pollTimeout.value) // Check if we've been polling for this org const pollData = localStorage.getItem(NEW_ORG_POLL_KEY) @@ -96,8 +100,8 @@ export const useOrganizationStore = defineStore('organization', () => { const parsed = JSON.parse(pollData) if (parsed.orgId === orgId) { pollStart = parsed.start - // If we've been polling for more than 2 minutes, give up - if (Date.now() - pollStart > 120000) { + // If we've been polling for more than the timeout, give up + if (Date.now() - pollStart > POLL_TIMEOUT_MS) { localStorage.removeItem(NEW_ORG_POLL_KEY) isNewOrganizationLoading.value = false return @@ -116,8 +120,8 @@ export const useOrganizationStore = defineStore('organization', () => { localStorage.setItem(NEW_ORG_POLL_KEY, JSON.stringify({ orgId, start: pollStart })) isNewOrganizationLoading.value = true - // Poll every 3 seconds - pollTimeout = setTimeout(async () => { + // Poll at configured interval + pollTimeout.value = setTimeout(async () => { await fetchOrganizations() const org = _organizations.value.get(orgId) if (org && isWaitingForStripeInfo(org)) { @@ -129,16 +133,16 @@ export const useOrganizationStore = defineStore('organization', () => { localStorage.removeItem(NEW_ORG_POLL_KEY) isNewOrganizationLoading.value = false } - }, 3000) + }, POLL_INTERVAL_MS) } watch(currentOrganization, async (currentOrganizationRaw, oldOrganization) => { if (!currentOrganizationRaw) { currentRole.value = null localStorage.removeItem(STORAGE_KEY) - if (pollTimeout) { - clearTimeout(pollTimeout) - pollTimeout = null + if (pollTimeout.value) { + clearTimeout(pollTimeout.value) + pollTimeout.value = null } localStorage.removeItem(NEW_ORG_POLL_KEY) isNewOrganizationLoading.value = false @@ -163,9 +167,9 @@ export const useOrganizationStore = defineStore('organization', () => { } else if (!orgIsWaiting && isNewOrganizationLoading.value) { // Stripe info was created, stop polling - if (pollTimeout) { - clearTimeout(pollTimeout) - pollTimeout = null + if (pollTimeout.value) { + clearTimeout(pollTimeout.value) + pollTimeout.value = null } localStorage.removeItem(NEW_ORG_POLL_KEY) isNewOrganizationLoading.value = false From 0f8f4f89116a804553fd9cc3d06fb0767d867cad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 13:48:01 +0000 Subject: [PATCH 5/6] Add localStorage validation and clarify polling pattern safety Co-authored-by: riderx <4084527+riderx@users.noreply.github.com> --- src/stores/organization.ts | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/stores/organization.ts b/src/stores/organization.ts index a53de8123f..3d9f38aed9 100644 --- a/src/stores/organization.ts +++ b/src/stores/organization.ts @@ -98,21 +98,29 @@ export const useOrganizationStore = defineStore('organization', () => { if (pollData) { try { const parsed = JSON.parse(pollData) - if (parsed.orgId === orgId) { - pollStart = parsed.start - // If we've been polling for more than the timeout, give up - if (Date.now() - pollStart > POLL_TIMEOUT_MS) { - localStorage.removeItem(NEW_ORG_POLL_KEY) - isNewOrganizationLoading.value = false - return + // Validate the parsed data structure + if (parsed && typeof parsed === 'object' && typeof parsed.orgId === 'string' && typeof parsed.start === 'number') { + if (parsed.orgId === orgId) { + pollStart = parsed.start + // If we've been polling for more than the timeout, give up + if (Date.now() - pollStart > POLL_TIMEOUT_MS) { + localStorage.removeItem(NEW_ORG_POLL_KEY) + isNewOrganizationLoading.value = false + return + } + } + else { + // Different org, reset + pollStart = Date.now() } } else { - // Different org, reset + // Invalid data structure, reset pollStart = Date.now() } } catch { + // Invalid JSON or other error, reset pollStart = Date.now() } } @@ -121,6 +129,8 @@ export const useOrganizationStore = defineStore('organization', () => { isNewOrganizationLoading.value = true // Poll at configured interval + // Note: Using recursive setTimeout is safer than setInterval for async operations + // as it ensures one poll completes before the next starts pollTimeout.value = setTimeout(async () => { await fetchOrganizations() const org = _organizations.value.get(orgId) From 325575e22ce3ff8d95399d8e250a1aa3da5d85d9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 13:49:17 +0000 Subject: [PATCH 6/6] Add error handling for fetchOrganizations during polling Co-authored-by: riderx <4084527+riderx@users.noreply.github.com> --- src/stores/organization.ts | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/stores/organization.ts b/src/stores/organization.ts index 3d9f38aed9..3f502e55f1 100644 --- a/src/stores/organization.ts +++ b/src/stores/organization.ts @@ -132,14 +132,22 @@ export const useOrganizationStore = defineStore('organization', () => { // Note: Using recursive setTimeout is safer than setInterval for async operations // as it ensures one poll completes before the next starts pollTimeout.value = setTimeout(async () => { - await fetchOrganizations() - const org = _organizations.value.get(orgId) - if (org && isWaitingForStripeInfo(org)) { - // Still waiting, poll again - await pollForStripeInfo(orgId) + try { + await fetchOrganizations() + const org = _organizations.value.get(orgId) + if (org && isWaitingForStripeInfo(org)) { + // Still waiting, poll again (timeout check happens at start of next call) + await pollForStripeInfo(orgId) + } + else { + // Stripe info created or org changed + localStorage.removeItem(NEW_ORG_POLL_KEY) + isNewOrganizationLoading.value = false + } } - else { - // Stripe info created or org changed + catch (error) { + // If fetch fails, stop polling to avoid infinite errors + console.error('Failed to fetch organizations during polling:', error) localStorage.removeItem(NEW_ORG_POLL_KEY) isNewOrganizationLoading.value = false }