Files
ccn/Services/Website/Sources/Library/Internal/Pages/SignInPage.swift
T

182 lines
6.2 KiB
Swift

import Elementary
import Foundation
import Infrastructure
import Persistence
/// The one passkey screen, which both signs in and joins. Drawn bare, without the site's navigation: its only way out is back.
///
/// The prototype stubs the passkey ceremony — see ``SessionController``.
struct SignInPage {
// MARK: Properties
let analytics: Analytics?
let assetVersion: String?
let locale: Locale
/// What went wrong with the last attempt, or `nil` when nothing did.
let error: String?
/// What the visitor was about to do, or `nil` when they came here directly.
let intent: String?
/// The path the visitor returns to, signed in or not.
let returnPath: String
// MARK: Initializers
/// Creates the sign-in screen.
/// - Parameters:
/// - returnPath: the path the visitor returns to, signed in or not.
/// - intent: what the visitor was about to do, or `nil` (the default) when they came here directly.
/// - error: what went wrong with the last attempt, or `nil` (the default) when nothing did.
/// - assetVersion: the version token appended to the page's asset URLs, or `nil` (the default) to leave them unversioned.
/// - analytics: the analytics tracker embedded in the document head, or `nil` (the default) to omit it.
init(
returnPath: String,
intent: String? = nil,
error: String? = nil,
assetVersion: String? = nil,
analytics: Analytics? = nil
) {
self.analytics = analytics
self.assetVersion = assetVersion
self.error = error
self.intent = intent
self.locale = .init(identifier: "en")
self.returnPath = returnPath
}
}
// MARK: - Page
extension SignInPage: Page {
// MARK: Computed
var content: some HTML {
div(.class("gate")) {
header(.class("gate__bar")) {
a(
.class("gate__close"),
.href(returnPath),
.custom(name: "aria-label", value: "Close and go back")
) {
Icon(.close)
}
span(.class("gate__wordmark")) {
"CCN"
}
}
main(.class("gate__main")) {
div(.class("gate__intro")) {
if let intent {
p(.class("type-eyebrow")) {
intent
}
}
h1(.class("type-h1")) {
"Sign in or join"
}
p(.class("gate__lead")) {
"No password. Your device confirms it's you."
}
}
form(
.class("gate__form"),
.action(.Route.signIn),
.method(.post)
) {
input(
.type(.hidden),
.name("return"),
.value(returnPath)
)
button(
.class("button button--primary button--xl button--block"),
.type(.submit)
) {
Icon(.key, size: 18)
"Continue with passkey"
}
div(.class("field")) {
label(
.class("field__label"),
.for("email")
) {
"New here, or on a new device? Enter your email"
}
input(
.class("input input--lg"),
.id("email"),
.type(.email),
.name("email"),
.placeholder("you@example.com"),
.custom(name: "autocomplete", value: "username webauthn")
)
.attributes(
.custom(name: "aria-invalid", value: "true"),
.custom(name: "aria-describedby", value: "email-error"),
when: error != nil
)
if let error {
p(
.class("field__error"),
.id("email-error")
) {
error
}
}
div(.class("gate__saved")) {
Icon(.key, size: 16)
span {
strong {
AccountRepository.demoEmail
}
span(.class("gate__saved-note")) {
" · saved passkey"
}
}
}
p(.class("field__hint")) {
"Unknown email? We create your account and passkey in the same step — nothing else to fill in."
}
}
}
p(.class("notice notice--warn")) {
"Prototype: passkeys are stubbed. Continue signs you in as the saved demo buyer; an email signs in as that account, or creates it. To sell, use joost@kestrelrow.example."
}
p(.class("gate__recover")) {
a(.href(.Route.recover)) {
"Lost access to your passkeys?"
}
}
}
}
}
/// Kept out of search results: the screen is a step, not a destination.
@HTMLBuilder
var metadata: some HTML {
meta(
.name("robots"),
.content("noindex")
)
siteMetadata
}
var scripts: [any Asset] {
[StaticFile.shared]
}
var stylesheets: [any Asset] {
[StaticFile.shared, StaticFile.signIn]
}
var title: String {
"Sign in or join — CCN"
}
}