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
54 changes: 54 additions & 0 deletions packages/vue/src/__tests__/components/actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,60 @@ describe('SignInButton', () => {
expect(signIn).toHaveBeenCalledWith(options);
});
});

it('should render plain text slot content inside the styled, click-wired button', async () => {
const signIn = vi.fn().mockResolvedValue(undefined);
const mockContext = createMockThunderIDContext({signIn});

const wrapper = mount(SignInButton, {
global: {
provide: {
[THUNDERID_KEY as symbol]: mockContext,
},
},
slots: {
default: 'Sign In',
},
});

const button = wrapper.find('button');
expect(button.exists()).toBe(true);
expect(button.text()).toBe('Sign In');

await button.trigger('click');
await vi.waitFor(() => {
expect(signIn).toHaveBeenCalled();
});
});

it('should bypass the styled button when asChild is set', async () => {
const signIn = vi.fn().mockResolvedValue(undefined);
const mockContext = createMockThunderIDContext({signIn});

const wrapper = mount(SignInButton, {
global: {
provide: {
[THUNDERID_KEY as symbol]: mockContext,
},
},
props: {
asChild: true,
},
slots: {
default: ({signIn: doSignIn, isLoading}: {signIn: () => void; isLoading: boolean}) =>
h('a', {href: '#', onClick: doSignIn}, isLoading ? 'Signing in…' : 'Sign in'),
},
});

const link = wrapper.find('a');
expect(link.exists()).toBe(true);
expect(wrapper.find('button').exists()).toBe(false);

await link.trigger('click');
await vi.waitFor(() => {
expect(signIn).toHaveBeenCalled();
});
});
});

