Преглед на файлове

implementacao didit - validacao de documentos

Gustavo Zanatta преди 3 дни
родител
ревизия
da81c00152

+ 4 - 0
src-capacitor/android/app/src/debug/AndroidManifest.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+    <application android:usesCleartextTraffic="true" />
+</manifest>

+ 7 - 0
src-capacitor/android/app/src/main/AndroidManifest.xml

@@ -27,6 +27,13 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
 
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:scheme="br.com.diarista.provider" />
+            </intent-filter>
+
         </activity>
 
         <provider

+ 19 - 0
src/api/verification.js

@@ -0,0 +1,19 @@
+import { Capacitor } from "@capacitor/core";
+
+import api from "src/api";
+
+export const createVerificationSession = async () => {
+  const { data: response } = await api.post(
+    "/verification/session",
+    { native: Capacitor.isNativePlatform() },
+    { skipSuccessNotify: true },
+  );
+
+  return response.payload;
+};
+
+export const getMyVerification = async () => {
+  const { data: response } = await api.get("/verification/me");
+
+  return response.payload;
+};

+ 140 - 16
src/components/dashboard/DashboardPendingApproval.vue

@@ -1,39 +1,163 @@
 <template>
   <div class="q-mx-md q-mb-md">
     <div class="font16 fontbold gradient-diarista q-mb-sm section-title">
-      {{ $t("provider.dashboard.pending_approval.title") }}
+      {{ headline }}
     </div>
 
     <div class="pending-alert column items-center text-center">
-      <q-icon
-        class="pending-alert__icon q-mb-sm"
-        name="mdi-alert-outline"
-        size="28px"
-      />
+      <q-spinner-dots v-if="busy" class="q-mb-sm" color="primary" size="32px" />
 
-      <div class="pending-alert__text font14 fontbold q-mb-md">
-        {{ $t("provider.dashboard.pending_approval.analyzing") }}
-      </div>
+      <q-icon v-else class="pending-alert__icon q-mb-sm" :name="icon" size="28px" />
 
-      <div class="pending-alert__text font14 fontbold q-mb-md">
-        {{ $t("provider.dashboard.pending_approval.release") }}
+      <div
+        v-for="(line, index) in lines"
+        :key="index"
+        class="pending-alert__text font14 q-mb-md"
+        :class="index === lines.length - 1 ? 'fontregular' : 'fontbold'"
+      >
+        {{ line }}
       </div>
 
-      <div class="pending-alert__text font14 fontbold q-mb-md">
-        {{ $t("provider.dashboard.pending_approval.email_notice") }}
-      </div>
+      <q-btn
+        v-if="showAction"
+        class="full-width"
+        color="primary"
+        :label="actionLabel"
+        :loading="loading"
+        no-caps
+        rounded
+        unelevated
+        @click="start"
+      />
 
-      <div class="pending-alert__text font14 fontregular">
-        {{ $t("provider.dashboard.pending_approval.come_back_later") }}
+      <div
+        v-if="showAttempts"
+        class="pending-alert__text font12 fontregular q-mt-sm"
+      >
+        {{ $t("verification.attempts_left", { count: attemptsLeft }) }}
       </div>
     </div>
   </div>
 </template>
 
 <script setup>
+import { computed, onMounted } from "vue";
+import { useI18n } from "vue-i18n";
+
+import {
+  useIdentityVerification,
+  VERIFICATION_STATUS,
+} from "src/composables/useIdentityVerification";
+
 defineOptions({
   name: "DashboardPendingApproval",
 });
