Skip to content

Commit 0e776ad

Browse files
committed
feat(experimental): add SocialLoginReversed stage
1 parent 48d0f46 commit 0e776ad

1 file changed

Lines changed: 131 additions & 0 deletions

File tree

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<!--
2+
@component
3+
Type: stage
4+
Name: SocialLoginReversed
5+
6+
Custom login stage that renders username/password before social login buttons,
7+
regardless of the callback order AM sends. Set the "Stage" field on your AM
8+
Page Node to "SocialLoginReversed" to activate this layout.
9+
-->
10+
11+
<script lang="ts">
12+
import { afterUpdate, onMount } from 'svelte';
13+
14+
import Alert from '$components/primitives/alert/alert.svelte';
15+
import Button from '$components/primitives/button/button.svelte';
16+
import Form from '$components/primitives/form/form.svelte';
17+
import { interpolate } from '$core/_utilities/i18n.utilities';
18+
import { styleStore } from '$core/style.store';
19+
import CallbackMapper from '$journey/_utilities/callback-mapper.svelte';
20+
import { captureLinks } from '$journey/stages/_utilities/stage.utilities';
21+
import { convertStringToKey } from '$journey/stages/_utilities/step.utilities';
22+
23+
import type { JourneyStep } from '@forgerock/journey-client/types';
24+
25+
import type { Maybe } from '$core/interfaces';
26+
import type {
27+
CallbackMetadata,
28+
StageFormObject,
29+
StageJourneyObject,
30+
StepMetadata,
31+
} from '$journey/journey.interfaces';
32+
33+
export let componentStyle: 'app' | 'inline' | 'modal';
34+
export let form: StageFormObject;
35+
export let formEl: HTMLFormElement | null = null;
36+
export let journey: StageJourneyObject;
37+
export let metadata: Maybe<{ callbacks: CallbackMetadata[]; step: StepMetadata }>;
38+
export let step: JourneyStep;
39+
40+
let alertNeedsFocus = false;
41+
let formMessageKey = '';
42+
let linkWrapper: HTMLElement;
43+
44+
function determineSubmission() {
45+
if (metadata?.step?.derived.isStepSelfSubmittable()) {
46+
form?.submit();
47+
}
48+
}
49+
50+
afterUpdate(() => {
51+
alertNeedsFocus = !!form?.message;
52+
});
53+
54+
onMount(() => {
55+
if (componentStyle === 'modal') {
56+
captureLinks(linkWrapper, journey);
57+
}
58+
});
59+
60+
$: formMessageKey = convertStringToKey(form?.message);
61+
62+
$: indexedCallbacks = (step?.callbacks ?? []).map((cb, idx) => ({ cb, idx }));
63+
$: credentialEntries = indexedCallbacks.filter((e) => e.cb.payload?.type !== 'SelectIdPCallback');
64+
$: socialEntries = indexedCallbacks.filter((e) => e.cb.payload?.type === 'SelectIdPCallback');
65+
</script>
66+
67+
<Form bind:formEl ariaDescribedBy="socialLastFailureAlert" onSubmitWhenValid={form?.submit}>
68+
{#if form?.message}
69+
<Alert id="socialLastFailureAlert" needsFocus={alertNeedsFocus} type="error">
70+
{interpolate(formMessageKey, null, form?.message)}
71+
</Alert>
72+
{/if}
73+
74+
{#each credentialEntries as { cb, idx }}
75+
<CallbackMapper
76+
props={{
77+
callback: cb,
78+
callbackMetadata: metadata?.callbacks[idx],
79+
selfSubmitFunction: determineSubmission,
80+
stepMetadata: metadata?.step && { ...metadata.step },
81+
style: $styleStore,
82+
}}
83+
/>
84+
{/each}
85+
86+
{#if metadata?.step?.derived.isUserInputOptional || !metadata?.step?.derived.isStepSelfSubmittable()}
87+
<Button busy={journey?.loading} style="primary" type="submit" width="full">
88+
{interpolate('next', null, 'Next')}
89+
</Button>
90+
{/if}
91+
92+
{#if socialEntries.length > 0}
93+
<div class="divider">
94+
<span>{interpolate('orContinueWith', null, 'or continue with')}</span>
95+
</div>
96+
97+
{#each socialEntries as { cb, idx }}
98+
<CallbackMapper
99+
props={{
100+
callback: cb,
101+
callbackMetadata: metadata?.callbacks[idx],
102+
selfSubmitFunction: determineSubmission,
103+
stepMetadata: metadata?.step && { ...metadata.step },
104+
style: $styleStore,
105+
}}
106+
/>
107+
{/each}
108+
{/if}
109+
110+
{#if componentStyle !== 'inline'}
111+
<div bind:this={linkWrapper}></div>
112+
{/if}
113+
</Form>
114+
115+
<style>
116+
.divider {
117+
display: flex;
118+
align-items: center;
119+
gap: 0.75rem;
120+
margin: 0.5rem 0;
121+
color: var(--color-secondary-dark, #6b7280);
122+
font-size: 0.875rem;
123+
}
124+
125+
.divider::before,
126+
.divider::after {
127+
content: '';
128+
flex: 1;
129+
border-top: 1px solid var(--color-border, #e5e7eb);
130+
}
131+
</style>

0 commit comments

Comments
 (0)