describe('SignOutButton', () => {
Expand Down
46 changes: 38 additions & 8 deletions packages/vue/src/components/actions/SignInButton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,33 @@ import useThunderID from '../../composables/useThunderID';
*
* If a custom `signInUrl` is configured, navigates to it instead.
* Falls back to i18n translation for the button text.
*
* By default, slot content is rendered *inside* the styled button (matching
* {@link BaseSignInButton}'s convention) — the click handler and styling are
* wired up for you.
*
* @example
* <!-- Default: content is wrapped in the styled, click-wired button -->
* <SignInButton>Sign In</SignInButton>
* <SignInButton v-slot="{ isLoading }">{{ isLoading ? 'Signing in…' : 'Sign in' }}</SignInButton>
*
* @example
* <!-- asChild: full control — you render the element and wire the click yourself -->
* <SignInButton as-child v-slot="{ signIn, isLoading }">
* <button @click="signIn" :disabled="isLoading">Sign In</button>
* </SignInButton>
*/
const SignInButton: Component = defineComponent({
name: 'SignInButton',
props: {
asChild: {default: false, type: Boolean},
signInOptions: {default: undefined, type: Object as PropType<Record<string, any>>},
},
emits: ['click', 'error'],
setup(props: {signInOptions?: Record<string, any>}, {slots, emit, attrs}: SetupContext): () => VNode {
setup(
props: {asChild: boolean; signInOptions?: Record<string, any>},
{slots, emit, attrs}: SetupContext,
): () => VNode {
const {signIn, signInUrl, signInOptions: contextSignInOptions} = useThunderID();
const isLoading: Ref<boolean> = ref(false);

Expand All @@ -55,17 +74,28 @@ const SignInButton: Component = defineComponent({
};

return (): VNode => {
if (slots['default']) {
// asChild: caller renders their own element and wires the click themselves.
if (props.asChild && slots['default']) {
const nodes: VNode[] = slots['default']({isLoading: isLoading.value, signIn: handleSignIn});
return nodes.length === 1 ? nodes[0] : h(Fragment, null, nodes);
}

return h(BaseSignInButton, {
class: attrs.class,
isLoading: isLoading.value,
onClick: handleSignIn,
style: attrs.style,
});
// Default: forward slot content (or fallback text) into the styled,
// click-wired BaseSignInButton — same convention as BaseSignInButton itself.
const slotContent: (() => VNode[]) | undefined = slots['default']
? (): VNode[] => slots['default']!({isLoading: isLoading.value, signIn: handleSignIn})
: undefined;

return h(
BaseSignInButton,
{
class: attrs.class,
isLoading: isLoading.value,
onClick: handleSignIn,
style: attrs.style,
},
slotContent,
);
};
},
});
Expand Down
45 changes: 37 additions & 8 deletions packages/vue/src/components/actions/SignUpButton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,29 @@ import useThunderID from '../../composables/useThunderID';
*
* If a custom `signUpUrl` is configured, navigates to it instead.
* Falls back to i18n translation for the button text.
*
* By default, slot content is rendered *inside* the styled button (matching
* {@link BaseSignUpButton}'s convention) — the click handler and styling are
* wired up for you.
*
* @example
* <!-- Default: content is wrapped in the styled, click-wired button -->
* <SignUpButton>Sign Up</SignUpButton>
* <SignUpButton v-slot="{ isLoading }">{{ isLoading ? 'Signing up…' : 'Sign up' }}</SignUpButton>
*
* @example
* <!-- asChild: full control — you render the element and wire the click yourself -->
* <SignUpButton as-child v-slot="{ signUp, isLoading }">
* <button @click="signUp" :disabled="isLoading">Sign Up</button>
* </SignUpButton>
*/
const SignUpButton: Component = defineComponent({
name: 'SignUpButton',
props: {
asChild: {default: false, type: Boolean},
},
emits: ['click', 'error'],
setup(_: {}, {slots, emit, attrs}: SetupContext): () => VNode {
setup(props: {asChild: boolean}, {slots, emit, attrs}: SetupContext): () => VNode {
const {signUp, signUpUrl} = useThunderID();
const isLoading: Ref<boolean> = ref(false);

Expand All @@ -42,17 +60,28 @@ const SignUpButton: Component = defineComponent({
};

return (): VNode => {
if (slots['default']) {
// asChild: caller renders their own element and wires the click themselves.
if (props.asChild && slots['default']) {
const nodes: VNode[] = slots['default']({isLoading: isLoading.value, signUp: handleSignUp});
return nodes.length === 1 ? nodes[0] : h(Fragment, null, nodes);
}

return h(BaseSignUpButton, {
class: attrs.class,
isLoading: isLoading.value,
onClick: handleSignUp,
style: attrs.style,
});
// Default: forward slot content (or fallback text) into the styled,
// click-wired BaseSignUpButton — same convention as BaseSignUpButton itself.
const slotContent: (() => VNode[]) | undefined = slots['default']
? (): VNode[] => slots['default']!({isLoading: isLoading.value, signUp: handleSignUp})
: undefined;

return h(
BaseSignUpButton,
{
class: attrs.class,
isLoading: isLoading.value,
onClick: handleSignUp,
style: attrs.style,
},
slotContent,
);
};
},
});
Expand Down
4 changes: 2 additions & 2 deletions samples/browser/quickstart/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ A minimal Vite + vanilla JS app demonstrating sign-in and sign-out with the Thun
cp .env.example .env
```

2. Edit `.env` with the credentials you set in `thunderid-config/thunderid.env`:
2. Edit `.env` with your application's credentials:
```
VITE_THUNDERID_CLIENT_ID=BROWSER_QUICKSTART
VITE_THUNDERID_CLIENT_ID=<your-client-id>
VITE_THUNDERID_BASE_URL=https://localhost:8090
```

Expand Down
128 changes: 0 additions & 128 deletions samples/browser/quickstart/thunderid-config/thunderid-config.yaml

This file was deleted.

2 changes: 0 additions & 2 deletions samples/browser/quickstart/thunderid-config/thunderid.env

This file was deleted.

6 changes: 3 additions & 3 deletions samples/express/quickstart/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ Protected routes validate the `Authorization: Bearer <token>` header against Thu
cp .env.example .env
```

2. Edit `.env` with the credentials you set in `thunderid-config/thunderid.env`:
2. Edit `.env` with your application's credentials:
```
THUNDERID_CLIENT_ID=EXPRESS_QUICKSTART
THUNDERID_CLIENT_SECRET=<the EXPRESS_QUICKSTART_CLIENT_SECRET value>
THUNDERID_CLIENT_ID=<your-client-id>
THUNDERID_CLIENT_SECRET=<your-client-secret>
THUNDERID_BASE_URL=https://localhost:8090
```

Expand Down
Loading
Loading