Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions src/components/Banner.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
32 changes: 28 additions & 4 deletions src/pages/dashboard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const { t } = useI18n()
const displayStore = useDisplayStore()
const apps = ref<Database['public']['Tables']['apps']['Row'][]>([])

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(() => {
Expand All @@ -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()
Expand Down Expand Up @@ -95,6 +96,29 @@ displayStore.defaultBack = '/app'
<Usage v-if="!lacksSecurityAccess" :force-demo="paymentFailed" />
</div>

<!-- Overlay for new organization loading (waiting for trial setup) -->
<div
v-if="isNewOrganizationLoading"
class="flex absolute inset-0 z-10 flex-col justify-center items-center bg-white/60 dark:bg-gray-900/60"
>
<div class="p-8 text-center bg-white rounded-xl border shadow-lg dark:bg-gray-800 dark:border-gray-700">
<div class="flex justify-center mb-4">
<div class="flex justify-center items-center w-16 h-16 bg-blue-100 rounded-full dark:bg-blue-900/30">
<Spinner size="w-8 h-8" color="fill-blue-500 text-blue-200" />
</div>
</div>
<h2 class="mb-2 text-2xl font-bold text-gray-900 dark:text-white">
{{ t('setting-up-account') }}
</h2>
<p class="mb-2 text-gray-600 dark:text-gray-400">
{{ t('initializing-trial') }}
</p>
<p class="text-sm text-gray-500 dark:text-gray-500">
{{ t('this-takes-few-seconds') }}
</p>
</div>
</div>

<!-- Overlay for empty state (no apps) -->
<div
v-if="hasNoApps"
Expand Down
104 changes: 103 additions & 1 deletion src/stores/organization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,99 @@ export const useOrganizationStore = defineStore('organization', () => {
const currentOrganization = ref<Organization | undefined>(undefined)
const currentOrganizationFailed = ref(false)
const currentRole = ref<OrganizationRole | null>(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<ReturnType<typeof setTimeout> | 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
}

Expand All @@ -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
Expand Down Expand Up @@ -374,6 +475,7 @@ export const useOrganizationStore = defineStore('organization', () => {
currentOrganization,
currentOrganizationFailed,
currentRole,
isNewOrganizationLoading,
setCurrentOrganization,
setCurrentOrganizationToMain,
setCurrentOrganizationToFirst,
Expand Down
Loading