Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/anchor-non-root-initial-route.md
Original file line number Diff line number Diff line change
@@ -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.
13 changes: 11 additions & 2 deletions src/host/layout/authRootBootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,20 @@ describe('generated auth root bootstrap', () => {
expect(rootLayout).toContain("<Stack.Protected guard={authState === 'unauthenticated'}>");
});

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("<Redirect href={'/products'} />");
expect(rootRedirect).toContain("<Redirect href={'/products'} withAnchor />");
expect(rootRedirect).not.toContain("<Redirect href={'/products'} />");
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');
});
});
33 changes: 33 additions & 0 deletions src/host/layout/templates/auth/adapter.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
22 changes: 15 additions & 7 deletions src/host/layout/templates/auth/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -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();

Expand Down
2 changes: 1 addition & 1 deletion src/host/layout/templates/redirect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export function getIndexRedirectRouteTsx(href: string): string {
return `import { Redirect } from 'expo-router';

export default function IndexRoute() {
return <Redirect href={'${escapeStringLiteral(href)}'} />;
return <Redirect href={'${escapeStringLiteral(href)}'} withAnchor />;
}
`;
}
Loading