DEV Community

Priscila Oliveira
Priscila Oliveira

Posted on

Como Implementar o SDK da Unico em React Native: Um Guia Completo com Bridge Nativa

TL;DR: Criei uma implementação completa de bridge nativa para integrar o SDK da Unico em aplicações React Native, incluindo captura de selfies e documentos. Este artigo documenta todo o processo, os desafios enfrentados e as soluções encontradas.


🎯 O Problema que Me Motivou

Durante o desenvolvimento de uma aplicação React Native que precisava de verificação biométrica, me deparei com um desafio frustrante: como integrar o SDK da Unico com React Native?

Os Obstáculos Encontrados:

  • Ausência de documentação específica para React Native
  • Falta de exemplos práticos de implementação
  • Complexidade da bridge nativa em duas plataformas
  • Configurações obscuras que consumiram dias

Após muito pesquisar e não encontrar recursos adequados, decidi criar uma solução completa e compartilhar com a comunidade para que outros desenvolvedores não passem pelo mesmo sufoco.


🏗️ A Arquitetura da Solução

Desenvolvi uma arquitetura robusta que combina Clean Architecture no React Native com bridges nativas otimizadas para iOS e Android.

Visão Geral da Estrutura:

📦 Projeto
├── 🤖 Android (Kotlin)
│   ├── UnicoSdkModule (Bridge principal)
│   ├── UnicoConfig (Credenciais)
│   ├── UnicoTheme (Personalização)
│   └── UnicoSdkPackage (Registro)
├── 🍎 iOS (Swift)
│   ├── UnicoSdkModule (Bridge principal)
│   ├── UnicoConfig (Credenciais)
│   ├── UnicoTheme (Personalização)
│   └── Bridge Header (Interoperabilidade)
└── ⚛️ React Native (TypeScript)
    ├── Domain (Regras de negócio)
    ├── Data (Repositórios)
    ├── Infrastructure (Serviços)
    └── Presentation (UI e Hooks)
Enter fullscreen mode Exit fullscreen mode

🤖 Implementação Android: Do Zero ao Funcionamento

1. Configuração do Gradle - A Base de Tudo

O primeiro desafio foi configurar corretamente o sistema de build do Android. O SDK da Unico não está no Maven Central, então precisei adicionar o repositório específico:

// android/build.gradle (projeto)
allprojects {
    repositories {
        google()
        mavenCentral()
        // 🎯 CRÍTICO: Repositório do SDK da Unico
        maven { url "https://maven-sdk.unico.run/sdk-mobile" }
    }
}
Enter fullscreen mode Exit fullscreen mode

E no android/app/build.gradle, a dependência e configuração das variáveis de ambiente:

dependencies {
    implementation "io.unico:capture:5.33.0"
    // outras dependências...
}

android {
    defaultConfig {
        // 🔑 Injeção das variáveis do .env
        buildConfigField "String", "UNICO_BUNDLE_ID", "\"${project.env.get("UNICO_BUNDLE_ID") ?: ""}\""
        buildConfigField "String", "UNICO_HOST_KEY", "\"${project.env.get("UNICO_HOST_KEY") ?: ""}\""
    }
}
Enter fullscreen mode Exit fullscreen mode

2. A Bridge Principal - UnicoSdkModule.kt

Esta é a peça central que conecta JavaScript ao SDK nativo. Implementei usando handlers especializados para melhor organização:

class UnicoSdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {

