feat: add email sign up
This commit is contained in:
@@ -1,31 +1,25 @@
|
|||||||
<script lang='ts' setup>
|
<script lang='ts' setup>
|
||||||
import { modalController } from "@ionic/vue";
|
import { modalController } from "@ionic/vue";
|
||||||
|
import VerifyCode from "./verify-code.vue";
|
||||||
|
|
||||||
const name = ref();
|
async function closeModal() {
|
||||||
|
await modalController.dismiss();
|
||||||
const cancel = () => modalController.dismiss(null, "cancel");
|
}
|
||||||
const confirm = () => modalController.dismiss(name.value, "confirm");
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<IonHeader>
|
<IonPage>
|
||||||
<IonToolbar>
|
<IonHeader class="ion-no-border">
|
||||||
<IonButtons slot="start">
|
<IonToolbar>
|
||||||
<IonButton color="medium" @click="cancel">
|
<IonButtons slot="start">
|
||||||
Cancel
|
<IonButton @click="closeModal">
|
||||||
</IonButton>
|
Close
|
||||||
</IonButtons>
|
</IonButton>
|
||||||
<IonTitle>Modal</IonTitle>
|
</IonButtons>
|
||||||
<IonButtons slot="end">
|
</IonToolbar>
|
||||||
<IonButton :strong="true" @click="confirm">
|
</IonHeader>
|
||||||
Confirm
|
<IonContent :fullscreen="true" class="ion-padding">
|
||||||
</IonButton>
|
<VerifyCode />
|
||||||
</IonButtons>
|
</IonContent>
|
||||||
</IonToolbar>
|
</IonPage>
|
||||||
</IonHeader>
|
|
||||||
<IonContent class="ion-padding">
|
|
||||||
<IonItem>
|
|
||||||
<IonInput v-model="name" label-placement="stacked" label="Enter your name" placeholder="Your name" />
|
|
||||||
</IonItem>
|
|
||||||
</IonContent>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
139
src/auth/components/verify-code.vue
Normal file
139
src/auth/components/verify-code.vue
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
<script lang='ts' setup>
|
||||||
|
import type { AuthUserSignup } from "../type";
|
||||||
|
import { toastController } from "@ionic/vue";
|
||||||
|
import { logoGoogle, phonePortraitOutline } from "ionicons/icons";
|
||||||
|
import { emailPattern } from "@/utils";
|
||||||
|
import { authClient } from "..";
|
||||||
|
|
||||||
|
const form = ref<AuthUserSignup>({
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
confirmPassword: "",
|
||||||
|
verificationCode: "",
|
||||||
|
});
|
||||||
|
const step = ref(1);
|
||||||
|
|
||||||
|
function onEmailBlur() {
|
||||||
|
const isEmailValid = emailPattern.test(form.value.email);
|
||||||
|
if (!isEmailValid) {
|
||||||
|
toastController
|
||||||
|
.create({
|
||||||
|
message: "Please enter a valid email address.",
|
||||||
|
duration: 1500,
|
||||||
|
position: "bottom",
|
||||||
|
})
|
||||||
|
.then((toast) => {
|
||||||
|
toast.present();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function submitSendVerification() {
|
||||||
|
const isEmailValid = emailPattern.test(form.value.email);
|
||||||
|
if (!isEmailValid) {
|
||||||
|
const toast = await toastController.create({
|
||||||
|
message: "Please enter a valid email address.",
|
||||||
|
duration: 1500,
|
||||||
|
position: "bottom",
|
||||||
|
});
|
||||||
|
|
||||||
|
await toast.present();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error } = await authClient.emailOtp.sendVerificationOtp({
|
||||||
|
email: form.value.email, // required
|
||||||
|
type: "sign-in", // required
|
||||||
|
});
|
||||||
|
if (data?.success) {
|
||||||
|
step.value = 2;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const toast = await toastController.create({
|
||||||
|
message: error?.message || "Failed to send verification code.",
|
||||||
|
duration: 1500,
|
||||||
|
position: "bottom",
|
||||||
|
});
|
||||||
|
|
||||||
|
await toast.present();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
step.value = 1;
|
||||||
|
form.value.email = "";
|
||||||
|
form.value.password = "";
|
||||||
|
form.value.confirmPassword = "";
|
||||||
|
form.value.verificationCode = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitSignup() {
|
||||||
|
const { data, error } = await authClient.signIn.emailOtp({
|
||||||
|
email: form.value.email,
|
||||||
|
otp: form.value.verificationCode,
|
||||||
|
});
|
||||||
|
if (data?.token) {
|
||||||
|
reset();
|
||||||
|
step.value = 1;
|
||||||
|
const toast = await toastController.create({
|
||||||
|
message: "Email verified successfully!",
|
||||||
|
duration: 1500,
|
||||||
|
position: "bottom",
|
||||||
|
});
|
||||||
|
|
||||||
|
await toast.present();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const toast = await toastController.create({
|
||||||
|
message: error?.message || "Failed to verify the code.",
|
||||||
|
duration: 1500,
|
||||||
|
position: "bottom",
|
||||||
|
});
|
||||||
|
|
||||||
|
await toast.present();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="step === 1">
|
||||||
|
<h1><strong>What's your email?</strong></h1>
|
||||||
|
<p>You'll use this email to login and access everything we have to offer.</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<ion-item>
|
||||||
|
<ion-input v-model="form.email" type="email" placeholder="Enter your email address" @ion-blur="onEmailBlur" />
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-button expand="block" class="ion-margin-top" shape="round" @click="submitSendVerification">
|
||||||
|
Sign up
|
||||||
|
</ion-button>
|
||||||
|
|
||||||
|
<divider text="Or continue with" />
|
||||||
|
|
||||||
|
<ion-button color="medium" expand="block" class="ion-margin-top" shape="round">
|
||||||
|
<IonIcon slot="start" aria-hidden="true" :icon="phonePortraitOutline" />
|
||||||
|
Phone Number
|
||||||
|
</ion-button>
|
||||||
|
<ion-button color="medium" expand="block" class="ion-margin-top" shape="round">
|
||||||
|
<IonIcon slot="start" aria-hidden="true" :icon="logoGoogle" />
|
||||||
|
Google
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="step === 2">
|
||||||
|
<h1><strong>Verify your email</strong></h1>
|
||||||
|
<p>We have sent a verification code to {{ form.email }}. Please enter the code below to verify your email address.</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<ion-item>
|
||||||
|
<ion-input-otp v-model="form.verificationCode" :length="6" />
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-button expand="block" class="ion-margin-top" shape="round" @click="submitSignup">
|
||||||
|
Submit
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang='css' scoped></style>
|
||||||
@@ -1,12 +1,19 @@
|
|||||||
import { modalController } from "@ionic/vue";
|
import { modalController } from "@ionic/vue";
|
||||||
|
import { emailOTPClient } from "better-auth/client/plugins";
|
||||||
import { createAuthClient } from "better-auth/vue";
|
import { createAuthClient } from "better-auth/vue";
|
||||||
|
import SignupContent from "./components/signup.vue";
|
||||||
|
|
||||||
export const authClient = createAuthClient({
|
export const authClient = createAuthClient({
|
||||||
baseURL: import.meta.env.VITE_BASE_API_URL, // The base URL of your auth server
|
baseURL: import.meta.env.VITE_BASE_API_URL, // The base URL of your auth server
|
||||||
|
plugins: [emailOTPClient()],
|
||||||
});
|
});
|
||||||
|
|
||||||
export function signupController() {
|
export async function modelControllerSignup(presentingElement?: HTMLElement) {
|
||||||
return modalController.create({
|
const modal = await modalController.create({
|
||||||
component: () => import("./components/signup.vue"),
|
component: SignupContent,
|
||||||
|
presentingElement,
|
||||||
|
canDismiss: async (data, role) => role !== "gesture",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return modal;
|
||||||
}
|
}
|
||||||
|
|||||||
6
src/auth/type.ts
Normal file
6
src/auth/type.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export interface AuthUserSignup {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
confirmPassword: string;
|
||||||
|
verificationCode: string;
|
||||||
|
}
|
||||||
15
src/composables/useAuth.ts
Normal file
15
src/composables/useAuth.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { authClient } from "@/auth";
|
||||||
|
|
||||||
|
export function useAuth() {
|
||||||
|
// Better Auth 提供的 session
|
||||||
|
const session = authClient.useSession();
|
||||||
|
|
||||||
|
const user = computed(() => session.value.data?.user);
|
||||||
|
const isAuthenticated = computed(() => !!session.value.data);
|
||||||
|
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
session,
|
||||||
|
isAuthenticated,
|
||||||
|
};
|
||||||
|
}
|
||||||
2
src/utils/index.ts
Normal file
2
src/utils/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./ionic-helper";
|
||||||
|
export * from "./pattern";
|
||||||
1
src/utils/ionic-helper.ts
Normal file
1
src/utils/ionic-helper.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export type PageInstance = InstanceType<typeof import("@ionic/vue").IonPage>;
|
||||||
1
src/utils/pattern.ts
Normal file
1
src/utils/pattern.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const emailPattern = /^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/;
|
||||||
Reference in New Issue
Block a user