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/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/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' + 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) + const isWaitingForStripeInfo = (org: Organization): boolean => { + 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 + const pollForStripeInfo = async (orgId: string) => { + if (pollTimeout.value) + clearTimeout(pollTimeout.value) + + // 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) + // 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 { + // Invalid data structure, reset + pollStart = Date.now() + } + } + catch { + // Invalid JSON or other error, reset + pollStart = Date.now() + } + } + + localStorage.setItem(NEW_ORG_POLL_KEY, JSON.stringify({ orgId, start: pollStart })) + 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 () => { + 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 + } + } + 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 + } + }, POLL_INTERVAL_MS) + } watch(currentOrganization, async (currentOrganizationRaw, oldOrganization) => { if (!currentOrganizationRaw) { currentRole.value = null localStorage.removeItem(STORAGE_KEY) + if (pollTimeout.value) { + clearTimeout(pollTimeout.value) + pollTimeout.value = null + } + localStorage.removeItem(NEW_ORG_POLL_KEY) + isNewOrganizationLoading.value = false return } @@ -90,7 +176,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.value) { + clearTimeout(pollTimeout.value) + pollTimeout.value = 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 +475,7 @@ export const useOrganizationStore = defineStore('organization', () => { currentOrganization, currentOrganizationFailed, currentRole, + isNewOrganizationLoading, setCurrentOrganization, setCurrentOrganizationToMain, setCurrentOrganizationToFirst,