    @ReactMethod
    fun captureSelfie(promise: Promise) {
        runOnMainThread {
            try {
                // Validações de permissão e activity
                val activity = activity ?: run {
                    promise.reject("ERROR", "Activity not found")
                    return@runOnMainThread
                }

                if (!hasPermissions()) {
                    promise.reject("PERMISSION_DENIED", "Permissão de câmera necessária")
                    return@runOnMainThread
                }

                // Configuração do SDK
                val config = UnicoConfig()
                val theme = UnicoTheme()

                AcessoBio(activity, callback)
                    .setAutoCapture(true)
                    .setSmartFrame(true)
                    .setEnvironment(Environment.UAT)
                    .setTheme(theme)
                    .build()
                    .prepareCamera(config, selfieCallback)

            } catch (e: Exception) {
                promise.reject("ERROR", "Erro geral: ${e.message}")
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Gerenciamento de Credenciais

Criei uma classe específica para gerenciar as credenciais de forma segura:

class UnicoConfig : AcessoBioConfigDataSource {
    override fun getBundleIdentifier(): String {
        return BuildConfig.UNICO_BUNDLE_ID ?: ""
    }

    override fun getHostKey(): String {
        return BuildConfig.UNICO_HOST_KEY ?: ""
    }
}
Enter fullscreen mode Exit fullscreen mode

As credenciais fluem do arquivo .envreact-native-configBuildConfigUnicoConfig.


🍎 Implementação iOS: Swift + Objective-C

1. Configuração do CocoaPods

No iOS, o gerenciamento de dependências é feito via CocoaPods:

# Podfile
target 'MeuApp' do
  # SDK da Unico
  pod 'unicocheck-ios'
  pod 'react-native-config', :path => '../node_modules/react-native-config'

  # Setup de permissões - apenas Camera
  setup_permissions(['Camera'])
end
Enter fullscreen mode Exit fullscreen mode

2. Bridge Header - Conectando Worlds

Um dos maiores desafios no iOS foi configurar corretamente o bridge header para conectar Objective-C com Swift:

// MeuApp-Bridging-Header.h
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
#import <React/RCTViewManager.h>
#import <React/RCTUtils.h>
Enter fullscreen mode Exit fullscreen mode

3. Arquitetura com Handlers Especializados

Implementei uma arquitetura modular no iOS usando handlers especializados:

@objc(UnicoSdk)
class UnicoSdkModule: RCTEventEmitter {

    @objc
    func captureSelfie(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
        DispatchQueue.main.async {
            guard let rootViewController = RCTPresentedViewController() else {
                reject("ERROR", "Não foi possível obter o view controller", nil)
                return
            }

            let captureHandler = SelfieCaptureHandler(
                resolve: resolve, 
                reject: reject, 
                eventEmitter: self
            )
            captureHandler.startCapture(from: rootViewController)
        }
    }
}

// Handler especializado para selfie
class SelfieCaptureHandler: NSObject {
    private let resolve: RCTPromiseResolveBlock
    private let reject: RCTPromiseRejectBlock
    private weak var eventEmitter: RCTEventEmitter?

    func startCapture(from viewController: UIViewController) {
        let manager = AcessoBioManager(viewController: viewController)
        manager?.setTheme(UnicoTheme())
        manager?.setEnvironment(.UAT)
        manager?.setSmartFrame(true)
        manager?.setAutoCapture(true)
        manager?.build().prepareSelfieCamera(self, config: UnicoConfig())
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Configuração de Variáveis no Info.plist

No iOS, as variáveis do .env são injetadas automaticamente no Info.plist:

<!-- Info.plist -->
<key>UNICO_SDK_KEY</key>
<string>$(UNICO_SDK_KEY)</string>
<key>UNICO_BUNDLE_IDENTIFIER</key>
<string>$(UNICO_BUNDLE_IDENTIFIER)</string>
Enter fullscreen mode Exit fullscreen mode

⚛️ React Native: Clean Architecture

1. Estrutura de Camadas

Implementei uma arquitetura limpa e escalável:

// Domain Layer - Regras de negócio
export enum DocumentType {
  CNH_FRENTE = 'CNH_FRENTE',
  CNH_VERSO = 'CNH_VERSO',
  RG_FRENTE = 'RG_FRENTE',
  RG_VERSO = 'RG_VERSO',
  CPF = 'CPF',
}

export type CapturedItem = {
  id: string;
  type: ActionType;
  documentType?: DocumentType;
  result: UnicoResult;
  timestamp: string;
  status: DocumentStatus;
};

// Use Case - Lógica de captura
export const executeCaptureSelfie = async (
  unicoRepository: UnicoRepository
): Promise<CapturedItem> => {
  const result = await unicoRepository.captureSelfie();

  if (!result.success || !result.data) {
    throw new Error(result.error || 'Erro na captura da selfie');
  }

  return {
    id: `selfie_${Date.now()}`,
    type: ActionType.SELFIE,
    result: result.data,
    timestamp: new Date().toISOString(),
    status: DocumentStatus.CAPTURED,
  };
};
Enter fullscreen mode Exit fullscreen mode

2. Infrastructure Layer - Conexão com Nativo

// UnicoSdkService.ts - Ponte com módulos nativos
import { NativeEventEmitter, NativeModules } from 'react-native';

const { UnicoSdk: UnicoSdkModule } = NativeModules;

export const testConnection = (): Promise<string> => {
  return UnicoSdkModule.testConnection();
};

export const captureSelfie = (): Promise<UnicoResult> => {
  return UnicoSdkModule.captureSelfie();
};

export const captureDocument = (
  documentType: DocumentType
): Promise<UnicoResult> => {
  return UnicoSdkModule.captureDocument(documentType);
};
Enter fullscreen mode Exit fullscreen mode

3. Presentation Layer - Hooks Especializados

// useCapture.ts - Hook para capturas
export const useCapture = () => {
  const [isLoading, setIsLoading] = useState(false);
  const { requestPermissionOrRedirect } = usePermissions();

  const captureSelfie = useCallback(async (): Promise<CapturedItem | null> => {
    try {
      const granted = await requestPermissionOrRedirect();
      if (!granted) return null;

      setIsLoading(true);
      const newItem = await useCases.captureSelfie.execute();

      Alert.alert('✅ Sucesso', 'Selfie capturada!');
      return newItem;
    } catch (error: unknown) {
      Alert.alert('Erro', (error as Error).message);
      return null;
    } finally {
      setIsLoading(false);
    }
  }, [requestPermissionOrRedirect, useCases.captureSelfie]);

  return { isLoading, captureSelfie };
};
Enter fullscreen mode Exit fullscreen mode

🎨 Funcionalidades Avançadas

1. Fluxo Guiado Inteligente

Implementei um sistema de fluxo guiado que orienta o usuário através dos passos necessários:

const DEFAULT_STEPS: FlowStep[] = [
  {
    id: 'selfie',
    title: 'Capturar Selfie',
    description: 'Primeiro, vamos tirar uma selfie sua',
    icon: '🤳',
  },
  {
    id: 'cnh_frente',
    title: 'CNH Frente',
    description: 'Agora capture a frente da sua CNH',
    icon: '🪪',
  },
  {
    id: 'cnh_verso',
    title: 'CNH Verso',
    description: 'Por último, capture o verso da CNH',
    icon: '🔄',
  },
  {
    id: 'submit',
    title: 'Enviar',
    description: 'Pronto! Vamos enviar tudo para verificação',
    icon: '📤',
  },
];
Enter fullscreen mode Exit fullscreen mode

2. Sistema de Eventos Nativos

Para melhor UX, implementei um sistema de eventos que permite ao React Native reagir a ações do usuário no SDK nativo:

// JavaScript - Escutando eventos nativos
import { NativeEventEmitter } from 'react-native';
const eventEmitter = new NativeEventEmitter(UnicoSdk);

eventEmitter.addListener('onErrorAcessoBio', (event) => {
  console.error('Erro do SDK:', event.code, event.description);
});

eventEmitter.addListener('onUserClosedCameraManually', () => {
  console.log('Usuário fechou a câmera');
});
Enter fullscreen mode Exit fullscreen mode

3. Personalização Visual Completa

Criei classes de tema que permitem personalizar completamente a interface do SDK:

// Android
class UnicoTheme : IAcessoBioTheme {
    override fun getColorBackground(): Any = "#F1F0F8"
    override fun getColorBoxMessage(): Any = "#FFFFFF"
    override fun getColorTextMessage(): Any = "#322E50"
    // ... 14 cores personalizáveis
}
Enter fullscreen mode Exit fullscreen mode
// iOS
class UnicoTheme: AcessoBioThemeDelegate {
    func getColorBackground() -> Any! {
        return UIColor(red: 0.94, green: 0.94, blue: 0.97, alpha: 1.0)
    }
    // ... mesmas cores, implementação iOS
}
Enter fullscreen mode Exit fullscreen mode

🚨 Principais Desafios e Soluções

1. Gerenciamento de Permissões

Desafio: Diferentes APIs de permissão em cada plataforma.

Solução: Abstração unificada com verificação prévia:

export const ensureCameraPermission = async (): Promise<boolean> => {
  try {
    const permissions = await checkPermissions();
    return permissions.camera;
  } catch (error) {
    console.error('Erro ao verificar permissões:', error);
    return false;
  }
};
Enter fullscreen mode Exit fullscreen mode

2. Thread Safety

Desafio: SDK requer UI thread, React Native executa em background thread.

Solução: Garantir execução na main thread:

// Android
private fun runOnMainThread(action: () -> Unit) {
    if (Looper.myLooper() == Looper.getMainLooper()) {
        action()
    } else {
        Handler(Looper.getMainLooper()).post(action)
    }
}
Enter fullscreen mode Exit fullscreen mode
// iOS
@objc
func captureSelfie(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
    DispatchQueue.main.async {
        // Operações de UI aqui
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Configuração de Credenciais

Desafio: Credenciais precisam estar disponíveis em runtime sem hardcode.

Solução: Fluxo .envreact-native-config → plataforma nativa:

# .env
UNICO_BUNDLE_ID=com.empresa.app
UNICO_HOST_KEY=chave_secreta
Enter fullscreen mode Exit fullscreen mode

4. Debugging Complexo

Desafio: Debuggar código que atravessa 3 camadas (JS → Bridge → SDK).

Solução: Logging detalhado em cada camada:

Log.d(TAG, "✅ AcessoBio.prepareCamera() chamado com sucesso")
Log.e(TAG, "❌ Exception geral em captureSelfie", e)
Enter fullscreen mode Exit fullscreen mode

📊 Resultados Alcançados

Funcionalidades Implementadas:

  • Captura de selfie com biometria facial
  • Captura de documentos (CNH, RG, CPF) com frames guiados
  • Fluxo guiado completo de verificação
  • Gerenciamento de permissões nativo
  • Personalização visual completa
  • Eventos nativos para JavaScript
  • Arquitetura escalável e testável
  • Documentação completa com guias passo-a-passo

Métricas de Qualidade:

  • 🎯 100% funcional em dispositivos reais
  • 📱 Suporte completo iOS e Android
  • 🧪 Arquitetura testável com separação de responsabilidades
  • 📚 Documentação completa com troubleshooting
  • 🔧 Configuração simplificada via arquivo .env

🎓 Lições Aprendidas

1. Documentação é Crucial

A falta de documentação específica para React Native custou semanas de desenvolvimento. Por isso, criei uma documentação detalhada para evitar que outros passem pelo mesmo problema.

2. Arquitetura Limpa Compensa

Investir tempo em arquitetura limpa desde o início facilitou enormemente a manutenção e extensão do código.

3. Thread Safety é Crítico

Problemas de thread causaram crashes esporádicos. Garantir execução na main thread é fundamental.

4. Testes em Dispositivos Reais

Simuladores não são suficientes para testar funcionalidades de câmera e biometria.

5. Comunidade é Poderosa

Compartilhar conhecimento gera valor exponencial para toda a comunidade.


🎯 Conclusão

Desenvolver uma bridge nativa para o SDK da Unico foi um desafio técnico complexo, mas extremamente gratificante. O resultado é uma solução robusta, bem documentada e pronta para uso em produção.

O mais importante: esta implementação agora está disponível para toda a comunidade React Native, evitando que outros desenvolvedores enfrentem os mesmos obstáculos que encontrei.

Principais Benefícios para a Comunidade:

  1. ⏰ Economia de Tempo: Semanas de desenvolvimento economizadas
  2. 📚 Conhecimento Compartilhado: Documentação completa e detalhada
  3. 🏗️ Arquitetura Sólida: Base para implementações futuras
  4. 🔧 Troubleshooting: Soluções para problemas reais
  5. 🤝 Open Source: Disponível para todos

📞 Como Contribuir

Este projeto é open source e feito para a comunidade. Se você:

  • Tem sugestões de melhorias
  • 🐛 Encontrou bugs ou problemas
  • 📝 Quer melhorar a documentação
  • 🌍 Pode traduzir para outros idiomas
  • 💡 Tem ideias para novas funcionalidades

Sua contribuição é muito bem-vinda!

Links Úteis:

  • 📂 Repositório: GitHub - SDK Unico Bridge
  • 📚 Documentação Completa: Guias detalhados para iOS e Android
  • 🐛 Issues: Reporte problemas ou sugira melhorias
  • 💬 Discussões: Participe da comunidade

Se este artigo te ajudou, considere:

  • Dar uma estrela no repositório
  • 📤 Compartilhar com outros desenvolvedores
  • 💬 Deixar feedback nos comentários
  • 🤝 Contribuir com melhorias

Top comments (1)

Collapse
 
jamey_h66 profile image
Jamey H

Nice posting! Looking forward to collaborating with you