Skip to content

Commit 844643f

Browse files
fix: save org/app state on resume, enforce access checks, remove redundant prompts (#569)
* fix: save org in onboarding state so resume skips org selection Previously the org selection prompt appeared every time, even when resuming. Now markStepDone persists orgId/orgName in the temp file, and tryResumeOnboarding reads it back — asking "resume?" before org selection. If the saved org is no longer available, falls back to the normal org picker and resets progress. * fix: inform user when old onboarding progress cannot be resumed Old temp files without orgId are now explicitly flagged with a warning instead of silently discarding the saved state. * fix: save and restore appId in onboarding resume state Without this, resuming onboarding uses the appId from capacitor.config instead of the one created during step 1, causing subsequent steps to reference the wrong app. * fix: remove redundant channel confirm and cleanup temp on start over - Remove "Create channel X for Y in Capgo?" confirmation — user already picked the channel name, no need to ask again - Delete temp file when user chooses "No, start over" on the resume prompt so they aren't asked again on the next run - Fix "Done" → "done" in channel success message * fix: enforce role and 2FA checks on resume, handle RPC errors - Validate saved org has admin/super_admin role before auto-resuming (permissions may have changed since state was saved) - Check enforcing_2fa guard on resume just like selectOrganizationForInit does - Handle RPC error from get_orgs_v7 in the resume path instead of silently treating API failures as "org no longer available" - Fall back to org selection with stepToSkip=0 in all failure cases
1 parent 0970602 commit 844643f

File tree

1 file changed

+100
-31
lines changed

1 file changed

+100
-31
lines changed

src/init/command.ts

Lines changed: 100 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ let globalChannelName = defaultChannel
5151
let globalPlatform: 'ios' | 'android' = 'ios'
5252
let globalDelta = false
5353
let globalCurrentVersion: string | undefined
54+
let globalAppId: string | undefined
5455

5556
function readTmpObj() {
5657
tmpObject ??= readdirSync(tmp.tmpdir)
@@ -391,10 +392,16 @@ async function ensureWorkspaceReadyForInit(initialAppId?: string): Promise<strin
391392
}
392393
}
393394

