diff --git a/projects/developer/src/lib/api/accountRequest.ts b/projects/developer/src/lib/api/accountRequest.ts index eb91e2b4..29a80205 100644 --- a/projects/developer/src/lib/api/accountRequest.ts +++ b/projects/developer/src/lib/api/accountRequest.ts @@ -1,6 +1,14 @@ import { accessToken } from '$lib/auth/accessToken.ts'; +import { developerErrorMessage } from './developerErrorMessage.ts'; import { traktHeaders } from './traktHeaders.ts'; +async function errorCode(response: Response): Promise { + const body = await response.json().catch(() => null) as + | { error?: unknown } + | null; + return body?.error; +} + // Account requests bypass playground history, response previews, and storage. export async function accountRequest( slot: number, @@ -18,9 +26,10 @@ export async function accountRequest( redirect: 'error', }); if (!response.ok) { + const localized = developerErrorMessage(await errorCode(response)); const messages: Record = { 400: - 'Your GitHub connection could not be verified. Reconnect and try again.', + 'The request could not be accepted. Check your details and try again.', 401: 'Your session has expired. Refresh your account or sign in again.', 403: 'This account cannot perform this action. Check your app limit and your GitHub account connection.', @@ -29,7 +38,8 @@ export async function accountRequest( 429: 'Too many requests. Please wait before trying again.', }; throw new Error( - messages[response.status] ?? + localized ?? + messages[response.status] ?? `The request failed (${response.status}). Please try again.`, ); } diff --git a/projects/developer/src/lib/api/developerErrorMessage.spec.ts b/projects/developer/src/lib/api/developerErrorMessage.spec.ts new file mode 100644 index 00000000..52673adb --- /dev/null +++ b/projects/developer/src/lib/api/developerErrorMessage.spec.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from 'vitest'; +import { developerErrorMessage } from './developerErrorMessage.ts'; + +const API_ERROR_CODES = [ + 'authentication_required', + 'invalid_request_body', + 'invalid_application_id', + 'invalid_redirect_uri', + 'invalid_origin', + 'redirect_uris_too_long', + 'origins_too_long', + 'application_limit_reached', + 'application_not_found', + 'application_create_failed', + 'github_required', + 'github_not_linked', + 'github_unavailable', + 'github_code_expired', + 'github_account_taken', + 'github_account_mismatch', +]; + +describe('developerErrorMessage', () => { + it.each(API_ERROR_CODES)('has copy for %s', (code) => { + const message = developerErrorMessage(code); + expect(message).toBeTruthy(); + expect(message).not.toBe(code); + }); + it('returns null for an unknown code', () => { + expect(developerErrorMessage('something_new')).toBeNull(); + }); + it('returns null for anything that is not a string', () => { + expect(developerErrorMessage(undefined)).toBeNull(); + expect(developerErrorMessage({ secret: 'x' })).toBeNull(); + }); + it('does not resolve inherited object keys', () => { + expect(developerErrorMessage('toString')).toBeNull(); + expect(developerErrorMessage('__proto__')).toBeNull(); + }); +}); diff --git a/projects/developer/src/lib/api/developerErrorMessage.ts b/projects/developer/src/lib/api/developerErrorMessage.ts new file mode 100644 index 00000000..e50f31a2 --- /dev/null +++ b/projects/developer/src/lib/api/developerErrorMessage.ts @@ -0,0 +1,34 @@ +const DEVELOPER_ERROR_MESSAGES = { + authentication_required: + 'Your session has expired. Refresh your account or sign in again.', + invalid_request_body: + 'The request could not be accepted. Check your details and try again.', + invalid_application_id: 'This app could not be found. Reload your apps.', + invalid_redirect_uri: 'Enter valid redirect URIs, one per line.', + invalid_origin: 'Enter valid HTTP or HTTPS origins, one per line.', + redirect_uris_too_long: + 'Enter 1-25 redirect URIs, within 2,048 characters in total.', + origins_too_long: 'Enter up to 25 origins, within 255 characters in total.', + application_limit_reached: 'You have reached the app limit for your account.', + application_not_found: 'This app is no longer available. Reload your apps.', + application_create_failed: 'The app could not be created. Please try again.', + github_required: 'Connect your GitHub account before creating an app.', + github_not_linked: 'No GitHub account is linked to this Trakt account.', + github_unavailable: + 'GitHub is not responding right now. Please try again in a moment.', + github_code_expired: + 'The GitHub connection expired before it finished. Connect again.', + github_account_taken: + 'This GitHub account is already connected to another Trakt account.', + github_account_mismatch: + 'Your Trakt account is connected to a different GitHub account. Use Switch GitHub account to change it.', +} as const; + +export type DeveloperErrorCode = keyof typeof DEVELOPER_ERROR_MESSAGES; + +export function developerErrorMessage(code: unknown): string | null { + return typeof code === 'string' && + Object.hasOwn(DEVELOPER_ERROR_MESSAGES, code) + ? DEVELOPER_ERROR_MESSAGES[code as DeveloperErrorCode] + : null; +} diff --git a/projects/developer/src/lib/features/apps/ApplicationForm.svelte b/projects/developer/src/lib/features/apps/ApplicationForm.svelte index 497d89b6..c4dad125 100644 --- a/projects/developer/src/lib/features/apps/ApplicationForm.svelte +++ b/projects/developer/src/lib/features/apps/ApplicationForm.svelte @@ -1,21 +1,15 @@ + + + + diff --git a/projects/developer/src/lib/features/apps/GithubMark.svelte b/projects/developer/src/lib/features/apps/GithubMark.svelte new file mode 100644 index 00000000..d6ad5ff7 --- /dev/null +++ b/projects/developer/src/lib/features/apps/GithubMark.svelte @@ -0,0 +1,10 @@ + + + diff --git a/projects/developer/src/lib/features/apps/applications.spec.ts b/projects/developer/src/lib/features/apps/applications.spec.ts index 74ddf4d7..ca04e043 100644 --- a/projects/developer/src/lib/features/apps/applications.spec.ts +++ b/projects/developer/src/lib/features/apps/applications.spec.ts @@ -56,4 +56,54 @@ describe('app management transport', () => { ); await expect(saveApplication(0, input)).rejects.toThrow('app limit'); }); + it('maps a known error code to the portal copy', async () => { + vi.mocked(accessToken).mockResolvedValue('test-token'); + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue( + Response.json({ error: 'github_account_taken' }, { status: 403 }), + ), + ); + await expect(saveApplication(0, input)).rejects.toThrow( + 'already connected to another Trakt account', + ); + }); + it('never shows server text, even inside a JSON error', async () => { + vi.mocked(accessToken).mockResolvedValue('test-token'); + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue( + Response.json({ error: 'Application limit reached (5)' }, { + status: 403, + }), + ), + ); + await expect(saveApplication(0, input)).rejects.toThrow( + 'This account cannot perform this action', + ); + }); + it('ignores an error that is not a string', async () => { + vi.mocked(accessToken).mockResolvedValue('test-token'); + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue( + Response.json({ error: { secret: 'x' } }, { status: 400 }), + ), + ); + await expect(saveApplication(0, input)).rejects.toThrow( + 'Check your details', + ); + }); + it('falls back to the status copy for an unknown code', async () => { + vi.mocked(accessToken).mockResolvedValue('test-token'); + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue( + Response.json({ error: 'something_new' }, { status: 429 }), + ), + ); + await expect(saveApplication(0, input)).rejects.toThrow( + 'Too many requests', + ); + }); }); diff --git a/projects/developer/src/lib/features/apps/applications.ts b/projects/developer/src/lib/features/apps/applications.ts index 8b1dcdd6..b0c67320 100644 --- a/projects/developer/src/lib/features/apps/applications.ts +++ b/projects/developer/src/lib/features/apps/applications.ts @@ -13,7 +13,6 @@ export const applicationSchema = z.object({ approved_at: z.string().nullish(), scopes: z.array(z.string()), created_at: z.string(), - github_username: z.string().nullish(), permissions: z.object({ scrobble: z.boolean().nullish(), checkin: z.boolean().nullish(), @@ -26,7 +25,6 @@ export type ApplicationInput = { description?: string; redirect_uri: string[]; origins: string[]; - github_code?: string; }; export async function listApplications(slot: number): Promise { diff --git a/projects/developer/src/lib/features/apps/developerProfile.spec.ts b/projects/developer/src/lib/features/apps/developerProfile.spec.ts new file mode 100644 index 00000000..3743e1f9 --- /dev/null +++ b/projects/developer/src/lib/features/apps/developerProfile.spec.ts @@ -0,0 +1,82 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { accessToken } from '$lib/auth/accessToken.ts'; +import { + getDeveloperProfile, + linkGithub, + unlinkGithub, +} from './developerProfile.ts'; +vi.mock('$lib/auth/accessToken.ts', () => ({ accessToken: vi.fn() })); +vi.mock( + '$env/static/public', + () => ({ PUBLIC_TRAKT_CLIENT_ID: 'test-client' }), +); +afterEach(() => { + vi.unstubAllGlobals(); + vi.clearAllMocks(); +}); + +describe('developer profile transport', () => { + it('reads a linked profile', async () => { + vi.mocked(accessToken).mockResolvedValue('test-token'); + const profile = { + github: { + id: 6339681, + username: 'vladjerca', + linked_at: '2026-09-23T08:21:05.000Z', + }, + applications: { count: 1, limit: 5 }, + }; + const fetcher = vi.fn().mockResolvedValue(Response.json(profile)); + vi.stubGlobal('fetch', fetcher); + + await expect(getDeveloperProfile(2)).resolves.toEqual(profile); + expect(fetcher.mock.calls[0]![0]).toBe( + 'https://api.trakt.tv/v3/users/me/developer', + ); + expect(accessToken).toHaveBeenCalledWith(2); + }); + it('reads an unlinked profile', async () => { + vi.mocked(accessToken).mockResolvedValue('test-token'); + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue( + Response.json({ github: null, applications: { count: 2, limit: 5 } }), + ), + ); + await expect(getDeveloperProfile(0)).resolves.toEqual({ + github: null, + applications: { count: 2, limit: 5 }, + }); + }); + it('rejects a malformed profile', async () => { + vi.mocked(accessToken).mockResolvedValue('test-token'); + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue(Response.json({ github: null })), + ); + await expect(getDeveloperProfile(0)).rejects.toThrow( + 'could not be read', + ); + }); + it('links with the code and the switch intent, and unlinks with DELETE', async () => { + vi.mocked(accessToken).mockResolvedValue('test-token'); + const fetcher = vi.fn().mockImplementation(() => + Promise.resolve(new Response(null, { status: 204 })) + ); + vi.stubGlobal('fetch', fetcher); + + await linkGithub(1, 'abc', true); + await unlinkGithub(1); + + expect(fetcher.mock.calls.map(([url, init]) => [url, init.method])).toEqual( + [ + ['https://api.trakt.tv/v3/users/me/developer/github', 'PUT'], + ['https://api.trakt.tv/v3/users/me/developer/github', 'DELETE'], + ], + ); + expect(JSON.parse(fetcher.mock.calls[0]![1].body)).toEqual({ + code: 'abc', + switch: true, + }); + }); +}); diff --git a/projects/developer/src/lib/features/apps/developerProfile.ts b/projects/developer/src/lib/features/apps/developerProfile.ts new file mode 100644 index 00000000..a6a27e1c --- /dev/null +++ b/projects/developer/src/lib/features/apps/developerProfile.ts @@ -0,0 +1,45 @@ +import { z } from 'zod'; +import { accountRequest } from '$lib/api/accountRequest.ts'; + +export const developerProfileSchema = z.object({ + github: z + .object({ + id: z.number(), + username: z.string(), + linked_at: z.string().nullish(), + }) + .nullable(), + applications: z.object({ + count: z.number(), + limit: z.number(), + }), +}); +export type DeveloperProfile = z.infer; + +const GITHUB_PATH = '/v3/users/me/developer/github'; + +export async function getDeveloperProfile( + slot: number, +): Promise { + const response = await accountRequest(slot, '/v3/users/me/developer'); + const parsed = developerProfileSchema.safeParse(await response.json()); + if (!parsed.success) { + throw new Error( + 'Your developer account could not be read. Please try again.', + ); + } + return parsed.data; +} +export async function linkGithub( + slot: number, + code: string, + allowSwitch: boolean, +): Promise { + await accountRequest(slot, GITHUB_PATH, { + method: 'PUT', + body: JSON.stringify({ code, switch: allowSwitch }), + }); +} +export async function unlinkGithub(slot: number): Promise { + await accountRequest(slot, GITHUB_PATH, { method: 'DELETE' }); +} diff --git a/projects/developer/src/lib/features/apps/githubConnect.spec.ts b/projects/developer/src/lib/features/apps/githubConnect.spec.ts index 2ac09974..aeeed896 100644 --- a/projects/developer/src/lib/features/apps/githubConnect.spec.ts +++ b/projects/developer/src/lib/features/apps/githubConnect.spec.ts @@ -7,17 +7,8 @@ import { completeGithubConnect, githubConnectUrl, isGithubCallback, - takeGithubCode, - takeGithubDraft, } from './githubConnect.ts'; -const DRAFT = { - name: 'My App', - description: 'Half typed', - redirects: 'https://example.com/callback', - origins: '', -}; - function fakeStorage(): Storage { const store: Record = {}; return { @@ -36,12 +27,14 @@ afterEach(() => { }); describe('githubConnectUrl', () => { - it('builds the authorize url and stashes state and return path', () => { + it('builds the authorize url and stashes the state and intent', () => { const storage = fakeStorage(); vi.stubGlobal('sessionStorage', storage); vi.stubGlobal('location', { origin: 'https://developer.trakt.tv' }); vi.stubGlobal('crypto', { randomUUID: () => 'fixed-state' }); - const url = new URL(githubConnectUrl('/apps/new', DRAFT)); + + const url = new URL(githubConnectUrl('switch')); + expect(url.origin + url.pathname).toBe( 'https://github.com/login/oauth/authorize', ); @@ -49,52 +42,26 @@ describe('githubConnectUrl', () => { expect(url.searchParams.get('redirect_uri')).toBe( 'https://developer.trakt.tv/apps', ); + expect(url.searchParams.has('scope')).toBe(false); expect(url.searchParams.get('state')).toBe('fixed-state'); expect(storage.getItem('trakt-developer-github-state')).toBe( 'fixed-state', ); - expect(storage.getItem('trakt-developer-github-return')).toBe( - '/apps/new', - ); - expect(storage.getItem('trakt-developer-github-draft')).toBe( - JSON.stringify(DRAFT), - ); - }); -}); - -describe('takeGithubDraft', () => { - it('restores the draft the user had typed before the redirect', () => { - const storage = fakeStorage(); - vi.stubGlobal('sessionStorage', storage); - vi.stubGlobal('location', { origin: 'https://developer.trakt.tv' }); - vi.stubGlobal('crypto', { randomUUID: () => 'fixed-state' }); - githubConnectUrl('/apps/new', DRAFT); - expect(takeGithubDraft()).toEqual(DRAFT); - expect(takeGithubDraft()).toBeNull(); - }); - it('drops a draft that is not readable', () => { - const storage = fakeStorage(); - storage.setItem('trakt-developer-github-draft', 'not-json'); - vi.stubGlobal('sessionStorage', storage); - expect(takeGithubDraft()).toBeNull(); - }); - it('drops a draft whose fields are the wrong shape', () => { - const storage = fakeStorage(); - storage.setItem( - 'trakt-developer-github-draft', - JSON.stringify({ ...DRAFT, name: 42 }), - ); - vi.stubGlobal('sessionStorage', storage); - expect(takeGithubDraft()).toBeNull(); + expect(storage.getItem('trakt-developer-github-intent')).toBe('switch'); }); }); describe('completeGithubConnect', () => { - it('accepts a matching state, stashes the code and ignores unknown params', () => { + function pending(intent: string) { const storage = fakeStorage(); storage.setItem('trakt-developer-github-state', 'expected-state'); - storage.setItem('trakt-developer-github-return', '/apps/new'); + storage.setItem('trakt-developer-github-intent', intent); vi.stubGlobal('sessionStorage', storage); + return storage; + } + + it('returns the code for a matching state and ignores unknown params', () => { + const storage = pending('link'); const params = new URLSearchParams({ code: 'abc', state: 'expected-state', @@ -102,15 +69,22 @@ describe('completeGithubConnect', () => { }); expect(completeGithubConnect(params)).toEqual({ status: 'connected', - returnPath: '/apps/new', + code: 'abc', + allowSwitch: false, }); - expect(storage.getItem('trakt-developer-github-code')).toBe('abc'); expect(storage.getItem('trakt-developer-github-state')).toBeNull(); + expect(storage.getItem('trakt-developer-github-intent')).toBeNull(); + }); + it('carries a switch intent through', () => { + pending('switch'); + expect( + completeGithubConnect( + new URLSearchParams({ code: 'abc', state: 'expected-state' }), + ), + ).toEqual({ status: 'connected', code: 'abc', allowSwitch: true }); }); it('rejects a mismatched state', () => { - const storage = fakeStorage(); - storage.setItem('trakt-developer-github-state', 'expected-state'); - vi.stubGlobal('sessionStorage', storage); + pending('link'); expect( completeGithubConnect( new URLSearchParams({ code: 'abc', state: 'other' }), @@ -123,26 +97,8 @@ describe('completeGithubConnect', () => { completeGithubConnect(new URLSearchParams({ code: 'abc' })), ).toEqual({ status: 'invalid' }); }); - it('refuses a return path that is not a portal route', () => { - const storage = fakeStorage(); - storage.setItem('trakt-developer-github-state', 'expected-state'); - storage.setItem( - 'trakt-developer-github-return', - 'https://evil.example.com', - ); - vi.stubGlobal('sessionStorage', storage); - expect( - completeGithubConnect( - new URLSearchParams({ code: 'abc', state: 'expected-state' }), - ), - ).toEqual({ status: 'connected', returnPath: '/' }); - }); it('reports a denial callback and clears the pending state', () => { - const storage = fakeStorage(); - storage.setItem('trakt-developer-github-state', 'expected-state'); - storage.setItem('trakt-developer-github-return', '/apps/new'); - storage.setItem('trakt-developer-github-draft', JSON.stringify(DRAFT)); - vi.stubGlobal('sessionStorage', storage); + const storage = pending('switch'); const params = new URLSearchParams({ error: 'access_denied', error_description: 'The user has denied your application access.', @@ -150,9 +106,7 @@ describe('completeGithubConnect', () => { }); expect(completeGithubConnect(params)).toEqual({ status: 'denied' }); expect(storage.getItem('trakt-developer-github-state')).toBeNull(); - expect(storage.getItem('trakt-developer-github-return')).toBeNull(); - expect(storage.getItem('trakt-developer-github-code')).toBeNull(); - expect(storage.getItem('trakt-developer-github-draft')).toBeNull(); + expect(storage.getItem('trakt-developer-github-intent')).toBeNull(); }); }); @@ -166,13 +120,3 @@ describe('isGithubCallback', () => { expect(isGithubCallback(new URLSearchParams())).toBe(false); }); }); - -describe('takeGithubCode', () => { - it('reads and clears the stashed code', () => { - const storage = fakeStorage(); - storage.setItem('trakt-developer-github-code', 'abc'); - vi.stubGlobal('sessionStorage', storage); - expect(takeGithubCode()).toBe('abc'); - expect(takeGithubCode()).toBeNull(); - }); -}); diff --git a/projects/developer/src/lib/features/apps/githubConnect.ts b/projects/developer/src/lib/features/apps/githubConnect.ts index 70cf0413..f45044fb 100644 --- a/projects/developer/src/lib/features/apps/githubConnect.ts +++ b/projects/developer/src/lib/features/apps/githubConnect.ts @@ -1,23 +1,12 @@ -import { z } from 'zod'; import { PUBLIC_GITHUB_CLIENT_ID } from '$env/static/public'; -import { safeReturnPath } from '$lib/auth/accountNavigation.ts'; const STATE_KEY = 'trakt-developer-github-state'; -const RETURN_PATH_KEY = 'trakt-developer-github-return'; -const CODE_KEY = 'trakt-developer-github-code'; -const DRAFT_KEY = 'trakt-developer-github-draft'; +const INTENT_KEY = 'trakt-developer-github-intent'; -const draftSchema = z.object({ - name: z.string(), - description: z.string(), - redirects: z.string(), - origins: z.string(), -}); - -export type ApplicationDraft = z.infer; +export type GithubConnectIntent = 'link' | 'switch'; export type GithubConnectOutcome = - | { status: 'connected'; returnPath: string } + | { status: 'connected'; code: string; allowSwitch: boolean } | { status: 'denied' } | { status: 'invalid' }; @@ -31,14 +20,10 @@ export function isGithubCallback(searchParams: URLSearchParams): boolean { return searchParams.has('code') || searchParams.has('error'); } -export function githubConnectUrl( - returnPath: string, - draft: ApplicationDraft, -): string { +export function githubConnectUrl(intent: GithubConnectIntent): string { const state = globalThis.crypto.randomUUID(); globalThis.sessionStorage?.setItem(STATE_KEY, state); - globalThis.sessionStorage?.setItem(RETURN_PATH_KEY, returnPath); - globalThis.sessionStorage?.setItem(DRAFT_KEY, JSON.stringify(draft)); + globalThis.sessionStorage?.setItem(INTENT_KEY, intent); const url = new URL('https://github.com/login/oauth/authorize'); url.searchParams.set('client_id', PUBLIC_GITHUB_CLIENT_ID); url.searchParams.set('redirect_uri', `${globalThis.location.origin}/apps`); @@ -52,33 +37,13 @@ export function completeGithubConnect( const code = searchParams.get('code'); const state = searchParams.get('state'); const expectedState = take(STATE_KEY); - const returnPath = safeReturnPath(take(RETURN_PATH_KEY)); + const intent = take(INTENT_KEY); if (!state || !expectedState || state !== expectedState) { - take(DRAFT_KEY); return { status: 'invalid' }; } - if (!code) { - take(DRAFT_KEY); - return { status: 'denied' }; - } - - globalThis.sessionStorage?.setItem(CODE_KEY, code); - return { status: 'connected', returnPath }; -} + if (!code) return { status: 'denied' }; -export function takeGithubCode(): string | null { - return take(CODE_KEY); -} - -export function takeGithubDraft(): ApplicationDraft | null { - const raw = take(DRAFT_KEY); - if (!raw) return null; - try { - const parsed = draftSchema.safeParse(JSON.parse(raw)); - return parsed.success ? parsed.data : null; - } catch { - return null; - } + return { status: 'connected', code, allowSwitch: intent === 'switch' }; } diff --git a/projects/developer/src/lib/features/apps/validateApplication.spec.ts b/projects/developer/src/lib/features/apps/validateApplication.spec.ts index b7bd4fb0..13d8ed7f 100644 --- a/projects/developer/src/lib/features/apps/validateApplication.spec.ts +++ b/projects/developer/src/lib/features/apps/validateApplication.spec.ts @@ -33,17 +33,6 @@ describe('V3 application validation parity', () => { expect(() => parseApplication('App', '', 'myapp://callback', origin)) .toThrow(); }); - it('carries the github code through when present', () => { - expect( - parseApplication('App', '', 'myapp://callback', '', 'abc123'), - ).toEqual({ - name: 'App', - description: undefined, - redirect_uri: ['myapp://callback'], - origins: [], - github_code: 'abc123', - }); - }); it('enforces required fields and combined limits', () => { expect(() => parseApplication(' ', '', 'myapp://callback', '')).toThrow(); expect(() => parseApplication('App', '', '', '')).toThrow(); diff --git a/projects/developer/src/lib/features/apps/validateApplication.ts b/projects/developer/src/lib/features/apps/validateApplication.ts index d898d722..97533675 100644 --- a/projects/developer/src/lib/features/apps/validateApplication.ts +++ b/projects/developer/src/lib/features/apps/validateApplication.ts @@ -6,7 +6,6 @@ export function parseApplication( description: string, redirects: string, originsText: string, - githubCode?: string, ): ApplicationInput { const redirect_uri = lines(redirects); const origins = lines(originsText); @@ -67,6 +66,5 @@ export function parseApplication( description: description.trim() || undefined, redirect_uri, origins: canonical, - github_code: githubCode, }; } diff --git a/projects/developer/src/lib/guides/create-an-app.md b/projects/developer/src/lib/guides/create-an-app.md index 6d38d614..b96fb2f4 100644 --- a/projects/developer/src/lib/guides/create-an-app.md +++ b/projects/developer/src/lib/guides/create-an-app.md @@ -1,5 +1,5 @@ --- -updatedAt: 2026-09-22T00:00:00.000Z +updatedAt: 2026-09-23T00:00:00.000Z --- # ✅ Create an App @@ -8,8 +8,8 @@ To use the Trakt API, you'll first need to [**create a new API app**](/apps). Once done, you'll have everything you need to use the API (Client ID and Client Secret). -Creating an app requires a verified GitHub account. You'll be asked to connect -one before your app can be created. +Creating an app requires a verified GitHub account. Connect it once from the +Developer panel on My Apps, and every app you create afterwards uses it. By creating an app, you agree to the following terms: diff --git a/projects/developer/src/routes/(portal)/apps/+page.svelte b/projects/developer/src/routes/(portal)/apps/+page.svelte index 57a16b5a..e148bfec 100644 --- a/projects/developer/src/routes/(portal)/apps/+page.svelte +++ b/projects/developer/src/routes/(portal)/apps/+page.svelte @@ -1,45 +1,5 @@ -{#if connectError} -
-

- {connectError === "denied" - ? "GitHub connection cancelled" - : "GitHub connection failed"} -

-

- {connectError === "denied" - ? "You did not authorize the connection. Return to your app and try again." - : "This connection could not be verified. Return to your app and try again."} -

- Back to My Apps -
-{:else} - -{/if} - - +