Skip to content
Open
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
6 changes: 6 additions & 0 deletions .changeset/add-fallback-to-authenticated-content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@aws-blocks/auth-common": patch
"@aws-blocks/blocks": patch
---

Add optional `fallback` parameter to `AuthenticatedContent` for rendering alternative content when the user is not authenticated.
19 changes: 19 additions & 0 deletions packages/auth-common/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,25 @@ document.body.appendChild(
);
```

### Fallback for unauthenticated users

Pass an optional third argument to display alternative content when the user is NOT signed in:

```typescript
const loginPrompt = document.createElement('p');
loginPrompt.textContent = 'Please sign in to continue.';

document.body.appendChild(
AuthenticatedContent(authApi, (user) => {
const el = document.createElement('div');
el.textContent = `Welcome, ${user.username}`;
return el;
}, loginPrompt)
);
```

When no fallback is provided the container renders nothing while signed out (backward-compatible).

## Auth State Change Subscription

Subscribe to auth state changes from any source (same window + other tabs):
Expand Down
42 changes: 42 additions & 0 deletions packages/auth-common/src/ui.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, test, beforeEach } from 'node:test';

Check warning on line 4 in packages/auth-common/src/ui.test.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/correctness/noUnusedImports

Several of these imports are unused.
import assert from 'node:assert';
import { Window } from 'happy-dom';
import type { AuthState, AuthAction } from './index.js';
Expand Down Expand Up @@ -133,12 +133,12 @@
const signInBtn = buttons.find((b) => b.textContent === 'Sign In');
assert.ok(signInBtn, `Should find Sign In button. Found: ${buttons.map(b => b.textContent)}`);

const actionDiv = signInBtn!.parentElement!;

Check warning on line 136 in packages/auth-common/src/ui.test.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/style/noNonNullAssertion

Forbidden non-null assertion.

Check warning on line 136 in packages/auth-common/src/ui.test.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/style/noNonNullAssertion

Forbidden non-null assertion.
const inputs = actionDiv.querySelectorAll('input') as NodeListOf<HTMLInputElement>;
inputs[0].value = 'alice';
inputs[1].value = 'secret';

signInBtn!.click();

Check warning on line 141 in packages/auth-common/src/ui.test.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/style/noNonNullAssertion

Forbidden non-null assertion.
await flush();

assert.strictEqual(api.calls.length, 1);
Expand Down Expand Up @@ -468,7 +468,7 @@
// hydration sees the passkey form but the click resolves to the
// signed-in screen.
api.nextState = signedInState();
const btn = el.querySelector('button')!;

Check warning on line 471 in packages/auth-common/src/ui.test.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/style/noNonNullAssertion

Forbidden non-null assertion.
btn.click();
await flush();
await flush();
Expand Down Expand Up @@ -567,6 +567,48 @@

assert.strictEqual(el.children.length, 0);
});

test('renders fallback when signed out and fallback is provided', async () => {
const api = mockApi(signedOutState());
const fallback = document.createElement('div');
fallback.textContent = 'Please sign in';
const el = AuthenticatedContent(api, (user) => {
const span = document.createElement('span');
span.textContent = `Hello ${user.username}`;
return span;
}, fallback);
await flush();

assert.ok(el.textContent?.includes('Please sign in'));
});

test('renders content (not fallback) when signed in and fallback is provided', async () => {
const api = mockApi(signedInState());
const fallback = document.createElement('div');
fallback.textContent = 'Please sign in';
const el = AuthenticatedContent(api, (user) => {
const span = document.createElement('span');
span.textContent = `Hello ${user.username}`;
return span;
}, fallback);
await flush();

assert.ok(el.textContent?.includes('Hello alice'));
assert.ok(!el.textContent?.includes('Please sign in'));
});

test('renders nothing when signed out and no fallback is provided (backward compat)', async () => {
const api = mockApi(signedOutState());
const el = AuthenticatedContent(api, (user) => {
const span = document.createElement('span');
span.textContent = `Hello ${user.username}`;
return span;
});
await flush();

assert.strictEqual(el.children.length, 0);
assert.strictEqual(el.textContent, '');
});
});

describe('onAuthChange', () => {
Expand Down
23 changes: 20 additions & 3 deletions packages/auth-common/src/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
signUp: { username: string; password: string } & Record<string, string>;
confirmSignUp: { username: string; code: string; password?: string };
resendSignUpCode: { username: string };
signOut: {};

Check warning on line 35 in packages/auth-common/src/ui.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/complexity/noBannedTypes

Don't use '{}' as a type.
resetPassword: { username: string };
confirmResetPassword: { username: string; code: string; newPassword: string };
/**
Expand Down Expand Up @@ -62,14 +62,14 @@
* `credentialCreationOptions` JSON blob the browser feeds into
* `navigator.credentials.create(...)`.
*/
startPasskeyRegistration: {};

Check warning on line 65 in packages/auth-common/src/ui.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/complexity/noBannedTypes

Don't use '{}' as a type.
/**
* Finish a passkey enrolment. `credential` is the JSON-encoded
* `PublicKeyCredential` returned by `navigator.credentials.create(...)`.
*/
completePasskeyRegistration: { credential: string };
/** List the current user's registered passkeys. */
listPasskeys: {};

Check warning on line 72 in packages/auth-common/src/ui.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/complexity/noBannedTypes

Don't use '{}' as a type.
/** Delete a registered passkey by `credentialId`. */
deletePasskey: { credentialId: string };
}
Expand Down Expand Up @@ -280,11 +280,12 @@
/**
* Container that renders content only when the user is signed in.
* Automatically re-renders when auth state changes (same window + cross-tab).
* Shows nothing when signed out.
* Shows the optional `fallback` when signed out, or nothing if no fallback is provided.
*
* @param api - The state machine API from `auth.createApi()`
* @param render - Called with the authenticated user. Return the content to display.
* @returns An HTMLElement that shows content when signed in, empty when signed out.
* @param fallback - Optional. A DOM node to display when the user is NOT signed in.
* @returns An HTMLElement that shows content when signed in, the fallback (or nothing) when signed out.
*
* @example
* ```typescript
Expand All @@ -296,17 +297,33 @@
* })
* );
* ```
*
* @example
* ```typescript
* // With a fallback for unauthenticated users
* const loginPrompt = document.createElement('p');
* loginPrompt.textContent = 'Please sign in to continue.';
*
* document.body.appendChild(
* AuthenticatedContent(authApi, (user) => {
* const el = document.createElement('div');
* el.textContent = `Welcome, ${user.username}`;
* return el;
* }, loginPrompt)
* );
* ```
*/
export function AuthenticatedContent(
api: AuthStateApi,
render: (user: AuthUser) => Node,
fallback?: Node,
): HTMLElement {
const container = document.createElement('div');
onAuthChange(api, (user) => {
if (user) {
container.replaceChildren(render(user));
} else {
container.replaceChildren();
container.replaceChildren(...(fallback ? [fallback] : []));
}
});
return container;
Expand Down Expand Up @@ -565,7 +582,7 @@
if (state.state === 'signedIn') {
const heading = document.createElement('h3');
heading.style.cssText = 'margin-top: 0;';
heading.textContent = `Signed in as: ${state.user!.username}`;

Check warning on line 585 in packages/auth-common/src/ui.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/style/noNonNullAssertion

Forbidden non-null assertion.
div.appendChild(heading);
} else {
// Heading priority: (1) action-level override on the *first*
Expand Down Expand Up @@ -957,7 +974,7 @@
function renderExternalAction(action: AuthAction): Node {
const form = document.createElement('form');
form.method = action.method ?? 'GET';
form.action = action.url!;

Check warning on line 977 in packages/auth-common/src/ui.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/style/noNonNullAssertion

Forbidden non-null assertion.
form.style.cssText = 'margin-bottom: 8px;';

for (const field of action.fields) {
Expand Down
Loading