|
|
@@ -60,42 +60,6 @@
|
|
|
key="flow"
|
|
|
class="flow-screen"
|
|
|
>
|
|
|
- <div class="flow-header items-center row">
|
|
|
- <q-btn
|
|
|
- v-if="steps >= 4"
|
|
|
- class="flow-back-btn"
|
|
|
- color="primary"
|
|
|
- dense
|
|
|
- flat
|
|
|
- icon="mdi-arrow-left"
|
|
|
- round
|
|
|
- @click="goBack"
|
|
|
- />
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="flow-logo q-my-xl">
|
|
|
- <q-img
|
|
|
- :src="LogoDiariaCampos"
|
|
|
- style="max-width: 180px"
|
|
|
- />
|
|
|
- </div>
|
|
|
-
|
|
|
- <div
|
|
|
- v-if="steps === 3"
|
|
|
- class="register-later-row"
|
|
|
- >
|
|
|
- <q-btn
|
|
|
- align="right"
|
|
|
- class="register-later-btn"
|
|
|
- color="primary"
|
|
|
- flat
|
|
|
- icon-right="mdi-chevron-right-circle-outline"
|
|
|
- no-caps
|
|
|
- :label="$t('auth.register_later')"
|
|
|
- @click="goToLocationStep"
|
|
|
- />
|
|
|
- </div>
|
|
|
-
|
|
|
<q-form
|
|
|
ref="loginForm"
|
|
|
autocapitalize="off"
|
|
|
@@ -104,33 +68,81 @@
|
|
|
class="flow-form"
|
|
|
spellcheck="false"
|
|
|
@submit="onSubmit"
|
|
|
+ @validation-error="scrollToInvalidField"
|
|
|
>
|
|
|
- <div
|
|
|
- class="flow-content"
|
|
|
- :class="{ 'flow-content--centered': steps < 3, 'flow-content--email-only': steps === 1 && !smsEnabled }"
|
|
|
+ <q-scroll-area
|
|
|
+ ref="flowScrollArea"
|
|
|
+ class="flow-scroll"
|
|
|
>
|
|
|
- <LoginStep1Panel
|
|
|
- v-if="steps === 1"
|
|
|
- v-model:email="email"
|
|
|
- v-model:phone="phone"
|
|
|
- :sms-enabled="smsEnabled"
|
|
|
- />
|
|
|
-
|
|
|
- <LoginStep2Panel
|
|
|
- v-else-if="steps === 2"
|
|
|
- v-model:code="code"
|
|
|
- />
|
|
|
-
|
|
|
- <LoginStep3Panel
|
|
|
- v-else-if="steps === 3"
|
|
|
- v-model="stepThreeForm"
|
|
|
- />
|
|
|
-
|
|
|
- <LoginStepAvatarPanel
|
|
|
- v-else-if="steps === 4"
|
|
|
- v-model="stepAvatarForm"
|
|
|
- />
|
|
|
- </div>
|
|
|
+ <div class="flow-screen-content">
|
|
|
+ <div class="flow-header items-center row">
|
|
|
+ <q-btn
|
|
|
+ v-if="steps >= 4"
|
|
|
+ class="flow-back-btn"
|
|
|
+ color="primary"
|
|
|
+ dense
|
|
|
+ flat
|
|
|
+ icon="mdi-arrow-left"
|
|
|
+ round
|
|
|
+ @click="goBack"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="flow-logo q-my-xl">
|
|
|
+ <q-img
|
|
|
+ :src="LogoDiariaCampos"
|
|
|
+ style="max-width: 180px"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div
|
|
|
+ v-if="steps === 3"
|
|
|
+ class="register-later-row"
|
|
|
+ >
|
|
|
+ <q-btn
|
|
|
+ align="right"
|
|
|
+ class="register-later-btn"
|
|
|
+ color="primary"
|
|
|
+ flat
|
|
|
+ icon-right="mdi-chevron-right-circle-outline"
|
|
|
+ no-caps
|
|
|
+ :label="$t('auth.register_later')"
|
|
|
+ @click="goToLocationStep"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div
|
|
|
+ ref="flowContent"
|
|
|
+ class="flow-content"
|
|
|
+ :class="{ 'flow-content--centered': steps < 3, 'flow-content--email-only': steps === 1 && !smsEnabled }"
|
|
|
+ >
|
|
|
+ <LoginStep1Panel
|
|
|
+ v-if="steps === 1"
|
|
|
+ v-model:email="email"
|
|
|
+ v-model:phone="phone"
|
|
|
+ :server-errors="serverErrors"
|
|
|
+ :sms-enabled="smsEnabled"
|
|
|
+ />
|
|
|
+
|
|
|
+ <LoginStep2Panel
|
|
|
+ v-else-if="steps === 2"
|
|
|
+ v-model:code="code"
|
|
|
+ :server-errors="serverErrors"
|
|
|
+ />
|
|
|
+
|
|
|
+ <LoginStep3Panel
|
|
|
+ v-else-if="steps === 3"
|
|
|
+ v-model="stepThreeForm"
|
|
|
+ :server-errors="serverErrors"
|
|
|
+ />
|
|
|
+
|
|
|
+ <LoginStepAvatarPanel
|
|
|
+ v-else-if="steps === 4"
|
|
|
+ v-model="stepAvatarForm"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </q-scroll-area>
|
|
|
|
|
|
<div class="flow-footer">
|
|
|
<q-btn
|
|
|
@@ -140,7 +152,7 @@
|
|
|
rounded
|
|
|
type="submit"
|
|
|
:label="$t('auth.continue')"
|
|
|
- :loading="submitting"
|
|
|
+ :loading="loading"
|
|
|
>
|
|
|
<template #loading>
|
|
|
<q-spinner />
|
|
|
@@ -167,12 +179,14 @@ import {
|
|
|
} from "src/api/user";
|
|
|
|
|
|
import { getServicesConfig } from "src/api/services";
|
|
|
-import { onMounted, ref } from "vue";
|
|
|
+import { nextTick, onMounted, provide, ref, watch } from "vue";
|
|
|
import { useAuth } from "src/composables/useAuth";
|
|
|
import { useI18n } from "vue-i18n";
|
|
|
import { useQuasar } from "quasar";
|
|
|
import { useRegistrationFlowStore } from "src/stores/registrationFlow";
|
|
|
import { useRouter } from "vue-router";
|
|
|
+import { useScroll } from "src/composables/useScroll";
|
|
|
+import { useSubmitHandler } from "src/composables/useSubmitHandler";
|
|
|
import { userStore } from "src/stores/user";
|
|
|
|
|
|
import LoginStep1Panel from "src/components/login/LoginStep1Panel.vue";
|
|
|
@@ -194,6 +208,8 @@ const email = ref("");
|
|
|
const isLogin = ref(false);
|
|
|
|
|
|
const loginForm = ref(null);
|
|
|
+const flowContent = ref(null);
|
|
|
+const flowScrollArea = ref(null);
|
|
|
|
|
|
const phone = ref("");
|
|
|
|
|
|
@@ -224,7 +240,12 @@ const stepThreeForm = ref({
|
|
|
zip_code: "",
|
|
|
});
|
|
|
|
|
|
-const submitting = ref(false);
|
|
|
+const { loading, serverErrors, execute } = useSubmitHandler();
|
|
|
+const { scrollToComponent } = useScroll();
|
|
|
+
|
|
|
+provide("clearServerError", (field) => {
|
|
|
+ serverErrors.value[field] = null;
|
|
|
+});
|
|
|
|
|
|
const goBack = () => {
|
|
|
if (steps.value === 4) {
|
|
|
@@ -267,20 +288,46 @@ const onDeviceLocation = ({ lat, lng }) => {
|
|
|
};
|
|
|
|
|
|
const onSubmit = async () => {
|
|
|
- submitting.value = true;
|
|
|
-
|
|
|
try {
|
|
|
switch (steps.value) {
|
|
|
case 1: {
|
|
|
- const response = await sendValidationCode();
|
|
|
+ await execute(() => sendCode(email.value, phone.value));
|
|
|
+
|
|
|
+ if (Object.keys(serverErrors.value).length === 0) {
|
|
|
+ steps.value = 2;
|
|
|
+ }
|
|
|
|
|
|
- isLogin.value = response.data.payload.isLogin;
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
- case 2:
|
|
|
- await validateCodeInput();
|
|
|
+ case 2: {
|
|
|
+ let response;
|
|
|
+
|
|
|
+ await execute(async () => {
|
|
|
+ response = await validateCode(
|
|
|
+ email.value,
|
|
|
+ phone.value,
|
|
|
+ code.value,
|
|
|
+ isLogin.value,
|
|
|
+ );
|
|
|
+ });
|
|
|
+
|
|
|
+ if (
|
|
|
+ Object.keys(serverErrors.value).length === 0 &&
|
|
|
+ response?.status === 200
|
|
|
+ ) {
|
|
|
+ if (isLogin.value === true) {
|
|
|
+ await setAuthDataFromPayload(response.data.payload);
|
|
|
+
|
|
|
+ router.push({ name: "DashboardPage" });
|
|
|
+ } else {
|
|
|
+ stepThreeForm.value.phone = phone.value;
|
|
|
+ steps.value = 3;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
break;
|
|
|
+ }
|
|
|
|
|
|
case 3:
|
|
|
steps.value = 4;
|
|
|
@@ -288,13 +335,23 @@ const onSubmit = async () => {
|
|
|
|
|
|
case 4:
|
|
|
await registerUserAndClient();
|
|
|
+
|
|
|
+ if (Object.keys(serverErrors.value).length > 0) {
|
|
|
+ steps.value = 3;
|
|
|
+ }
|
|
|
+
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
break;
|
|
|
}
|
|
|
- } finally {
|
|
|
- submitting.value = false;
|
|
|
+ } catch (error) {
|
|
|
+ console.error("Login flow error", error);
|
|
|
+
|
|
|
+ $q.notify({
|
|
|
+ type: "negative",
|
|
|
+ message: t("auth.register_error"),
|
|
|
+ });
|
|
|
}
|
|
|
};
|
|
|
|
|
|
@@ -310,46 +367,35 @@ const registerUserAndClient = async () => {
|
|
|
};
|
|
|
|
|
|
Object.entries(stepThreeForm.value).forEach(([key, value]) => {
|
|
|
- if (key === "no_complement") {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (key === "complement" && stepThreeForm.value.no_complement) {
|
|
|
+ if (key === "no_complement" || key === "complement") {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
append(key, value);
|
|
|
});
|
|
|
|
|
|
+ payload.complement = stepThreeForm.value.complement || "";
|
|
|
+
|
|
|
append("code", code.value);
|
|
|
append("email", email.value);
|
|
|
- append("has_complement", !stepThreeForm.value.no_complement);
|
|
|
+ append("has_complement", true);
|
|
|
append("phone", stepThreeForm.value.phone || phone.value);
|
|
|
|
|
|
- try {
|
|
|
+ await execute(async () => {
|
|
|
const response = await createUserAndClient(payload);
|
|
|
|
|
|
- if (response.status >= 200 && response.status < 300) {
|
|
|
- await setAuthDataFromPayload(response.data.payload);
|
|
|
+ await setAuthDataFromPayload(response.data.payload);
|
|
|
|
|
|
- if (stepAvatarForm.value.avatar) {
|
|
|
- const user = await updateMe({
|
|
|
- avatar_base64: await fileToBase64(stepAvatarForm.value.avatar),
|
|
|
- });
|
|
|
+ if (stepAvatarForm.value.avatar) {
|
|
|
+ const user = await updateMe({
|
|
|
+ avatar_base64: await fileToBase64(stepAvatarForm.value.avatar),
|
|
|
+ });
|
|
|
|
|
|
- userStore().setUser(user);
|
|
|
- }
|
|
|
-
|
|
|
- router.push({ name: "DashboardPage" });
|
|
|
+ userStore().setUser(user);
|
|
|
}
|
|
|
- } catch (error) {
|
|
|
- console.error("Failed to create user and client", error);
|
|
|
|
|
|
- $q.notify({
|
|
|
- type: "negative",
|
|
|
- message: t("auth.register_error"),
|
|
|
- });
|
|
|
- }
|
|
|
+ router.push({ name: "DashboardPage" });
|
|
|
+ });
|
|
|
};
|
|
|
|
|
|
//
|
|
|
@@ -374,40 +420,25 @@ const saveRegistrationData = () => {
|
|
|
});
|
|
|
};
|
|
|
|
|
|
-const sendValidationCode = async () => {
|
|
|
- const response = await sendCode(email.value, phone.value);
|
|
|
+const scrollToInvalidField = async (field) => {
|
|
|
+ await nextTick();
|
|
|
|
|
|
- if (response.status === 201) {
|
|
|
- steps.value = 2;
|
|
|
- } else {
|
|
|
- console.error("Failed to send validation code");
|
|
|
- }
|
|
|
-
|
|
|
- return response;
|
|
|
-};
|
|
|
+ const target = field ?? flowContent.value?.querySelector(".q-field--error");
|
|
|
|
|
|
-const validateCodeInput = async () => {
|
|
|
- const response = await validateCode(
|
|
|
- email.value,
|
|
|
- phone.value,
|
|
|
- code.value,
|
|
|
- isLogin.value,
|
|
|
- );
|
|
|
-
|
|
|
- if (response.status === 200) {
|
|
|
- if (isLogin.value === true) {
|
|
|
- await setAuthDataFromPayload(response.data.payload);
|
|
|
- router.push({ name: "DashboardPage" });
|
|
|
- } else {
|
|
|
- stepThreeForm.value.phone = phone.value;
|
|
|
- steps.value = 3;
|
|
|
- }
|
|
|
- } else {
|
|
|
- console.error("Invalid validation code");
|
|
|
+ if (target && flowScrollArea.value) {
|
|
|
+ scrollToComponent(target, flowScrollArea);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-//
|
|
|
+watch(
|
|
|
+ [steps, () => Object.values(serverErrors.value).some(Boolean)],
|
|
|
+ ([, hasErrors]) => {
|
|
|
+ if (hasErrors) {
|
|
|
+ scrollToInvalidField();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ { flush: "post" },
|
|
|
+);
|
|
|
|
|
|
onMounted(() => {
|
|
|
loadServicesConfig();
|
|
|
@@ -498,12 +529,19 @@ onMounted(() => {
|
|
|
}
|
|
|
|
|
|
.flow-screen {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
width: 100%;
|
|
|
max-width: 500px;
|
|
|
- min-height: 100vh;
|
|
|
- padding: 16px 20px;
|
|
|
+ height: 100vh;
|
|
|
+ height: 100dvh;
|
|
|
+ background: var(--q-surface-dark);
|
|
|
+}
|
|
|
+
|
|
|
+.flow-screen-content {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ min-height: 100%;
|
|
|
+ padding: 16px 20px 0;
|
|
|
background: var(--q-surface-dark);
|
|
|
}
|
|
|
|
|
|
@@ -525,16 +563,25 @@ onMounted(() => {
|
|
|
}
|
|
|
|
|
|
.flow-form {
|
|
|
- flex: 1;
|
|
|
+ height: 100%;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
}
|
|
|
|
|
|
-.flow-content {
|
|
|
+.flow-scroll {
|
|
|
flex: 1;
|
|
|
- overflow-y: auto;
|
|
|
+ height: 0;
|
|
|
min-height: 0;
|
|
|
|
|
|
+ :deep(.q-scrollarea__content) {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.flow-content {
|
|
|
+ flex: 1;
|
|
|
+
|
|
|
&--centered {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
@@ -550,7 +597,8 @@ onMounted(() => {
|
|
|
|
|
|
.flow-footer {
|
|
|
flex-shrink: 0;
|
|
|
- padding: 8px 0 calc(12px + env(safe-area-inset-bottom));
|
|
|
+ padding: 8px 20px calc(12px + env(safe-area-inset-bottom));
|
|
|
+ background: var(--q-surface-dark);
|
|
|
}
|
|
|
|
|
|
.register-later-row {
|