Integração Mobile (Deep Link)

Objetivo

Implementar verificação de idade em um app React Native (Expo) usando Deep Link — o app abre a Carteira Digital diretamente no dispositivo (fluxo same-device).

Fluxo Mobile

sequenceDiagram
    participant U as Usuário
    participant App as App React Native
    participant VS as Verify Service
    participant W as Inji Wallet

    U->>App: Toca "Verificar Idade"
    App->>VS: POST /v1/verify/vp-request
    VS-->>App: transactionId + requestId + authorizationDetails
    App->>W: Linking.openURL("openid4vp://authorize?...")
    Note over W: Usuário autoriza credencial
    W->>VS: POST /v1/verify/vp-submission
    App->>App: AppState "active" → retoma polling
    App->>VS: GET /v1/verify/vp-request/{requestId}/status
    VS-->>App: VP_SUBMITTED
    App->>VS: GET /v1/verify/vp-result/{transactionId}
    VS-->>App: vcResults[0].vc → isOver18
    App->>U: Callback executado

Pré-requisitos

  • Expo SDK 51+ com expo-linking
  • scheme configurado no app.json (necessário para o wallet retornar ao app)
  • Inji Wallet instalado no dispositivo para testes

Passo 1 — Configurar o scheme no app.json

O scheme é o identificador do seu app para o Deep Link de retorno. O wallet usa o parâmetro origin para saber para onde voltar.

{
  "expo": {
    "scheme": "<seu-app>",
    "android": {
      "intentFilters": [
        {
          "action": "VIEW",
          "data": [{ "scheme": "<seu-app>" }],
          "category": ["BROWSABLE", "DEFAULT"]
        }
      ]
    }
  }
}
Nota

No iOS o scheme é registrado automaticamente pelo Expo a partir do campo scheme. No Android, o intentFilter acima é necessário para builds standalone.

Passo 3 — Polling de status e obtenção do resultado

// src/services/verifyService.ts (continuação)

export async function pollVPStatus(
  requestId: string
): Promise<"ACTIVE" | "VP_SUBMITTED" | "EXPIRED"> {
  const response = await fetch(
    `${BASE_URL}/v1/verify/vp-request/${requestId}/status`
  );
  if (!response.ok) throw new Error(`Erro ao buscar status: ${response.status}`);
  const data = await response.json();
  return data.status;
}

export async function getVPResult(
  transactionId: string
): Promise<{ verified: boolean; underage: boolean }> {
  const response = await fetch(
    `${BASE_URL}/v1/verify/vp-result/${transactionId}`
  );
  if (!response.ok) throw new Error(`Erro ao buscar resultado: ${response.status}`);

  const data = await response.json();

  if (data.vpResultStatus !== "SUCCESS" || !data.vcResults?.length) {
    throw new Error(`Resultado indisponível: ${data.vpResultStatus}`);
  }

  const vcRaw = data.vcResults[0].vc;
  const vc = typeof vcRaw === "string" ? JSON.parse(vcRaw) : vcRaw;

  const subject =
    vc?.credential?.credentialSubject ?? vc?.credentialSubject ?? vc;
  const isOver18 = subject?.isOver18 === true;

  return { verified: isOver18, underage: !isOver18 };
}

Passo 4 — Tela de verificação com retomada por AppState

O ponto crítico do fluxo mobile é detectar quando o usuário volta ao app após autorizar na carteira. Use AppState para isso:

// src/screens/AgeCheckScreen.tsx
import { useState, useRef, useEffect } from "react";
import { View, Text, Pressable, AppState } from "react-native";
import { openWalletForVerification, pollVPStatus, getVPResult } from "../services/verifyService";

export default function AgeCheckScreen({ navigation }) {
  const [loading, setLoading] = useState(false);
  const [status, setStatus] = useState<string | null>(null);
  const [error, setError] = useState<string | null>(null);
  const sessionRef = useRef<{ requestId: string; transactionId: string } | null>(null);
  const pollingRef = useRef(false);

  async function startPolling(requestId: string, transactionId: string) {
    if (pollingRef.current) return;
    pollingRef.current = true;
    setStatus("Aguardando resposta do wallet…");

    try {
      while (pollingRef.current) {
        const vpStatus = await pollVPStatus(requestId);

        if (vpStatus === "VP_SUBMITTED") {
          pollingRef.current = false;
          setStatus("Verificando credencial…");
          const result = await getVPResult(transactionId);
          if (result.verified) {
            navigation.replace("Success", { requestId });
          } else {
            navigation.replace("Underage");
          }
          return;
        }

        if (vpStatus === "EXPIRED") {
          pollingRef.current = false;
          setError("Sessão expirada. Tente novamente.");
          setLoading(false);
          return;
        }
        // status === "ACTIVE" — aguarda e repete
      }
    } catch {
      pollingRef.current = false;
      setError("Erro ao verificar status. Tente novamente.");
      setLoading(false);
    }
  }

  // Retoma o polling quando o app volta ao foreground
  useEffect(() => {
    const sub = AppState.addEventListener("change", (nextState) => {
      if (nextState === "active" && sessionRef.current) {
        pollingRef.current = false;
        setError(null);
        setLoading(true);
        startPolling(sessionRef.current.requestId, sessionRef.current.transactionId);
      }
    });
    return () => {
      sub.remove();
      pollingRef.current = false;
    };
  }, []);

  async function handleVerify() {
    setLoading(true);
    setError(null);
    setStatus("Abrindo wallet…");
    try {
      const { transactionId, requestId } = await openWalletForVerification();
      sessionRef.current = { requestId, transactionId };
      startPolling(requestId, transactionId);
    } catch (e: any) {
      setError(e.message);
      setLoading(false);
    }
  }

  return (
    <View>
      {status && <Text>{status}</Text>}
      {error && <Text style={{ color: "red" }}>{error}</Text>}
      <Pressable onPress={handleVerify} disabled={loading}>
        <Text>Verificar Idade</Text>
      </Pressable>
    </View>
  );
}
Importante

O AppState.addEventListener("change") é o mecanismo central do fluxo same-device. Sem ele, o app não sabe quando o usuário voltou da carteira e o polling nunca é retomado.

Próximo passo

Entenda a Policy 18+ e como configurar a Presentation Definition.