Skip to content
Merged
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: 199 additions & 0 deletions src/components/OTPInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
<script lang="ts" setup>
import useArcanaAuth from "@/use/arcanaAuth";
import useLoaderStore from "@/stores/loader";
import { computed, defineAsyncComponent, ref } from "vue";

const AppOverlay = defineAsyncComponent(
() => import("@/components/overlay.vue")
);

const emit = defineEmits<{
(e: "dismiss"): void;
(e: "resend"): void;
(e: "success"): void;
}>();

const otpLength = 6;
const attemptsUsed = ref(0);
const totalAttemptsAllowed = 3;
const resendCounter = ref(30);

const otp = ref("");
const otpInputs = ref<HTMLInputElement[]>([]);
const arcanaAuth = useArcanaAuth();
const loader = useLoaderStore();
const hasOTPError = ref(false);
startOTPCounterTimer();

function startOTPCounterTimer() {
resendCounter.value = 30;
const interval = setInterval(() => {
--resendCounter.value;
if (resendCounter.value === 0) {
clearInterval(interval);
}
}, 1000);
}

function handlePaste(event: ClipboardEvent) {
hasOTPError.value = false;
if (!event.clipboardData) return;
const paste = event.clipboardData.getData("text/plain");
if (paste.startsWith("0x") || isNaN(Number(paste))) return;
if (paste.length === otpLength) {
otp.value = paste;
}
}

function focusInput(index: number) {
otpInputs.value[index]?.focus();
}

const isValidOTP = computed(
() => otp.value.length === otpLength && !isNaN(Number(otp.value))
);

function handleKeyDown(event: KeyboardEvent, index: number) {
hasOTPError.value = false;
if ([event.code, event.key].includes("Backspace")) {
event.preventDefault();
otp.value = otp.value.slice(0, index) + otp.value.slice(index + 1);
focusInput(index - 1);
} else if (event.code === "Delete") {
event.preventDefault();
otp.value = otp.value.slice(0, index) + otp.value.slice(index + 1);
} else if (event.code === "ArrowLeft") {
event.preventDefault();
if (index !== 0) focusInput(index - 1);
} else if (event.code === "ArrowRight") {
event.preventDefault();
if (index !== 5) focusInput(index + 1);
} else if (
event.code === "Spacebar" ||
event.code === "Space" ||
event.code === "ArrowUp" ||
event.code === "ArrowDown"
) {
event.preventDefault();
} else if (!isNaN(Number(event.key))) {
event.preventDefault();
otp.value =
otp.value.slice(0, index) + event.key + otp.value.slice(index + 1);
focusInput(index + 1);
}
}

async function submitOTP() {
hasOTPError.value = false;
try {
loader.showLoader("Verifying the OTP...");
console.log(arcanaAuth.getAuthInstance());
await arcanaAuth.getAuthInstance().loginWithOTPComplete(otp.value);
emit("success");
emit("dismiss");
} catch (error) {
console.error(error);
hasOTPError.value = true;
} finally {
loader.hideLoader();
}
}

async function resendOTP() {
emit("resend");
startOTPCounterTimer();
}
</script>

<template>
<AppOverlay>
<div
class="max-w-[360px] w-screen bg-eerie-black rounded-[10px] border-1 border-jet flex flex-col relative p-[1.75rem] gap-4"
role="dialog"
>
<button class="absolute right-4 top-4" @click="emit('dismiss')">
<img src="@/assets/images/close.svg" alt="Close" />
</button>
<div class="flex flex-col items-center justify-center gap-3 text-center">
<h2 class="text-[2rem] font-bold">Verification</h2>
<span class="text-sm text-secondary-400"
>Please enter the OTP that was sent to your email address</span
>
</div>
<form
class="relative isolate flex w-full flex-col gap-5"
@submit.prevent="submitOTP"
>
<div
class="relative z-10 flex gap-2 w-full items-center justify-between"
>
<input
v-for="i in otpLength"
v-model="otp[i - 1]"
:key="`otp-input-${i}`"
type="number"
step="1"
min="0"
max="9"
pattern="\d*"
maxlength="1"
autocomplete="off"
ref="otpInputs"
@input="void 0"
@keydown="handleKeyDown($event, i - 1)"
@paste.prevent="handlePaste"
class="input flex flex-grow justify-center items-center text-center"
/>
</div>
<span
:class="[
'relative z-0 -mt-3 ml-2 text-start text-xs text-tertiary-500 transition-all duration-200 ease-in-out',
hasOTPError
? 'translate-y-0 opacity-100'
: '-translate-y-8 opacity-0',
]"
>{{
attemptsUsed === totalAttemptsAllowed
? "The OTP attempts have been exceeded."
: "The entered OTP is either invalid or already used."
}}</span
>
<button
:disabled="!isValidOTP"
:title="!isValidOTP ? 'Please enter a valid OTP' : ''"
class="btn btn-submit disabled:opacity-50 disabled:cursor-not-allowed"
>
Verify OTP
</button>
<div class="flex items-center justify-center">
<button
class="text-sm font-bold"
v-if="resendCounter === 0"
type="button"
@click.stop="resendOTP"
>
Resend OTP
</button>
<span class="text-sm" v-else>{{
`Resend OTP in ${resendCounter} seconds`
}}</span>
</div>
</form>
</div>
</AppOverlay>
</template>

<style scoped>
/* Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}

/* Firefox */
input[type="number"] {
-moz-appearance: textfield;
appearance: textfield;
}
</style>
30 changes: 20 additions & 10 deletions src/pages/login.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import useWalletConnect from "@/use/walletconnect";
import type { GetAccountResult, PublicClient } from "@wagmi/core";
import { normaliseEmail } from "@/utils/normalise";
import useOkxWallet from "@/use/okxwallet";
import OTPInput from "@/components/OTPInput.vue";

const arcanaAuth = useArcanaAuth();
const authStore = useAuthStore();
Expand All @@ -24,6 +25,7 @@ const passwordlessEmailId = ref("");
const toast = useToast();
const walletConnect = useWalletConnect();
const okxWallet = useOkxWallet();
const showOTPInput = ref(false);

const query = route.query;
const verifier = query.verifier as string;
Expand Down Expand Up @@ -74,22 +76,24 @@ async function socialLogin(type: string) {

async function passwordlessLogin() {
try {
loaderStore.showLoader(
`Click on the verification mail sent to ${passwordlessEmailId.value}...`
showOTPInput.value = true;
const authInstance = arcanaAuth.getAuthInstance();
const loginState = await authInstance.loginWithOTPStart(
normaliseEmail(passwordlessEmailId.value)
);
await arcanaAuth
.getAuthInstance()
.loginWithLink(normaliseEmail(passwordlessEmailId.value));
authStore.provider = arcanaAuth.getProvider();
authStore.isLoggedIn = true;
authStore.loggedInWith = "";
await loginState.begin();
} catch (e: any) {
toast.error(e);
} finally {
loaderStore.hideLoader();
showOTPInput.value = false;
}
}

async function passwordlessLoginSuccess() {
authStore.provider = arcanaAuth.getProvider();
authStore.isLoggedIn = true;
authStore.loggedInWith = "";
}

async function onConnectToWalletConnect() {
const accountDetails = walletConnect.getAccount();
const isConnected = accountDetails.isConnected;
Expand Down Expand Up @@ -290,5 +294,11 @@ async function onConnectToOkxWallet() {
</div>
<LandingDescription class="flex-grow" />
</div>
<OTPInput
v-if="showOTPInput"
@dismiss="showOTPInput = false"
@resend="passwordlessLogin"
@success="passwordlessLoginSuccess"
/>
</div>
</template>