395+
let globalOrgId: string | undefined
396+
let globalOrgName: string | undefined
397+
394398
function markStepDone(step: number, pathToPackageJson?: string, channelName?: string) {
395399
try {
396400
writeFileSync(getTmpObjectPath(), JSON.stringify({
397401
step_done: step,
402+
orgId: globalOrgId,
403+
orgName: globalOrgName,
404+
appId: globalAppId,
398405
pathToPackageJson: pathToPackageJson ?? globalPathToPackageJson,
399406
channelName: channelName ?? globalChannelName,
400407
platform: globalPlatform,
@@ -414,14 +421,30 @@ function markStepDone(step: number, pathToPackageJson?: string, channelName?: st
414421
}
415422
}
416423

417-
async function readStepsDone(orgId: string, apikey: string): Promise<number | undefined> {
424+
interface ResumeResult {
425+
stepDone: number
426+
orgId: string
427+
orgName: string
428+
appId?: string
429+
}
430+
431+
async function tryResumeOnboarding(apikey: string): Promise<ResumeResult | undefined> {
418432
try {
419433
const rawData = readFileSync(getTmpObjectPath(), 'utf-8')
420434
if (!rawData || rawData.length === 0)
421435
return undefined
422436

423-
const { step_done, pathToPackageJson, channelName, platform, delta, currentVersion } = JSON.parse(rawData)
437+
const { step_done, orgId, orgName, appId: savedAppId, pathToPackageJson, channelName, platform, delta, currentVersion } = JSON.parse(rawData)
438+
if (!orgId || !step_done) {
439+
pLog.warn('⚠️ Found previous onboarding progress, but it was saved in an older format.')
440+
pLog.info(' Starting fresh. Your previous progress cannot be resumed.')
441+
return undefined
442+
}
443+
424444
pLog.info(formatInitResumeMessage(step_done, initOnboardingSteps.length))
445+
if (orgName) {
446+
pLog.info(` Organization: ${orgName}`)
447+
}
425448
const resumeChoice = await pSelect({
426449
message: 'Would you like to continue from where you left off?',
427450
options: [
@@ -446,9 +469,14 @@ async function readStepsDone(orgId: string, apikey: string): Promise<number | un
446469
if (typeof currentVersion === 'string' && currentVersion.length > 0) {
447470
globalCurrentVersion = currentVersion
448471
}
449-
return step_done
472+
if (savedAppId) {
473+
globalAppId = savedAppId
474+
}
475+
return { stepDone: step_done, orgId, orgName, appId: savedAppId }
450476
}
451477

478+
// User chose to start over — delete the saved progress
479+
cleanupStepsDone()
452480
return undefined
453481
}
454482
catch (err) {
@@ -1201,33 +1229,21 @@ async function addChannelStep(orgId: string, apikey: string, appId: string) {
12011229
}
12021230

12031231
globalChannelName = channelName
1204-
const doChannel = await pConfirm({ message: `Create channel ${channelName} for ${appId} in Capgo?` })
1205-
await cancelCommand(doChannel, orgId, apikey)
1206-
if (doChannel) {
1207-
const s = pSpinner()
1208-
// create production channel public
1209-
s.start(`Running: ${pm.runner} @capgo/cli@latest channel add ${channelName} ${appId} --default`)
1210-
try {
1211-
const addChannelRes = await addChannelInternal(channelName, appId, {
1212-
default: true,
1213-
apikey,
1214-
}, true)
1215-
if (!addChannelRes)
1216-
s.stop(`Channel already added ✅`)
1217-
else
1218-
s.stop(`Channel add Done ✅`)
1219-
}
1220-
catch (error) {
1221-
s.stop(`Channel creation failed ❌`)
1222-
throw error
1223-
}
1232+
const s = pSpinner()
1233+
s.start(`Running: ${pm.runner} @capgo/cli@latest channel add ${channelName} ${appId} --default`)
1234+
try {
1235+
const addChannelRes = await addChannelInternal(channelName, appId, {
1236+
default: true,
1237+
apikey,
1238+
}, true)
1239+
if (!addChannelRes)
1240+
s.stop(`Channel already added ✅`)
1241+
else
1242+
s.stop(`Channel add done ✅`)
12241243
}
1225-
else {
1226-
pLog.info(`If you change your mind, run it for yourself with: "${pm.runner} @capgo/cli@latest channel add ${channelName} ${appId} --default"`)
1227-
pLog.info(`Alternatively, you can:`)
1228-
pLog.info(` • Set the channel in your capacitor.config.ts file`)
1229-
pLog.info(` • Use the JavaScript setChannel() method to dynamically set the channel`)
1230-
pLog.info(` • Configure channels later from the Capgo web console`)
1244+
catch (error) {
1245+
s.stop(`Channel creation failed ❌`)
1246+
throw error
12311247
}
12321248
await markStep(orgId, apikey, 'add-channel', appId)
12331249
return channelName
@@ -2318,13 +2334,65 @@ export async function initApp(apikeyCommand: string, appId: string, options: Sup
23182334
const supabase = await createSupabaseClient(options.apikey, options.supaHost, options.supaAnon)
23192335
await verifyUser(supabase, options.apikey, ['upload', 'all', 'read', 'write'])
23202336

2321-
const organization = await selectOrganizationForInit(supabase, ['admin', 'super_admin'])
2337+
// Try to resume from saved state before asking for org selection
2338+
const resumed = await tryResumeOnboarding(options.apikey)
2339+
let stepToSkip = resumed?.stepDone ?? 0
2340+
2341+
let organization: Organization
2342+
if (resumed) {
2343+
// Fetch orgs to validate the saved one still exists and is accessible
2344+
const { error: orgError, data: allOrganizations } = await supabase.rpc('get_orgs_v7')
2345+
if (orgError || !allOrganizations) {
2346+
pLog.error(`Cannot verify organization access: ${orgError ? JSON.stringify(orgError) : 'no data returned'}`)
2347+
pLog.warn('Falling back to organization selection.')
2348+
organization = await selectOrganizationForInit(supabase, ['admin', 'super_admin'])
2349+
stepToSkip = 0
2350+
}
2351+
else {
2352+
const savedOrg = allOrganizations.find(org => org.gid === resumed.orgId)
2353+
const normalizeRole = (role: string | null | undefined) => role?.replace(/^org_/, '') ?? ''
2354+
const hasRequiredRole = savedOrg && ['admin', 'super_admin'].includes(normalizeRole(savedOrg.role))
2355+
const blocked2fa = savedOrg?.enforcing_2fa && !savedOrg['2fa_has_access']
2356+
2357+
if (!savedOrg) {
2358+
pLog.warn(`Previously used organization "${resumed.orgName}" is no longer available. Please select a new one.`)
2359+
organization = await selectOrganizationForInit(supabase, ['admin', 'super_admin'])
2360+
stepToSkip = 0
2361+
}
2362+
else if (!hasRequiredRole) {
2363+
pLog.warn(`You no longer have admin access to "${savedOrg.name}". Please select a different organization.`)
2364+
organization = await selectOrganizationForInit(supabase, ['admin', 'super_admin'])
2365+
stepToSkip = 0
2366+
}
2367+
else if (blocked2fa) {
2368+
pLog.warn(`Organization "${savedOrg.name}" now requires 2FA. Enable it at https://web.capgo.app/settings/account`)
2369+
pLog.warn('Please select a different organization or enable 2FA and try again.')
2370+
organization = await selectOrganizationForInit(supabase, ['admin', 'super_admin'])
2371+
stepToSkip = 0
2372+
}
2373+
else {
2374+
organization = savedOrg
2375+
pLog.info(`Using organization "${savedOrg.name}"`)
2376+
}
2377+
}
2378+
}
2379+
else {
2380+
organization = await selectOrganizationForInit(supabase, ['admin', 'super_admin'])
2381+
}
2382+
23222383
const orgId = organization.gid
2384+
globalOrgId = orgId
2385+
globalOrgName = organization.name
2386+
2387+
if (resumed?.appId) {
2388+
appId = resumed.appId
2389+
globalAppId = appId
2390+
}
2391+
23232392
const pendingOnboardingSelection = await maybeReusePendingOnboardingApp(organization, options.apikey, appId, supabase)
23242393
appId = pendingOnboardingSelection.appId ?? appId
23252394
await ensureCapacitorProjectReady(orgId, options.apikey, appId, pendingOnboardingSelection.pendingApp)
23262395

2327-
let stepToSkip = await readStepsDone(orgId, options.apikey) ?? 0
23282396
if (pendingOnboardingSelection.reusedPendingApp) {
23292397
stepToSkip = Math.max(stepToSkip, 1)
23302398
}
@@ -2351,6 +2419,7 @@ export async function initApp(apikeyCommand: string, appId: string, options: Sup
23512419
renderCurrentStep(1)
23522420
await checkPrerequisitesStep(orgId, options.apikey)
23532421
appId = await addAppStep(organization, options.apikey, appId, options)
2422+
globalAppId = appId
23542423
markStepDone(1)
23552424
}
23562425

0 commit comments

Comments
 (0)