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
199 changes: 133 additions & 66 deletions frontend/components/SignatureForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ class SignatureForm extends Component {
this.state = {
loading: false,
passkeyAvailable: false,
usePasskey: false, // disabled by default; enabled only when ?passkey=true in URL
usePasskey: false,
emailOptional: false, // true when passkey is available and user hasn't switched to email
form: {
name: null,
occupation: null,
city: null,
organization: null,
email: null,
share_email: false,
id: this.props.signature && this.props.signature.id,
Expand All @@ -37,11 +39,10 @@ class SignatureForm extends Component {
}

async componentDidMount() {
// Only enable passkey when ?passkey=true is in the URL
const params = new URLSearchParams(window.location.search);
if (params.get('passkey') === 'true' && isPasskeySupported()) {
const available = await isPlatformAuthenticatorAvailable();
this.setState({ passkeyAvailable: available, usePasskey: available });
this.setState({ passkeyAvailable: available, usePasskey: available, emailOptional: available });
}
}

Expand All @@ -62,7 +63,6 @@ class SignatureForm extends Component {

await this.props.onSubmit(formData);

// just in case
setTimeout(() => {
this.setState({ loading: false });
}, 2000);
Expand All @@ -73,9 +73,12 @@ class SignatureForm extends Component {

render() {
const { error, t, letter } = this.props;
const { passkeyAvailable, usePasskey } = this.state;
const showEmailField = !this.updatingSignature && !usePasskey;
const showEmailOptional = !this.updatingSignature && usePasskey && passkeyAvailable;
const { passkeyAvailable, usePasskey, emailOptional } = this.state;

// When ?passkey=true and passkey is available, show the redesigned flow
const showPasskeyFlow = passkeyAvailable && !this.updatingSignature;
// Fallback: current interface when passkey is NOT available
const showFallbackFlow = !passkeyAvailable && !this.updatingSignature;

return (
<form onSubmit={this.handleSubmit}>
Expand Down Expand Up @@ -117,76 +120,140 @@ class SignatureForm extends Component {
/>
</div>

{/* Passkey toggle */}
{passkeyAvailable && !this.updatingSignature && (
<div className="w-full py-2">
<div className="flex items-center gap-2 text-sm">
{/* ── Redesigned passkey flow ───────────────────────────────── */}
{showPasskeyFlow && (
<>
{/* Email: optional (with subtext) or required (with checkbox) */}
{emailOptional ? (
// Passkey-first mode: email optional + subtext
<div className="w-full py-1">
<Input type="email" name="email" placeholder={t('sign.email')} onChange={this.handleChange} />
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{t('sign.email.optional.subtext')}
</p>
</div>
) : (
// Email mode: required + checkbox opt-in
<div className="w-full py-1">
<Input
type="email"
name="email"
placeholder={t('sign.email')}
onChange={this.handleChange}
required
/>
{letter.user_id && (
<div className="my-2">
<Label>
<div className="mt-1 mr-0">
<Checkbox
id="share_email"
name="share_email"
onChange={(e) => this.handleChange('share_email', e.target.checked)}
/>
</div>
<label className="ml-1 text-sm text-gray-600 dark:text-gray-300">
{t('sign.share_email')}
</label>
</Label>
</div>
)}
</div>
)}

{/* Primary button */}
<div className="mt-4 mb-2">
<button
type="button"
onClick={() => this.setState({ usePasskey: true })}
className={`px-3 py-1.5 rounded-lg border transition-colors ${
usePasskey
? 'bg-gray-900 text-white border-gray-900 dark:bg-white dark:text-black dark:border-white'
: 'bg-transparent text-gray-500 border-gray-300 dark:border-gray-600'
}`}
type="submit"
className="text-white text-base font-sans border-white bg-gray-900 p-3 rounded-lg w-full disabled:bg-gray-500 dark:bg-black dark:text-white dark:border-white border-2 font-bold"
disabled={this.state.loading}
>
🔐 {t('sign.passkey')}
{t('sign.button.passkey')}
</button>
</div>

{/* Toggle link */}
<div className="text-center mb-6">
{emailOptional ? (
<button
type="button"
className="text-sm text-gray-500 underline hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
onClick={() => this.setState({ usePasskey: false, emailOptional: false })}
>
{t('sign.email.instead')}
</button>
) : (
<button
type="button"
className="text-sm text-gray-500 underline hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
onClick={() => this.setState({ usePasskey: true, emailOptional: true })}
>
{t('sign.passkey.instead')}
</button>
)}
</div>
</>
)}

{/* ── Fallback: standard interface when passkey not available ── */}
{showFallbackFlow && (
<>
<div className="w-full py-1">
<Input
type="email"
name="email"
placeholder={t('sign.email')}
onChange={this.handleChange}
required
/>
</div>
{letter.user_id && (
<div className="my-1">
<Label>
<div className="mt-1 mr-0">
<Checkbox
id="share_email"
name="share_email"
onChange={(e) => this.handleChange('share_email', e.target.checked)}
/>
</div>
<label className="ml-1">{t('sign.share_email')}</label>
</Label>
</div>
)}
<div className="mt-4 mb-6">
<button
type="button"
onClick={() => this.setState({ usePasskey: false })}
className={`px-3 py-1.5 rounded-lg border transition-colors ${
!usePasskey
? 'bg-gray-900 text-white border-gray-900 dark:bg-white dark:text-black dark:border-white'
: 'bg-transparent text-gray-500 border-gray-300 dark:border-gray-600'
}`}
className="text-white text-base font-sans border-white bg-gray-900 p-3 rounded-lg w-full disabled:bg-gray-500 dark:bg-black dark:text-white dark:border-white border-2 font-bold"
disabled={this.state.loading}
>
✉️ {t('sign.email')}
{this.props.signature ? t('sign.update') : t('sign.button')}
</button>
</div>
{usePasskey && (
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{t('sign.passkey.info')}
</p>
)}
</div>
)}

{/* Email field: required for email flow, optional for passkey (for updates) */}
{showEmailField && (
<div className="w-full py-1">
<Input type="email" name="email" placeholder={t('sign.email')} onChange={this.handleChange} required />
</div>
)}
{showEmailOptional && (
<div className="w-full py-1">
<Input type="email" name="email" placeholder={t('sign.email.optional')} onChange={this.handleChange} />
</div>
</>
)}

{!this.updatingSignature && !usePasskey && letter.user_id && (
<div className="my-1">
<Label>
<div className="mt-1 mr-0">
<Checkbox
id="share_email"
name="share_email"
onChange={(e) => this.handleChange('share_email', e.target.checked)}
/>
</div>
<label className="ml-1">{t('sign.share_email')}</label>
</Label>
</div>
{/* ── Existing signature update (no passkey flow) ── */}
{this.updatingSignature && (
<>
<div className="w-full py-1">
<Input
type="email"
name="email"
placeholder={t('sign.email')}
onChange={this.handleChange}
/>
</div>
<div className="mt-4 mb-6">
<button
className="text-white text-base font-sans border-white bg-gray-900 p-3 rounded-lg w-full disabled:bg-gray-500 dark:bg-black dark:text-white dark:border-white border-2 font-bold"
disabled={this.state.loading}
>
{t('sign.update')}
</button>
</div>
</>
)}
</div>
<div className="mt-4 mb-6">
<button
className="text-white text-base font-sans border-white bg-gray-900 p-3 rounded-lg w-full disabled:bg-gray-500 dark:bg-black dark:text-white dark:border-white border-2 font-bold"
disabled={this.state.loading}
>
{this.props.signature ? t('sign.update') : usePasskey ? t('sign.button.passkey') : t('sign.button')}
</button>
</div>
{error && <div className="text-red font-bold text-center m-4">{error.message}</div>}
</form>
);
Expand Down
38 changes: 37 additions & 1 deletion frontend/pages/_app.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,51 @@ export const GlobalStyle = createGlobalStyle`
}
`;

const GIT_SHA = process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA || process.env.GIT_SHA || 'unknown';
const GIT_MSG = process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_MESSAGE || process.env.GIT_MSG || '';

function DevBanner() {
const [visible, setVisible] = React.useState(false);
React.useEffect(() => {
const params = new URLSearchParams(window.location.search);
setVisible(params.get('devmode') === 'true');
document.body.style.paddingTop = params.get('devmode') === 'true' ? '36px' : '';
}, []);

if (!visible) return null;

const sha = GIT_SHA.slice(0, 8);
return (
<div
style={{
background: '#ff6b6b',
color: '#fff',
textAlign: 'center',
padding: '6px 12px',
fontSize: '13px',
fontFamily: 'monospace',
fontWeight: 'bold',
position: 'fixed',
top: 0,
left: 0,
right: 0,
zIndex: 9999,
}}
>
🔧 devmode: {sha} {GIT_MSG && `— ${GIT_MSG}`}
</div>
);
}

function MyApp({ Component, pageProps }) {
const router = useRouter();
const locale = router.locale;
const messages = getMessagesForLocale(locale);
// Use currentLocale as needed
return (
<IntlContext.Provider value={{ locale, messages }}>
<ThemeProvider theme={theme}>
<GlobalStyle />
<DevBanner />
<Component {...pageProps} />
</ThemeProvider>
</IntlContext.Provider>
Expand Down
3 changes: 3 additions & 0 deletions frontend/public/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@
"sign.city": "Stadt",
"sign.email": "E-Mail",
"sign.email.optional": "E-Mail (optional, für Updates)",
"sign.email.optional.subtext": "um Updates zu diesem Brief zu erhalten",
"sign.email.instead": "stattdessen mit Ihrer E-Mail anmelden",
"sign.passkey.instead": "stattdessen mit Passkey anmelden",
"sign.name": "Ihr Name",
"sign.occupation": "Beruf",
"sign.organization": "Organisation",
Expand Down
3 changes: 3 additions & 0 deletions frontend/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@
"sign.city": "city",
"sign.email": "email",
"sign.email.optional": "email (optional, for updates)",
"sign.email.optional.subtext": "to receive updates about this letter",
"sign.email.instead": "sign with your email instead",
"sign.passkey.instead": "sign with passkey instead",
"sign.name": "Your name",
"sign.occupation": "occupation",
"sign.organization": "organization",
Expand Down
3 changes: 3 additions & 0 deletions frontend/public/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@
"sign.city": "ville",
"sign.email": "email",
"sign.email.optional": "email (optionnel, pour les mises à jour)",
"sign.email.optional.subtext": "pour recevoir des mises à jour concernant cette lettre",
"sign.email.instead": "signer avec votre email à la place",
"sign.passkey.instead": "signer avec passkey à la place",
"sign.name": "Votre nom",
"sign.occupation": "occupation",
"sign.organization": "organisation",
Expand Down
3 changes: 3 additions & 0 deletions frontend/public/locales/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@
"sign.city": "stad",
"sign.email": "email",
"sign.email.optional": "email (optioneel, voor updates)",
"sign.email.optional.subtext": "om updates te ontvangen over deze brief",
"sign.email.instead": "in plaats daarvan aanmelden met je email",
"sign.passkey.instead": "in plaats daarvan aanmelden met passkey",
"sign.name": "Uw naam",
"sign.occupation": "beroep",
"sign.organization": "organisatie",
Expand Down