<template> <div v-if="instance.disableRegistration" style="margin-bottom: 1rem"> <p>{{ i18n.ts.signupsDisabled }}</p> <a href="https://fedidb.org/software/iceshrimp"> <MkButton rounded gradate >{{ i18n.ts.findOtherInstance }} </MkButton> </a> </div> <form class="qlvuhzng _formRoot" autocomplete="new-password" @submit.prevent="onSubmit" > <MkInput v-if="instance.disableRegistration" v-model="invitationCode" class="_formBlock" type="text" :spellcheck="false" required data-cy-signup-invitation-code @update:modelValue="onChangeInvitationCode" > <template #label>{{ i18n.ts.invitationCode }}</template> <template #prefix><i class="ph-key ph-bold ph-lg"></i></template> </MkInput> <div v-if=" !instance.disableRegistration || (instance.disableRegistration && invitationState === 'entered') " > <MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-signup-username @update:modelValue="onChangeUsername" > <template #label >{{ i18n.ts.username }} <div v-tooltip:dialog="i18n.ts.usernameInfo" class="_button _help" > <i class="ph-question ph-bold"></i></div ></template> <template #prefix>@</template> <template #suffix>@{{ host }}</template> <template #caption> <span v-if="usernameState === 'wait'" style="color: #6e6a86" ><i class="ph-circle-notch ph-bold ph-lg fa-pulse ph-fw ph-lg" ></i> {{ i18n.ts.checking }}</span > <span v-else-if="usernameState === 'ok'" style="color: var(--success)" ><i class="ph-check ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.available }}</span > <span v-else-if="usernameState === 'unavailable'" style="color: var(--error)" ><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.unavailable }}</span > <span v-else-if="usernameState === 'error'" style="color: var(--error)" ><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.error }}</span > <span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)" ><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.usernameInvalidFormat }}</span > <span v-else-if="usernameState === 'min-range'" style="color: var(--error)" ><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.tooShort }}</span > <span v-else-if="usernameState === 'max-range'" style="color: var(--error)" ><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.tooLong }}</span > </template> </MkInput> <MkInput v-if="instance.emailRequiredForSignup" v-model="email" class="_formBlock" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail" > <template #label >{{ i18n.ts.emailAddress }} <div v-tooltip:dialog="i18n.ts._signup.emailAddressInfo" class="_button _help" > <i class="ph-question ph-bold"></i></div ></template> <template #prefix ><i class="ph-envelope-simple-open ph-bold ph-lg"></i ></template> <template #caption> <span v-if="emailState === 'wait'" style="color: #6e6a86" ><i class="ph-circle-notch ph-bold ph-lg fa-pulse ph-fw ph-lg" ></i> {{ i18n.ts.checking }}</span > <span v-else-if="emailState === 'ok'" style="color: var(--success)" ><i class="ph-check ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.available }}</span > <span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)" ><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts._emailUnavailable.used }}</span > <span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)" ><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts._emailUnavailable.format }}</span > <span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)" ><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts._emailUnavailable.disposable }}</span > <span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)" ><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts._emailUnavailable.mx }}</span > <span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)" ><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts._emailUnavailable.smtp }}</span > <span v-else-if="emailState === 'unavailable'" style="color: var(--error)" ><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.unavailable }}</span > <span v-else-if="emailState === 'error'" style="color: var(--error)" ><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.error }}</span > </template> </MkInput> <MkInput v-model="password" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password @update:modelValue="onChangePassword" > <template #label>{{ i18n.ts.password }}</template> <template #prefix ><i class="ph-lock ph-bold ph-lg"></i ></template> <template #caption> <span v-if="passwordStrength == 'low'" style="color: var(--error)" ><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.weakPassword }}</span > <span v-if="passwordStrength == 'medium'" style="color: var(--warn)" ><i class="ph-check ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.normalPassword }}</span > <span v-if="passwordStrength == 'high'" style="color: var(--success)" ><i class="ph-check ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.strongPassword }}</span > </template> </MkInput> <MkInput v-model="retypedPassword" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype" > <template #label >{{ i18n.ts.password }} ({{ i18n.ts.retype }})</template > <template #prefix ><i class="ph-lock ph-bold ph-lg"></i ></template> <template #caption> <span v-if="passwordRetypeState == 'match'" style="color: var(--success)" ><i class="ph-check ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.passwordMatched }}</span > <span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)" ><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.passwordNotMatched }}</span > </template> </MkInput> <MkSwitch v-if="instance.tosUrl" v-model="ToSAgreement" class="_formBlock tou" > <I18n :src="i18n.ts.agreeTo"> <template #0> <a :href="instance.tosUrl" class="_link" target="_blank" >{{ i18n.ts.tos }}</a > </template> </I18n> </MkSwitch> <MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey" /> <MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey" /> <MkButton class="_formBlock" type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit >{{ i18n.ts.start }}</MkButton > </div> </form> </template> <script lang="ts" setup> import {} from "vue"; import getPasswordStrength from "syuilo-password-strength"; import { toUnicode } from "punycode/"; import MkButton from "./MkButton.vue"; import MkInput from "./form/input.vue"; import MkSwitch from "./form/switch.vue"; import MkCaptcha from "@/components/MkCaptcha.vue"; import * as config from "@/config"; import * as os from "@/os"; import { login } from "@/account"; import { instance } from "@/instance"; import { i18n } from "@/i18n"; const props = withDefaults( defineProps<{ autoSet?: boolean; }>(), { autoSet: false, }, ); const emit = defineEmits<{ (ev: "signup", user: Record<string, any>): void; (ev: "signupEmailPending"): void; }>(); const host = toUnicode(config.host); let hcaptcha = $ref(); let recaptcha = $ref(); let username: string = $ref(""); let password: string = $ref(""); let retypedPassword: string = $ref(""); let invitationCode: string = $ref(""); let email = $ref(""); let usernameState: | null | "wait" | "ok" | "unavailable" | "error" | "invalid-format" | "min-range" | "max-range" = $ref(null); let invitationState: null | "entered" = $ref(null); let emailState: | null | "wait" | "ok" | "unavailable:used" | "unavailable:format" | "unavailable:disposable" | "unavailable:mx" | "unavailable:smtp" | "unavailable" | "error" = $ref(null); let passwordStrength: "" | "low" | "medium" | "high" = $ref(""); let passwordRetypeState: null | "match" | "not-match" = $ref(null); let submitting: boolean = $ref(false); let ToSAgreement: boolean = $ref(false); let hCaptchaResponse = $ref(null); let reCaptchaResponse = $ref(null); const shouldDisableSubmitting = $computed((): boolean => { return ( submitting || (instance.tosUrl && !ToSAgreement) || (instance.enableHcaptcha && !hCaptchaResponse) || (instance.enableRecaptcha && !reCaptchaResponse) || passwordRetypeState === "not-match" ); }); function onChangeInvitationCode(): void { if (invitationCode === "") { invitationState = null; return; } invitationState = "entered"; } function onChangeUsername(): void { if (username === "") { usernameState = null; return; } { const err = !username.match(/^[a-zA-Z0-9_]+$/) ? "invalid-format" : username.length < 1 ? "min-range" : username.length > 20 ? "max-range" : null; if (err) { usernameState = err; return; } } usernameState = "wait"; os.api("username/available", { username, }) .then((result) => { usernameState = result.available ? "ok" : "unavailable"; }) .catch(() => { usernameState = "error"; }); } function onChangeEmail(): void { if (email === "") { emailState = null; return; } emailState = "wait"; os.api("email-address/available", { emailAddress: email, }) .then((result) => { emailState = result.available ? "ok" : result.reason === "used" ? "unavailable:used" : result.reason === "format" ? "unavailable:format" : result.reason === "disposable" ? "unavailable:disposable" : result.reason === "mx" ? "unavailable:mx" : result.reason === "smtp" ? "unavailable:smtp" : "unavailable"; }) .catch(() => { emailState = "error"; }); } function onChangePassword(): void { if (password === "") { passwordStrength = ""; return; } const strength = getPasswordStrength(password); passwordStrength = strength > 0.7 ? "high" : strength > 0.3 ? "medium" : "low"; } function onChangePasswordRetype(): void { if (retypedPassword === "") { passwordRetypeState = null; return; } passwordRetypeState = password === retypedPassword ? "match" : "not-match"; } function onSubmit(): void { if (submitting) return; submitting = true; os.api("signup", { username, password, emailAddress: email, invitationCode, "hcaptcha-response": hCaptchaResponse, "g-recaptcha-response": reCaptchaResponse, }) .then(() => { if (instance.emailRequiredForSignup) { os.alert({ type: "success", title: i18n.ts._signup.almostThere, text: i18n.t("_signup.emailSent", { email }), }); emit("signupEmailPending"); } else { os.api("signin", { username, password, }).then((res) => { emit("signup", res); if (props.autoSet) { login(res.i); } }); } }) .catch(() => { submitting = false; hcaptcha.reset?.(); recaptcha.reset?.(); os.alert({ type: "error", text: i18n.ts.somethingHappened, }); }); } </script> <style lang="scss" scoped> .qlvuhzng { .captcha { margin: 16px 0; } } </style>