+
+const { t } = useI18n();
+
+const {
+  attemptsLeft,
+  canRetry,
+  hasOpenSession,
+  loading,
+  polling,
+  refresh,
+  start,
+  status,
+} = useIdentityVerification();
+
+onMounted(() => {
+  refresh().catch(() => {
+  });
+});
+
+const busy = computed(() => loading.value || polling.value);
+
+const headline = computed(() => {
+  if (busy.value) return t("verification.title");
+
+  switch (status.value) {
+    case VERIFICATION_STATUS.NOT_STARTED:
+    case VERIFICATION_STATUS.EXPIRED:
+      return t("verification.title");
+    case VERIFICATION_STATUS.PENDING:
+      return t("verification.in_progress_title");
+    case VERIFICATION_STATUS.DECLINED:
+      return t("verification.declined_title");
+    case VERIFICATION_STATUS.APPROVED:
+    case VERIFICATION_STATUS.IN_REVIEW:
+    default:
+      return t("provider.dashboard.pending_approval.title");
+  }
+});
+
+const icon = computed(() => {
+  switch (status.value) {
+    case VERIFICATION_STATUS.NOT_STARTED:
+    case VERIFICATION_STATUS.EXPIRED:
+      return "mdi-card-account-details-outline";
+    case VERIFICATION_STATUS.DECLINED:
+      return "mdi-alert-circle-outline";
+    default:
+      return "mdi-alert-outline";
+  }
+});
+
+const lines = computed(() => {
+  if (busy.value) {
+    return [loading.value ? t("verification.opening") : t("verification.checking")];
+  }
+
+  switch (status.value) {
+    case VERIFICATION_STATUS.NOT_STARTED:
+    case VERIFICATION_STATUS.EXPIRED:
+      return [t("verification.intro"), t("verification.documents_hint")];
+
+    case VERIFICATION_STATUS.PENDING:
+      return hasOpenSession.value
+        ? [t("verification.resume_hint")]
+        : [t("verification.in_progress")];
+
+    case VERIFICATION_STATUS.DECLINED:
+      return canRetry.value
+        ? [t("verification.declined")]
+        : [t("verification.attempts_exhausted")];
+
+    case VERIFICATION_STATUS.IN_REVIEW:
+      return [t("verification.in_review")];
+
+    default:
+      return [
+        t("provider.dashboard.pending_approval.analyzing"),
+        t("provider.dashboard.pending_approval.release"),
+        t("provider.dashboard.pending_approval.email_notice"),
+        t("provider.dashboard.pending_approval.come_back_later"),
+      ];
+  }
+});
+
+const showAction = computed(() => {
+  if (busy.value) return false;
+
+  if (status.value === VERIFICATION_STATUS.DECLINED) return canRetry.value;
+
+  if (status.value === VERIFICATION_STATUS.PENDING) return hasOpenSession.value;
+
+  return [VERIFICATION_STATUS.NOT_STARTED, VERIFICATION_STATUS.EXPIRED].includes(
+    status.value,
+  );
+});
+
+const actionLabel = computed(() => {
+  if (status.value === VERIFICATION_STATUS.DECLINED) return t("verification.retry");
+  if (status.value === VERIFICATION_STATUS.PENDING) return t("verification.resume");
+
+  return t("verification.start");
+});
+
+const showAttempts = computed(
+  () => showAction.value && Number.isFinite(attemptsLeft.value),
+);
 </script>
 
 <style lang="scss" scoped>

+ 6 - 406
src/components/login/LoginStep4Panel.vue

@@ -60,89 +60,6 @@
         </template>
       </div>
 
-      <div class="column items-center q-gutter-y-sm">
-        <div
-          :ref="fieldRefs.document_front"
-          class="cursor-pointer"
-          @click="openDocSubStep"
-        >
-          <div
-            v-if="docDone"
-            class="row items-center justify-center q-gutter-x-sm"
-          >
-            <div class="photo-circle photo-circle--small photo-circle--done">
-              <img
-                v-if="documentFrontPreviewUrl"
-                class="photo-circle__thumb"
-                :src="documentFrontPreviewUrl"
-              />
-            </div>
-
-            <div class="photo-circle photo-circle--small photo-circle--done">
-              <img
-                v-if="documentBackPreviewUrl"
-                class="photo-circle__thumb"
-                :src="documentBackPreviewUrl"
-              />
-            </div>
-          </div>
-
-          <div
-            v-else
-            class="photo-circle photo-circle--pending"
-          >
-            <q-icon
-              color="white"
-              name="mdi-card-account-details-outline"
-              size="52px"
-            />
-          </div>
-        </div>
-
-        <div
-          v-if="serverErrors.document_front"
-          class="text-negative font12 text-center"
-        >
-          {{ serverErrors.document_front }}
-        </div>
-
-        <div
-          v-if="serverErrors.document_back"
-          :ref="fieldRefs.document_back"
-          class="text-negative font12 text-center"
-        >
-          {{ serverErrors.document_back }}
-        </div>
-
-        <template v-if="!docDone">
-          <q-btn
-            color="primary-button"
-            no-caps
-            padding="10px 28px"
-            rounded
-            :label="t('provider.login.steps.step_4.btn_document')"
-            @click="openDocSubStep"
-          />
-
-          <div class="font12 text-center text-text">
-            {{ t('provider.login.steps.step_4.document_hint') }}
-          </div>
-        </template>
-
-        <template v-else>
-          <div class="items-center q-gutter-x-xs row">
-            <q-icon
-              color="positive"
-              name="check_circle"
-              size="18px"
-            />
-
-            <span class="text-primary">
-              {{ t('provider.login.steps.step_4.document_sent') }}
-            </span>
-          </div>
-        </template>
-      </div>
     </div>
 
     <Teleport to="body">
