diff --git a/.changeset/anchor-non-root-initial-route.md b/.changeset/anchor-non-root-initial-route.md new file mode 100644 index 0000000..ca6f81e --- /dev/null +++ b/.changeset/anchor-non-root-initial-route.md @@ -0,0 +1,5 @@ +--- +'@ankhorage/studio': patch +--- + +Anchor generated non-root auth redirects so the configured initial screen remains the root of navigator history without an erroneous Back button. diff --git a/src/host/layout/authRootBootstrap.test.ts b/src/host/layout/authRootBootstrap.test.ts index 5c5bc2f..84ee73a 100644 --- a/src/host/layout/authRootBootstrap.test.ts +++ b/src/host/layout/authRootBootstrap.test.ts @@ -82,11 +82,20 @@ describe('generated auth root bootstrap', () => { expect(rootLayout).toContain(""); }); - test('keeps a non-root post-sign-in redirect canonical', () => { + test('anchors a non-root post-sign-in route as the initial navigator history entry', () => { const files = generateAuthFiles('products'); const rootRedirect = files.find((file) => file.path === 'src/app/index.tsx')?.content ?? ''; + const rootLayout = files.find((file) => file.path === 'src/app/_layout.tsx')?.content ?? ''; + const appLayout = + files.find((file) => file.path === 'src/app/(app)/_layout.tsx')?.content ?? ''; + const tabsLayout = + files.find((file) => file.path === 'src/app/(app)/(tabs)/_layout.tsx')?.content ?? ''; - expect(rootRedirect).toContain(""); + expect(rootRedirect).toContain(""); + expect(rootRedirect).not.toContain(""); + expect(rootLayout).toContain("initialRouteName: '(app)'"); + expect(appLayout).toContain("initialRouteName: '(tabs)'"); + expect(tabsLayout).toContain("initialRouteName: 'products'"); expect(files.map((file) => file.path)).toContain('src/app/(app)/(tabs)/products.tsx'); }); }); diff --git a/src/host/layout/templates/auth/adapter.test.ts b/src/host/layout/templates/auth/adapter.test.ts new file mode 100644 index 0000000..ba0bd0d --- /dev/null +++ b/src/host/layout/templates/auth/adapter.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, test } from 'bun:test'; + +import { getAuthAdapterTs } from './adapter'; + +describe('getAuthAdapterTs', () => { + test('keeps Expo public environment reads static while typing generated process env', () => { + const source = getAuthAdapterTs(); + + expect(source).toContain('declare const process: {'); + expect(source).toContain('readonly EXPO_PUBLIC_SUPABASE_URL?: string;'); + expect(source).toContain('readonly EXPO_PUBLIC_SUPABASE_ANON_KEY?: string;'); + expect(source).toContain('process.env.EXPO_PUBLIC_SUPABASE_URL?.trim()'); + expect(source).toContain('process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY?.trim()'); + expect(source).not.toContain('globalThis.process'); + expect(source).not.toContain('env['); + }); + + test('omits compile-time false OAuth branches when no providers are configured', () => { + const source = getAuthAdapterTs(); + + expect(source).not.toContain('generatedOAuthProviders'); + expect(source).not.toContain('oauthProviders: generatedOAuthProviders'); + expect(source).not.toContain('generatedOAuthProviders.length > 0'); + }); + + test('renders configured OAuth providers directly without a generated length condition', () => { + const source = getAuthAdapterTs({ oauthProviders: ['google'] }); + + expect(source).toContain('const generatedOAuthProviders = ["google"] as const;'); + expect(source).toContain('oauthProviders: generatedOAuthProviders'); + expect(source).not.toContain('generatedOAuthProviders.length > 0'); + }); +}); diff --git a/src/host/layout/templates/auth/adapter.ts b/src/host/layout/templates/auth/adapter.ts index d90fbce..2c7df6d 100644 --- a/src/host/layout/templates/auth/adapter.ts +++ b/src/host/layout/templates/auth/adapter.ts @@ -3,16 +3,27 @@ interface AuthAdapterTemplateOptions { } export function getAuthAdapterTs(options: AuthAdapterTemplateOptions = {}) { - const oauthProviders = JSON.stringify(options.oauthProviders ?? []); + const oauthProviders = options.oauthProviders ?? []; + const oauthProviderDeclaration = + oauthProviders.length > 0 + ? `const generatedOAuthProviders = ${JSON.stringify(oauthProviders)} as const;\n\n` + : ''; + const oauthProviderProperty = + oauthProviders.length > 0 ? '\n oauthProviders: generatedOAuthProviders,' : ''; return `import type { AuthAdapter, AuthResult, AuthSession, AuthUser } from '@ankhorage/contracts/auth'; import { createSupabaseAuthAdapter } from '@ankhorage/supabase-auth'; import { AUTH_SESSION_STORAGE_KEY, authSessionStorage } from './session'; -const generatedOAuthProviders = ${oauthProviders} as const; +declare const process: { + readonly env: { + readonly EXPO_PUBLIC_SUPABASE_URL?: string; + readonly EXPO_PUBLIC_SUPABASE_ANON_KEY?: string; + }; +}; -const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL?.trim() ?? ''; +${oauthProviderDeclaration}const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL?.trim() ?? ''; const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY?.trim() ?? ''; export const authAdapter: AuthAdapter = @@ -21,10 +32,7 @@ export const authAdapter: AuthAdapter = url: supabaseUrl, anonKey: supabaseAnonKey, storage: authSessionStorage, - storageKey: AUTH_SESSION_STORAGE_KEY, - ...(generatedOAuthProviders.length > 0 - ? { oauthProviders: generatedOAuthProviders } - : {}), + storageKey: AUTH_SESSION_STORAGE_KEY,${oauthProviderProperty} }) : createMissingSupabaseAuthAdapter(); diff --git a/src/host/layout/templates/redirect.ts b/src/host/layout/templates/redirect.ts index e1ee0ac..a3b90e5 100644 --- a/src/host/layout/templates/redirect.ts +++ b/src/host/layout/templates/redirect.ts @@ -4,7 +4,7 @@ export function getIndexRedirectRouteTsx(href: string): string { return `import { Redirect } from 'expo-router'; export default function IndexRoute() { - return ; + return ; } `; }