@@ -203,168 +120,6 @@
         </div>
       </div>
     </Teleport>
-
-    <Teleport to="body">
-      <div
-        v-if="showDocSubStep"
-        class="fs-overlay doc-substep"
-      >
-        <div class="doc-substep__header">
-          <q-btn
-            flat
-            icon="arrow_back"
-            round
-            @click="closeDocSubStep"
-          />
-        </div>
-
-        <div class="doc-substep__body column items-center q-gutter-y-lg">
-          <div class="doc-slot column items-center q-gutter-y-sm">
-            <div class="text-primary">
-              {{ t('provider.login.steps.step_4.document_front') }}
-            </div>
-
-            <div class="font12 text-center text-text">
-              {{ t('provider.login.steps.step_4.document_front_desc') }}
-            </div>
-
-            <div
-              class="doc-slot__preview"
-              :class="docFrontDraft ? 'doc-slot__preview--done' : 'doc-slot__preview--pending'"
-            >
-              <q-img
-                v-if="docFrontDraft"
-                class="doc-slot__thumb"
-                :src="docFrontPreviewUrl"
-              />
-
-              <q-icon
-                v-else
-                color="white"
-                name="mdi-card-account-details-outline"
-                size="48px"
-              />
-            </div>
-
-            <div
-              v-if="docFrontDraft"
-              class="items-center q-gutter-x-xs row"
-            >
-              <q-icon
-                color="positive"
-                name="check_circle"
-                size="16px"
-              />
-
-              <span class="font12 text-primary">
-                {{ t('provider.login.steps.step_4.photo_captured') }}
-              </span>
-            </div>
-
-            <div
-              v-if="docFrontError"
-              class="text-negative font12 text-center"
-            >
-              {{ t('provider.login.steps.step_4.error_message') }}
-            </div>
-
-            <q-btn
-              color="primary-button"
-              no-caps
-              padding="8px 22px"
-              rounded
-              :label="docFrontDraft
-                ? t('provider.login.steps.step_4.btn_retake')
-                : t('provider.login.steps.step_4.btn_capture_front')"
-              :loading="loadingDocFront"
-              @click="captureDocFront"
-            />
-          </div>
-
-          <div class="doc-slot column items-center q-gutter-y-sm">
-            <div class="text-primary">
-              {{ t('provider.login.steps.step_4.document_back') }}
-            </div>
-
-            <div class="font12 text-center text-text">
-              {{ t('provider.login.steps.step_4.document_back_desc') }}
-            </div>
-
-            <div
-              class="doc-slot__preview"
-              :class="docBackDraft ? 'doc-slot__preview--done' : 'doc-slot__preview--pending'"
-            >
-              <q-img
-                v-if="docBackDraft"
-                class="doc-slot__thumb"
-                :src="docBackPreviewUrl"
-              />
-
-              <q-icon
-                v-else
-                color="white"
-                name="mdi-card-account-details-outline"
-                size="48px"
-              />
-            </div>
-
-            <div
-              v-if="docBackDraft"
-              class="items-center q-gutter-x-xs row"
-            >
-              <q-icon
-                color="positive"
-                name="check_circle"
-                size="16px"
-              />
-
-              <span class="font12 text-primary">
-                {{ t('provider.login.steps.step_4.photo_captured') }}
-              </span>
-            </div>
-
-            <div
-              v-if="docBackError"
-              class="text-negative font12 text-center"
-            >
-              {{ t('provider.login.steps.step_4.error_message') }}
-            </div>
-
-            <q-btn
-              color="primary-button"
-              no-caps
-              padding="8px 22px"
-              rounded
-              :label="docBackDraft
-                ? t('provider.login.steps.step_4.btn_retake')
-                : t('provider.login.steps.step_4.btn_capture_back')"
-              :loading="loadingDocBack"
-              @click="captureDocBack"
-            />
-          </div>
-        </div>
-
-        <div class="doc-substep__footer">
-          <q-btn
-            v-if="docSubStepDone"
-            color="primary-button"
-            no-caps
-            padding="14px 48px"
-            rounded
-            style="min-width: 200px"
-            :label="t('provider.login.steps.step_4.btn_continue')"
-            @click="confirmDocCapture"
-          />
-
-          <div
-            v-else
-            class="font12 text-center text-grey-6"
-          >
-            {{ t('provider.login.steps.step_4.upload_all_photos') }}
-          </div>
-        </div>
-      </div>
-    </Teleport>
   </q-card-section>
 </template>
 
@@ -395,8 +150,6 @@ const serverErrors = inject("serverErrors", ref({}));
 
 const fieldRefs = {
   selfie: ref(null),
-  document_front: ref(null),
-  document_back: ref(null),
 };
 
 defineExpose({
@@ -409,29 +162,7 @@ const resultIcon = ref('photo_camera');
 const resultMessage = ref('');
 const showResult = ref(false);
 
-const showDocSubStep = ref(false);
-const docFrontDraft = ref(null);
-const docBackDraft = ref(null);
-const docFrontPreviewUrl = ref(null);
-const docBackPreviewUrl = ref(null);
-const loadingDocFront = ref(false);
-const loadingDocBack = ref(false);
-const docFrontError = ref(false);
-const docBackError = ref(false);
-
 const selfiePreviewUrl = ref(null);
-const documentFrontPreviewUrl = ref(null);
-const documentBackPreviewUrl = ref(null);
-
-const docDone = computed(() => (
-  !!form.value.document_back
-  && !!form.value.document_front
-));
-
-const docSubStepDone = computed(() => (
-  !!docFrontDraft.value
-  && !!docBackDraft.value
-));
 
 const selfieDone = computed(() => !!form.value.selfie);
 
@@ -444,8 +175,6 @@ const syncPreviewUrl = (previewRef, file) => {
 };
 
 watch(() => form.value.selfie, (file) => syncPreviewUrl(selfiePreviewUrl, file), { immediate: true });
-watch(() => form.value.document_front, (file) => syncPreviewUrl(documentFrontPreviewUrl, file), { immediate: true });
-watch(() => form.value.document_back, (file) => syncPreviewUrl(documentBackPreviewUrl, file), { immediate: true });
 
 const base64ToFile = (base64, filename, mimeType = 'image/jpeg') => {
   const byteString = atob(base64);
@@ -471,12 +200,10 @@ const closeResult = () => {
   }
 };
 
-const openCamera = async (direction) => {
+const openCamera = async () => {
   const photo = await Camera.getPhoto({
     allowEditing: false,
-    direction: direction === 'front'
-      ? CameraDirection.Front
-      : CameraDirection.Rear,
+    direction: CameraDirection.Front,
     quality: 85,
     resultType: CameraResultType.Base64,
     saveToGallery: false,
@@ -490,93 +217,10 @@ const openCamera = async (direction) => {
   );
 };
 
-const setDraftFile = (side, file) => {
-  const draftRef = side === 'front' ? docFrontDraft : docBackDraft;
-  const previewRef = side === 'front' ? docFrontPreviewUrl : docBackPreviewUrl;
-
-  draftRef.value = file;
-  syncPreviewUrl(previewRef, file);
-};
-
-const captureDocSide = async (side) => {
-  const loadingRef = side === 'front' ? loadingDocFront : loadingDocBack;
-  const errorRef = side === 'front' ? docFrontError : docBackError;
-
-  if (loadingRef.value) {
-    return;
-  }
-
-  loadingRef.value = true;
-  errorRef.value = false;
-
-  try {
-    const file = await openCamera('rear');
-
-    setDraftFile(side, file);
-  } catch (err) {
-    const msg = (err?.message || '').toLowerCase();
-
-    if (
-      !msg.includes('cancel')
-      && !msg.includes('dismiss')
-      && !msg.includes('no image')
-    ) {
-      errorRef.value = true;
-    }
-  } finally {
-    loadingRef.value = false;
-  }
-};
-
-const captureDocFront = () => captureDocSide('front');
-const captureDocBack = () => captureDocSide('back');
-
-const openDocSubStep = () => {
-  if (!docFrontDraft.value && form.value.document_front) {
-    setDraftFile('front', form.value.document_front);
-  }
-
-  if (!docBackDraft.value && form.value.document_back) {
-    setDraftFile('back', form.value.document_back);
-  }
-
-  docFrontError.value = false;
-  docBackError.value = false;
-  showDocSubStep.value = true;
-
-  emit('update:show-sub-step', true);
-};
-
-const closeDocSubStep = () => {
-  showDocSubStep.value = false;
-
-  emit('update:show-sub-step', false);
-};
-
-const confirmDocCapture = () => {
-  if (!docSubStepDone.value) {
-    return;
-  }
-
-  form.value.document_front = docFrontDraft.value;
-  form.value.document_back = docBackDraft.value;
-  showDocSubStep.value = false;
-
-  emit('update:show-sub-step', false);
-};
-
 onBeforeUnmount(() => {
-  [
-    docFrontPreviewUrl,
-    docBackPreviewUrl,
-    selfiePreviewUrl,
-    documentFrontPreviewUrl,
-    documentBackPreviewUrl,
-  ].forEach((previewRef) => {
-    if (previewRef.value) {
-      URL.revokeObjectURL(previewRef.value);
-    }
-  });
+  if (selfiePreviewUrl.value) {
+    URL.revokeObjectURL(selfiePreviewUrl.value);
+  }
 });
 
 const takeSelfie = async () => {
@@ -587,7 +231,7 @@ const takeSelfie = async () => {
   loadingSelfie.value = true;
 
   try {
-    const file = await openCamera('front');
+    const file = await openCamera();
 
     form.value.selfie = file;
     resultError.value = false;
@@ -686,48 +330,4 @@ const takeSelfie = async () => {
   }
 }
 
-.doc-substep {
-  display: flex;
-  flex-direction: column;
-
-  &__header {
-    padding: 12px;
-  }
-
-  &__body {
-    flex: 1;
-    padding: 0 24px;
-    overflow-y: auto;
-  }
-
-  &__footer {
-    padding: 24px;
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-  }
-}
-
-.doc-slot__preview {
-  width: 160px;
-  height: 110px;
-  border-radius: 16px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  overflow: hidden;
-
-  &--pending {
-    background-color: rgba(139, 92, 246, 0.18);
-  }
-
-  &--done {
-    background: linear-gradient(-90deg, #ec48d1 5%, #6b11cb 65%, #2574fc 100%);
-  }
-}
-
-.doc-slot__thumb {
-  width: 100%;
-  height: 100%;
-}
 </style>

+ 175 - 0
src/composables/useIdentityVerification.js

@@ -0,0 +1,175 @@
+import { computed, onUnmounted, ref } from "vue";
+import { Browser } from "@capacitor/browser";
+import { useI18n } from "vue-i18n";
+import { Notify } from "quasar";
+
+import {
+  createVerificationSession,
+  getMyVerification,
+} from "src/api/verification";
+import { userStore } from "src/stores/user";
+
+export const VERIFICATION_STATUS = Object.freeze({
+  NOT_STARTED: "not_started",
+  PENDING: "pending",
+  APPROVED: "approved",
+  IN_REVIEW: "in_review",
+  DECLINED: "declined",
+  EXPIRED: "expired",
+});
+
+const POLL_INTERVAL_MS = 3000;
+const POLL_TIMEOUT_MS = 180000;
+
+export const useIdentityVerification = () => {
+  const { t } = useI18n();
+  const user = userStore();
+
+  const loading = ref(false);
+  const polling = ref(false);
+  const verification = ref(null);
+
+  let pollTimer = null;
+  let pollDeadline = 0;
+  let browserListener = null;
+
+  const status = computed(
+    () =>
+      verification.value?.identity_verification_status ??
+      user.user?.provider?.identity_verification_status ??
+      VERIFICATION_STATUS.NOT_STARTED,
+  );
+
+  const attemptsLeft = computed(() => verification.value?.attempts_left ?? null);
+
+  const isApproved = computed(() => status.value === VERIFICATION_STATUS.APPROVED);
+  const isInReview = computed(() => status.value === VERIFICATION_STATUS.IN_REVIEW);
+  const isDeclined = computed(() => status.value === VERIFICATION_STATUS.DECLINED);
+
+  const canRetry = computed(() => verification.value?.can_retry ?? false);
+
+  const hasOpenSession = computed(
+    () => verification.value?.has_open_session ?? false,
+  );
+
+  const needsVerification = computed(() =>
+    [
+      VERIFICATION_STATUS.NOT_STARTED,
+      VERIFICATION_STATUS.DECLINED,
+      VERIFICATION_STATUS.EXPIRED,
+    ].includes(status.value),
+  );
+
+  const refresh = async () => {
+    verification.value = await getMyVerification();
+
+    return verification.value;
+  };
+
+  const start = async () => {
+    if (loading.value) return;
+
+    loading.value = true;
+
+    try {
+      const session = await createVerificationSession();
+
+      await watchForReturn();
+      await Browser.open({ url: session.verification_url });
+    } catch (error) {
+      if (error?.response?.status !== 422) {
+        Notify.create({ type: "negative", message: t("verification.start_failed") });
+      }
+
+      await stopWatching();
+    } finally {
+      loading.value = false;
+    }
+  };
+
+  const watchForReturn = async () => {
+    await stopWatching();
+
+    browserListener = await Browser.addListener("browserFinished", startPolling);
+
+    document.addEventListener("visibilitychange", onVisibilityChange);
+  };
+
+  const onVisibilityChange = () => {
+    if (document.visibilityState === "visible") startPolling();
+  };
+
+  const startPolling = () => {
+    if (polling.value) return;
+
+    polling.value = true;
+    pollDeadline = Date.now() + POLL_TIMEOUT_MS;
+
+    const tick = async () => {
+      try {
+        const current = await refresh();
+
+        const settled = [
+          VERIFICATION_STATUS.APPROVED,
+          VERIFICATION_STATUS.IN_REVIEW,
+          VERIFICATION_STATUS.DECLINED,
+        ].includes(current?.identity_verification_status);
+
+        if (settled) {
+          if (current.identity_verification_status === VERIFICATION_STATUS.APPROVED) {
+            await user.fetchUser();
+          }
+
+          return stopPolling();
+        }
+      } catch {
+        // rede instável ao voltar do navegador: tenta de novo no próximo tick
+      }
+
+      if (Date.now() >= pollDeadline) return stopPolling();
+
+      pollTimer = setTimeout(tick, POLL_INTERVAL_MS);
+    };
+
+    tick();
+  };
+
+  const stopPolling = () => {
+    polling.value = false;
+
+    if (pollTimer) {
+      clearTimeout(pollTimer);
+      pollTimer = null;
+    }
+  };
+
+  const stopWatching = async () => {
+    document.removeEventListener("visibilitychange", onVisibilityChange);
+
+    if (browserListener) {
+      await browserListener.remove();
+      browserListener = null;
+    }
+  };
+
+  onUnmounted(async () => {
+    stopPolling();
+    await stopWatching();
+  });
+
+  return {
+    attemptsLeft,
+    canRetry,
+    hasOpenSession,
+    isApproved,
+    isDeclined,
+    isInReview,
+    loading,
+    needsVerification,
+    polling,
+    refresh,
+    start,
+    status,
+    verification,
+  };
+};

+ 10 - 0
src/composables/useProviderApproval.js

@@ -16,6 +16,14 @@ export const useProviderApproval = () => {
 
   const approvalStatus = computed(() => user.user?.provider?.approval_status ?? null);
 
+  const identityVerificationStatus = computed(
+    () => user.user?.provider?.identity_verification_status ?? null,
+  );
+
+  const isIdentityVerified = computed(
+    () => identityVerificationStatus.value === "approved",
+  );
+
   const isPendingProvider = computed(
     () => isProvider.value && approvalStatus.value === APPROVAL_STATUS.PENDING,
   );
@@ -26,6 +34,8 @@ export const useProviderApproval = () => {
 
   return {
     approvalStatus,
+    identityVerificationStatus,
+    isIdentityVerified,
     isPendingProvider,
     isProvider,
     isRejectedProvider,

+ 24 - 1
src/i18n/locales/en.json

@@ -225,7 +225,8 @@
           "btn_capture_selfie": "Capture selfie",
           "btn_capture_front": "Capture front",
           "btn_capture_back": "Capture back",
-          "upload_all_photos": "Attach all the necessary photos!"
+          "upload_all_photos": "Attach all the necessary photos!",
+          "selfie_required": "Send your selfie to continue."
         },
         "step_5": {
           "daily_price_title": "What is the value of your daily rate for up to 8 hours?",
@@ -931,5 +932,27 @@
       "chat_header": "--- Conversation with the assistant ---",
       "user_message_header": "--- User message ---"
     }
+  },
+  "verification": {
+    "title": "Identity verification",
+    "intro": "To release your registration we need to confirm your identity. It is quick: photograph a document and take a selfie.",
+    "documents_hint": "Have your ID or driver's license at hand.",
+    "start": "Verify identity",
+    "retry": "Try again",
+    "attempts_left": "You have {count} attempt(s) left.",
+    "opening": "Opening verification...",
+    "checking": "Confirming the result...",
+    "in_progress_title": "Verification in progress",
+    "in_progress": "We are processing your verification. This usually takes less than a minute.",
+    "in_review_title": "Verification under review",
+    "in_review": "Our team is checking your data. We will let you know by e-mail once it is done.",
+    "declined_title": "We could not confirm your identity",
+    "declined": "Check that the document is legible and that your registration data matches it.",
+    "attempts_exhausted": "You reached the attempt limit. Our team will review your registration manually.",
+    "approved_title": "Identity verified",
+    "approved": "All set! Your identity has been confirmed.",
+    "start_failed": "Could not start the verification. Please try again shortly.",
+    "resume": "Resume verification",
+    "resume_hint": "You started the verification and did not finish. Pick up where you left off — this does not use a new attempt."
   }
 }

+ 24 - 1
src/i18n/locales/es.json

@@ -225,7 +225,8 @@
           "btn_capture_selfie": "Capturar selfie",
           "btn_capture_front": "Capturar frente",
           "btn_capture_back": "Capturar dorso",
-          "upload_all_photos": "¡Adjunta todas las fotos necesarias!"
+          "upload_all_photos": "¡Adjunta todas las fotos necesarias!",
+          "selfie_required": "Envía tu selfie para continuar."
         },
         "step_5": {
           "daily_price_title": "¿Cuál es el valor de su jornada de hasta 8 horas?",
@@ -931,5 +932,27 @@
       "chat_header": "--- Conversación con el asistente ---",
       "user_message_header": "--- Mensaje del usuario ---"
     }
+  },
+  "verification": {
+    "title": "Verificación de identidad",
+    "intro": "Para liberar tu registro necesitamos confirmar tu identidad. Es rápido: fotografías un documento y te tomas una selfie.",
+    "documents_hint": "Ten a mano tu documento de identidad o licencia de conducir.",
+    "start": "Verificar identidad",
+    "retry": "Intentar de nuevo",
+    "attempts_left": "Te quedan {count} intento(s).",
+    "opening": "Abriendo la verificación...",
+    "checking": "Confirmando el resultado...",
+    "in_progress_title": "Verificación en curso",
+    "in_progress": "Estamos procesando tu verificación. Suele tardar menos de un minuto.",
+    "in_review_title": "Verificación en análisis",
+    "in_review": "Nuestro equipo está revisando tus datos. Te avisaremos por correo cuando termine.",
+    "declined_title": "No pudimos confirmar tu identidad",
+    "declined": "Comprueba que el documento sea legible y que tus datos de registro coincidan.",
+    "attempts_exhausted": "Alcanzaste el límite de intentos. Nuestro equipo revisará tu registro manualmente.",
+    "approved_title": "Identidad verificada",
+    "approved": "¡Todo listo! Tu identidad fue confirmada.",
+    "start_failed": "No fue posible iniciar la verificación. Inténtalo de nuevo en unos instantes.",
+    "resume": "Continuar verificación",
+    "resume_hint": "Empezaste la verificación y no la terminaste. Continúa donde lo dejaste: esto no consume un nuevo intento."
   }
 }

+ 24 - 1
src/i18n/locales/pt.json

@@ -225,7 +225,8 @@
           "btn_capture_selfie": "Tirar selfie",
           "btn_capture_front": "Fotografar frente",
           "btn_capture_back": "Fotografar verso",
-          "upload_all_photos": "Anexe todas as fotos necessárias!"
+          "upload_all_photos": "Anexe todas as fotos necessárias!",
+          "selfie_required": "Envie sua selfie para continuar."
         },
         "step_5": {
           "daily_price_title": "Qual valor da sua diária de até 8 horas?",
@@ -934,5 +935,27 @@
       "chat_header": "--- Conversa com o assistente ---",
       "user_message_header": "--- Mensagem do usuário ---"
     }
+  },
+  "verification": {
+    "title": "Verificação de identidade",
+    "intro": "Para liberar seu cadastro precisamos confirmar sua identidade. É rápido: você fotografa um documento e faz uma selfie.",
+    "documents_hint": "Tenha em mãos seu RG, CNH ou CIN.",
+    "start": "Verificar identidade",
+    "retry": "Tentar novamente",
+    "attempts_left": "Você tem {count} tentativa(s) restante(s).",
+    "opening": "Abrindo a verificação...",
+    "checking": "Confirmando o resultado...",
+    "in_progress_title": "Verificação em andamento",
+    "in_progress": "Estamos processando sua verificação. Isso costuma levar menos de um minuto.",
+    "in_review_title": "Verificação em análise",
+    "in_review": "Nossa equipe está conferindo seus dados. Avisaremos por e-mail assim que terminar.",
+    "declined_title": "Não conseguimos confirmar sua identidade",
+    "declined": "Verifique se o documento está legível e se seus dados de cadastro conferem com ele.",
+    "attempts_exhausted": "Você atingiu o limite de tentativas. Nossa equipe vai analisar seu cadastro manualmente.",
+    "approved_title": "Identidade verificada",
+    "approved": "Tudo certo! Sua identidade foi confirmada.",
+    "start_failed": "Não foi possível iniciar a verificação. Tente novamente em instantes.",
+    "resume": "Continuar verificação",
+    "resume_hint": "Você começou a verificação e não concluiu. Continue de onde parou — isso não consome uma nova tentativa."
   }
 }

+ 7 - 17
src/pages/LoginPage.vue

@@ -175,7 +175,7 @@ const FIELD_TO_STEP = {
   name: 3, email: 3, phone: 3, rg: 3, document: 3, birth_date: 3, gender: 3,
   zip_code: 3, address: 3, number: 3, district: 3, city: 3, state: 3,
   complement: 3, nickname: 3, instructions: 3, address_type: 3,
-  selfie: 4, document_front: 4, document_back: 4,
+  selfie: 4,
   daily_price_8h: 5, daily_price_6h: 5, daily_price_4h: 5, daily_price_2h: 5,
   working_days: 6,
 };
@@ -227,8 +227,6 @@ const createInitialStepFiveForm = () => ({
 });
 
 const createInitialStepFourForm = () => ({
-  document_back: null,
-  document_front: null,
   selfie: null,
 });
 
@@ -492,8 +490,6 @@ const registerUserAndProvider = async () => {
   });
 
   form.append("selfie", stepFourForm.value.selfie);
-  form.append("document_front", stepFourForm.value.document_front);
-  form.append("document_back", stepFourForm.value.document_back);
 
   append("recipient_name", stepThreeForm.value.name);
   append("recipient_email", stepThreeForm.value.email || email.value);
@@ -616,19 +612,13 @@ const validateCurrentStep = async () => {
     return false;
   }
 
-  if (steps.value === 4) {
-    const hasDocumentBack = !!stepFourForm.value.document_back;
-    const hasDocumentFront = !!stepFourForm.value.document_front;
-    const hasSelfie = !!stepFourForm.value.selfie;
-
-    if (!hasSelfie || !hasDocumentFront || !hasDocumentBack) {
-      $q.notify({
-        message: t("provider.login.steps.step_4.upload_all_photos"),
-        type: "negative",
-      });
+  if (steps.value === 4 && !stepFourForm.value.selfie) {
+    $q.notify({
+      message: t("provider.login.steps.step_4.selfie_required"),
+      type: "negative",
+    });
 
-      return false;
-    }
+    return false;
   }
 
   if (steps.value === 6 && !hasWorkingDaySelected()) {