Instalación IOS

📌 Documentación del SDK Milio para React Native (iOS)

🚀 Requisitos Previos

Antes de comenzar, asegúrate de cumplir con los siguientes requisitos:

  • React Native >=0.60 (se recomienda usar versiones actuales).
  • Xcode >=16.0 instalado.
  • CocoaPods (pod --version debería devolver una versión válida).
  • Token de transaccion proporcionado por Milio.
  • Version minima iOS 14.

1. Integración del SDK de iOS en React Native

✅ 1️⃣ Instalar la Dependencia del SDK con CocoaPods

Asegúrate de que CocoaPods esté instalado:

sudo gem install cocoapods
 

Luego, abre la carpeta ios y actualiza las dependencias:

pod deintegrate es opcional , este comando es para empezar desde cero con las dependencias nativas.

cd ios
pod deintegrate

Instalacion automatica

Si el SDK de Milio se distribuye a través de un repositorio privado de CocoaPods, agrégalo en tu Podfile (ios/Podfile):

source '[email protected]:miliopay/miliopods-specs.git'
source 'https://cdn.cocoapods.org/'

platform :ios, '14.0'
use_frameworks!

target 'SDKTestAppSpecsPrivate' do
  
  pod 'sdk-mobile-ios',
  :git => 'https://bitbucket.org/miliopay/sdk-mobile-ios.git',
  :tag => '0.0.98'
  
end


Finalmente, instala las dependencias:

pod install --repo-update

2. Crear el Bridge entre React Native y el SDK de iOS

Para exponer el SDK de iOS a React Native, crea el archivo:

   **ios/MilioSdkModule.m**
// ios/MilioSdk.m
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>

@interface RCT_EXTERN_MODULE(MilioSdk, RCTEventEmitter)

// ============================================
// MARK: - Inicialización
// ============================================
// ✅ CAMBIO: Ya no recibe token como parámetro
RCT_EXTERN_METHOD(initializeSdk:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

// ============================================
// MARK: - Configuración de Tema
// ============================================
RCT_EXTERN_METHOD(setThemeColors:(NSDictionary *)colorDict
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

// ============================================
// MARK: - Configuración de Logo
// ============================================
RCT_EXTERN_METHOD(setLogoUrl:(NSString *)url
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(clearLogoUrl:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(setWidthLogo:(nonnull NSNumber *)width
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

// ============================================
// MARK: - Configuración de Fuente
// ============================================
RCT_EXTERN_METHOD(setFont:(NSString *)fontOption
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

// ============================================
// MARK: - Configuración de Ambiente
// ============================================
RCT_EXTERN_METHOD(setEnvironment:(NSString *)environment
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

// ============================================
// MARK: - Configuración de Idioma
// ============================================
RCT_EXTERN_METHOD(setLanguage:(NSString *)languageCode
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

// ============================================
// MARK: - Métodos de Transacción
// ============================================

// QR - ✅ Solo recibe tokenTransaction
RCT_EXTERN_METHOD(openMilioSdkQr:(NSString *)tokenTransaction
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

// Recargar Manual - ✅ Solo recibe tokenTransaction
RCT_EXTERN_METHOD(openMilioPayManualCard:(NSString *)tokenTransaction
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

// Transferencia Inmediata - ✅ Solo recibe tokenTransaction
RCT_EXTERN_METHOD(openMilioPayAddFoundManualCard:(NSString *)tokenTransaction
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

// Recargar Billetera - ✅ Solo recibe tokenTransaction
RCT_EXTERN_METHOD(openMilioPayRechargeWallet:(NSString *)tokenTransaction
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

// Tokenización de Tarjetas - ✅ Solo recibe tokenTransaction
RCT_EXTERN_METHOD(openPaymentAliasConfiguration:(NSString *)tokenTransaction
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

// Transferencia Global (Cross-Border) - ✅ Solo recibe tokenTransaction
RCT_EXTERN_METHOD(openCrossBorderRequest:(NSString *)tokenTransaction
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

// Envío de Dinero (Outbound) - ✅ Solo recibe tokenTransaction
RCT_EXTERN_METHOD(openSendingMoneyRequest:(NSString *)tokenTransaction
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(setWalletRechargeConfig:(NSNumber *)minAmount
                  maxAmount:(NSNumber *)maxAmount
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

// ============================================
// MARK: - Debug
// ============================================
RCT_EXTERN_METHOD(debugAvailableImages:(NSString *)imageNameToTest)

+ (BOOL)requiresMainQueueSetup
{
    return YES;
}

@end

Ahora, crea el archivo Swift para implementar los métodos nativos:

3. Enlazar el Módulo en React Native

Nueva actualizacion del cliente React native iOS

Resumen de Mejoras en la Nueva Versión del Cliente Milio SDK

Inicialización Automática del SDK
El SDK se inicializa automáticamente al iniciar sesión con el token de autenticación.

Configuración Dinámica de Colores
Se aplica un tema visual personalizado desde React Native para adaptarse al branding del cliente.

Soporte Completo de Eventos del SDK
El cliente ahora puede recibir notificaciones en tiempo real:

  • Éxito o error en la transacción
  • Finalización completa de la operación
  • Cierre del SDK por el usuario
ios/MilioSdkModule.swift
import Foundation
import React
import UIKit
import sdk_mobile_ios

@objc(MilioSdk)
class MilioSDKBridge: RCTEventEmitter {

    private var delegateAdapter: DelegateAdapter?
    private var hasListeners = false

    // ============================================
    // MARK: - Setup
    // ============================================
    
    override static func requiresMainQueueSetup() -> Bool {
        return true
    }

    @objc override static func moduleName() -> String {
        return "MilioSdk"
    }

    override init() {
        super.init()
        // ✅ Inicializar delegate automáticamente
        initializeDelegate()
    }

    override func startObserving() {
        hasListeners = true
    }

    override func stopObserving() {
        hasListeners = false
    }

    override func supportedEvents() -> [String]! {
        return [
            "onTransactionError",
            "onTransactionSuccess",
            "onTransactionCompleted",
            "onTransactionClosed",
            "onTransactionBalanceUpdated",
            "MilioSdkDidClose",
            "MilioSdkDidUpdateBalance"
        ]
    }

    // ============================================
    // MARK: - Inicialización
    // ============================================
    
    private func initializeDelegate() {
        if delegateAdapter == nil {
            delegateAdapter = DelegateAdapter(bridge: self)
            TransactionConfigSdk.setTransactionCallback(delegateAdapter!)
            print("✅ iOS SDK: Delegate inicializado automáticamente")
        }
    }
    
    @objc func initializeSdk(
        _ resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        // ✅ CAMBIO: Ya no recibe token, solo inicializa el delegate
        initializeDelegate()
        print("✅ iOS SDK: Inicializado correctamente")
        resolve("SDK inicializado correctamente")
    }

    // ============================================
    // MARK: - Configuración de Tema
    // ============================================
    
    @objc func setThemeColors(
        _ colorDict: NSDictionary, 
        resolver resolve: RCTPromiseResolveBlock,
        rejecter reject: RCTPromiseRejectBlock
    ) {
        TransactionConfigSdk.setThemeColors(
            primaryColor: colorDict["primaryColor"] as? String ?? "#808080",
            secondaryColor: colorDict["secondaryColor"] as? String ?? "#242424",
            btnColorEnabled: colorDict["btnColorEnabled"] as? String ?? "#809BFF",
            btnColorDisabled: colorDict["btnColorDisabled"] as? String ?? "#D3D3D3",
            btnTextColorEnabled: colorDict["btnTextColorEnabled"] as? String ?? "#FFFFFF",
            btnTextColorDisabled: colorDict["btnTextColorDisabled"] as? String ?? "#888888",
            layoutBackgroundColor: colorDict["layoutBackgroundColor"] as? String ?? "#ffffff",
            textViewColor: colorDict["textViewColor"] as? String ?? "#242424",
            inputBorderColor: colorDict["inputBorderColor"] as? String ?? "#808080",
            textSelectionColor: colorDict["textSelectionColor"] as? String ?? "#809BFF"
        )
        print("✅ iOS SDK: Colores aplicados")
        resolve("Colores aplicados correctamente")
    }

    // ============================================
    // MARK: - Configuración de Logo
    // ============================================
    
    @objc func setLogoUrl(
        _ url: NSString,
        resolver resolve: RCTPromiseResolveBlock,
        rejecter reject: RCTPromiseRejectBlock
    ) {
        let str = (url as String).trimmingCharacters(in: .whitespacesAndNewlines)
        // if str.isEmpty {
        //     TransactionConfigSdk.setLogoURL(nil)
        //     print("✅ iOS SDK: Logo removido")
        //     resolve("Logo URL removida correctamente")
        //     return
        // }
        // guard URL(string: str) != nil else {
        //     reject("INVALID_URL", "El string proporcionado no es una URL válida", nil)
        //     return
        // }
        TransactionConfigSdk.setLogoURL(str)
        print("✅ iOS SDK: Logo URL configurado: \(str)")
        resolve("Logo URL guardada correctamente")
    }
    
    @objc func clearLogoUrl(
        _ resolve: RCTPromiseResolveBlock,
        rejecter reject: RCTPromiseRejectBlock
    ) {
        TransactionConfigSdk.setLogoURL(nil)
        print("✅ iOS SDK: Logo removido")
        resolve("Logo URL removida correctamente")
    }
    
    @objc func setWidthLogo(
        _ width: NSNumber,
        resolver resolve: RCTPromiseResolveBlock,
        rejecter reject: RCTPromiseRejectBlock
    ) {
        let widthValue = width.intValue
        TransactionConfigSdk.setWidthLogo(widthValue)
        print("✅ iOS SDK: Logo width configurado: \(widthValue)")
        resolve("Logo width configurado: \(widthValue)")
    }
    
    // ============================================
    // MARK: - Configuración de Fuente
    // ============================================
    
    @objc func setFont(
        _ fontOption: NSString,
        resolver resolve: RCTPromiseResolveBlock,
        rejecter reject: RCTPromiseRejectBlock
    ) {
        let fontString = fontOption as String
        switch fontString.lowercased() {
        case "satoshi":
            TransactionConfigSdk.setFont(option: .satoshi)
        default:
            TransactionConfigSdk.setFont(option: .satoshi)
        }
        print("✅ iOS SDK: Font configurada: \(fontString)")
        resolve("Font configurada: \(fontString)")
    }
    
    // ============================================
    // MARK: - Configuración de Ambiente
    // ============================================
    
    @objc func setEnvironment(
        _ environment: NSString,
        resolver resolve: RCTPromiseResolveBlock,
        rejecter reject: RCTPromiseRejectBlock
    ) {
        TransactionConfigSdk.setEnvironment(environment as String)
        print("✅ iOS SDK: Ambiente configurado: \(environment)")
        resolve("Ambiente configurado: \(environment)")
    }

    // ============================================
    // MARK: - Configuración de Idioma
    // ============================================
    
    @objc func setLanguage(
        _ languageCode: NSString,
        resolver resolve: RCTPromiseResolveBlock,
        rejecter reject: RCTPromiseRejectBlock
    ) {
        TransactionConfigSdk.setLanguage(languageCode as String)
        print("✅ iOS SDK: Idioma configurado: \(languageCode)")
        resolve("Idioma configurado: \(languageCode)")
    }

    // ============================================
    // MARK: - Debug
    // ============================================
    
    @objc func debugAvailableImages(_ imageName: NSString) {
        TransactionConfigSdk.debugImagesInResourceBundle()
    }

    // ============================================
    // MARK: - Métodos de Transacción
    // ============================================

    @objc func openMilioSdkQr(
        _ tokenTransaction: NSString, 
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        openTransaction(
            type: .qr, 
            tokenTransaction: tokenTransaction as String, 
            resolve: resolve,
            reject: reject
        )
    }

    @objc func openMilioPayManualCard(
        _ tokenTransaction: NSString, 
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        openTransaction(
            type: .manual, 
            tokenTransaction: tokenTransaction as String, 
            resolve: resolve,
            reject: reject
        )
    }

    @objc func openMilioPayAddFoundManualCard(
        _ tokenTransaction: NSString, 
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        openTransaction(
            type: .addFunds, 
            tokenTransaction: tokenTransaction as String, 
            resolve: resolve,
            reject: reject
        )
    }

    @objc func openMilioPayRechargeWallet(
        _ tokenTransaction: NSString, 
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        openTransaction(
            type: .recharge, 
            tokenTransaction: tokenTransaction as String, 
            resolve: resolve,
            reject: reject
        )
    }
    
    @objc func openPaymentAliasConfiguration(
        _ tokenTransaction: NSString,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        openTransaction(
            type: .aliasConfiguration,
            tokenTransaction: tokenTransaction as String,
            resolve: resolve,
            reject: reject
        )
    }
    
    @objc func openCrossBorderRequest(
        _ tokenTransaction: NSString,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        openTransaction(
            type: .crossBorder,
            tokenTransaction: tokenTransaction as String,
            resolve: resolve,
            reject: reject
        )
    }

    @objc func openSendingMoneyRequest(
        _ tokenTransaction: NSString,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        openTransaction(
            type: .sendingMoney,
            tokenTransaction: tokenTransaction as String,
            resolve: resolve,
            reject: reject
        )
    }

    @objc func openSendingMoneyRequestLocal(
        _ tokenTransaction: NSString,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        openTransaction(
            type: .sendingMoneyLocal,
            tokenTransaction: tokenTransaction as String,
            resolve: resolve,
            reject: reject
        )
    }

    @objc func openOutboundTransferSdk(
        _ tokenTransaction: NSString,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        openTransaction(
            type: .outboundTransfer,
            tokenTransaction: tokenTransaction as String,
            resolve: resolve,
            reject: reject
        )
    }

    // ============================================
    // MARK: - Helper Methods
    // ============================================

    private enum TransactionType {
        case qr
        case manual
        case addFunds
        case recharge
        case aliasConfiguration
        case crossBorder
        case sendingMoney
        case sendingMoneyLocal
        case outboundTransfer
    }

    private func openTransaction(
        type: TransactionType,
        tokenTransaction: String,
        resolve: @escaping RCTPromiseResolveBlock,
        reject: @escaping RCTPromiseRejectBlock
    ) {
        // ✅ CAMBIO CRÍTICO: Ya no requiere authToken
        print("🚀 iOS SDK: Iniciando transacción tipo: \(type)")
        print("🔑 iOS SDK: Token de transacción: \(String(tokenTransaction.prefix(20)))...")

        // ✅ Asegurar que el delegate esté inicializado
        initializeDelegate()

        DispatchQueue.main.async { 
            guard
                let rootVC = UIApplication.shared.connectedScenes
                    .compactMap({ $0 as? UIWindowScene })
                    .flatMap({ $0.windows })
                    .first(where: { $0.isKeyWindow })?.rootViewController
            else {
                print("❌ iOS SDK: No se encontró rootViewController")
                reject("NO_VIEW_CONTROLLER", "No se encontró un controlador de vista válido", nil)
                return
            }

            // ✅ CAMBIO: Todas las llamadas ahora solo usan tokenTransaction
            switch type {
            case .qr:
                print("📱 iOS SDK: Abriendo QR Scanner")
                TransactionConfigSdk.openMilioSdkQr(
                    from: rootVC, 
                    tokenTransaction: tokenTransaction
                )
            case .manual:
                print("💳 iOS SDK: Abriendo Manual Card")
                TransactionConfigSdk.openMilioPayManualCard(
                    from: rootVC, 
                    tokenTransaction: tokenTransaction
                )
            case .addFunds:
                print("💰 iOS SDK: Abriendo Add Funds")
                TransactionConfigSdk.openMilioPayAddFoundManualCard(
                    from: rootVC, 
                    tokenTransaction: tokenTransaction
                )
            case .recharge:
                print("🔄 iOS SDK: Abriendo Recharge Wallet")
                TransactionConfigSdk.openMilioPayRechargeWallet(
                    from: rootVC, 
                    tokenTransaction: tokenTransaction
                )
            case .aliasConfiguration:
                print("🔗 iOS SDK: Abriendo Alias Configuration")
                TransactionConfigSdk.openPaymentAliasConfigurationSheet(
                    from: rootVC,
                    tokenTransaction: tokenTransaction
                )
            case .crossBorder:
                print("🌍 iOS SDK: Abriendo Cross Border")
                TransactionConfigSdk.openTransferGlobal(
                    from: rootVC,
                    tokenTransaction: tokenTransaction
                )
            case .sendingMoney:
                print("📤 iOS SDK: Abriendo Sending Money")
                TransactionConfigSdk.openSendingMoneyRequest(
                    from: rootVC,
                    tokenTransaction: tokenTransaction
                )
            case .sendingMoneyLocal:
                print("📍 iOS SDK: Abriendo Sending Money Local")
                TransactionConfigSdk.openSendingMoneyRequestLocal(
                    from: rootVC,
                    tokenTransaction: tokenTransaction
                )
            case .outboundTransfer:
                print("🚀 iOS SDK: Abriendo Outbound Transfer")
                TransactionConfigSdk.openOutboundTransferSdk(
                    from: rootVC,
                    tokenTransaction: tokenTransaction
                )
            }

            print("✅ iOS SDK: Transacción \(type) iniciada correctamente")
            resolve("Transacción \(type) iniciada correctamente")
        }
    }

    // ============================================
    // MARK: - Delegate Adapter Class
    // ============================================
    
    class DelegateAdapter: NSObject, MilioSDKDelegate {
        weak var bridge: MilioSDKBridge?

        init(bridge: MilioSDKBridge) {
            self.bridge = bridge
            super.init()
            print("✅ iOS SDK: DelegateAdapter creado")
        }

        func onTransactionError(errorCode: Int, errorMessage: String) {
            print("❌ iOS SDK Delegate: Error - Código: \(errorCode), Mensaje: \(errorMessage)")
            bridge?.sendEvent(
                withName: "onTransactionError",
                body: [
                    "errorCode": errorCode,
                    "errorMessage": errorMessage
                ]
            )
        }

        func onTransactionSuccess(amount: Double, message: String) {
            print("✅ iOS SDK Delegate: Transacción exitosa - Monto: \(amount), Mensaje: \(message)")
            bridge?.sendEvent(
                withName: "onTransactionSuccess",
                body: [
                    "amount": amount,
                    "message": message
                ]
            )
        }

        func onTransactionCompleted(amount: Double, isAddition: Bool) {
            print("✅ iOS SDK Delegate: Transacción completada - Monto: \(amount), Es adición: \(isAddition)")
            bridge?.sendEvent(
                withName: "onTransactionCompleted",
                body: [
                    "amount": amount,
                    "isAddition": isAddition
                ]
            )
        }

        func didCloseSdk() {
            print("🚪 iOS SDK Delegate: SDK cerrado")
            DispatchQueue.main.async {
                self.bridge?.sendEvent(withName: "MilioSdkDidClose", body: nil)
                self.bridge?.sendEvent(withName: "onTransactionClosed", body: nil)
            }
        }

        func didUpdateBalance(newBalance: Double) {
            print("💰 iOS SDK Delegate: Balance actualizado - Nuevo balance: \(newBalance)")
            bridge?.sendEvent(
                withName: "MilioSdkDidUpdateBalance",
                body: newBalance
            )
            bridge?.sendEvent(
                withName: "onTransactionBalanceUpdated",
                body: ["newBalance": newBalance]
            )
        }
    }
}

Codigo de javascript

Esto deja claro que:

  • Es un ejemplo funcional, no producción.
  • Explica el uso de initializeSdk, los colores, y los eventos.
  • Ayuda a otros devs a saber dónde personalizar e integrar.
import React, { useEffect, useState } from 'react';
import {
  SafeAreaView,
  ScrollView,
  View,
  Text,
  StyleSheet,
  TouchableOpacity,
  StatusBar,
  Alert,
  NativeEventEmitter,
  Platform,
} from 'react-native';

import Icon from '@react-native-vector-icons/material-icons';

import MilioSdk from './MilioSdk';

const MilioSdkEmitter = new NativeEventEmitter(MilioSdk);

// ============================================
// MARK: - Types & Interfaces
// ============================================

interface ThemeConfig {
  id: string;
  name: string;
  icon: string;
  primaryColor: string;
  secondaryColor: string;
  btnColorEnabled: string;
  btnColorDisabled: string;
  btnTextColorEnabled: string;
  btnTextColorDisabled: string;
  layoutBackgroundColor: string;
  textViewColor: string;
  inputBorderColor: string;
  textSelectionColor: string;
}

interface EnvironmentConfig {
  id: string;
  name: string;
  icon: string;
  envString: string;
  description: string;
}

interface LanguageConfig {
  id: string;
  code: string;
  name: string;
  flag: string;
}

// ============================================
// MARK: - Configuration Data
// ============================================

const AVAILABLE_THEMES: ThemeConfig[] = [
  {
    id: 'default',
    name: 'Default (Blue)',
    icon: 'palette',
    primaryColor: '#808080',
    secondaryColor: '#242424',
    btnColorEnabled: '#809BFF',
    btnColorDisabled: '#D3D3D3',
    btnTextColorEnabled: '#FFFFFF',
    btnTextColorDisabled: '#888888',
    layoutBackgroundColor: '#ffffff',
    textViewColor: '#242424',
    inputBorderColor: '#808080',
    textSelectionColor: '#809BFF',
  },
  {
    id: 'dark',
    name: 'Dark Mode',
    icon: 'brightness-3',
    primaryColor: '#CCCCCC',
    secondaryColor: '#FFFFFF',
    btnColorEnabled: '#1E1E1E',
    btnColorDisabled: '#3A3A3A',
    btnTextColorEnabled: '#FFFFFF',
    btnTextColorDisabled: '#888888',
    layoutBackgroundColor: '#121212',
    textViewColor: '#FFFFFF',
    inputBorderColor: '#444444',
    textSelectionColor: '#BB86FC',
  },
  {
    id: 'green',
    name: 'Green Nature',
    icon: 'eco',
    primaryColor: '#6B8E23',
    secondaryColor: '#2F4F2F',
    btnColorEnabled: '#32CD32',
    btnColorDisabled: '#90EE90',
    btnTextColorEnabled: '#FFFFFF',
    btnTextColorDisabled: '#808080',
    layoutBackgroundColor: '#F0FFF0',
    textViewColor: '#2F4F2F',
    inputBorderColor: '#6B8E23',
    textSelectionColor: '#32CD32',
  },
  {
    id: 'purple',
    name: 'Purple Dream',
    icon: 'stars',
    primaryColor: '#9370DB',
    secondaryColor: '#4B0082',
    btnColorEnabled: '#8A2BE2',
    btnColorDisabled: '#DDA0DD',
    btnTextColorEnabled: '#FFFFFF',
    btnTextColorDisabled: '#888888',
    layoutBackgroundColor: '#F8F4FF',
    textViewColor: '#4B0082',
    inputBorderColor: '#9370DB',
    textSelectionColor: '#8A2BE2',
  },
  {
    id: 'orange',
    name: 'Sunset Orange',
    icon: 'wb-sunny',
    primaryColor: '#FF8C00',
    secondaryColor: '#8B4513',
    btnColorEnabled: '#FF6347',
    btnColorDisabled: '#FFB6C1',
    btnTextColorEnabled: '#FFFFFF',
    btnTextColorDisabled: '#888888',
    layoutBackgroundColor: '#FFF5EE',
    textViewColor: '#8B4513',
    inputBorderColor: '#FF8C00',
    textSelectionColor: '#FF6347',
  },
];

const AVAILABLE_ENVIRONMENTS: EnvironmentConfig[] = [
  {
    id: 'sandbox',
    name: 'Sandbox',
    icon: 'science',
    envString: 'SANDBOX',
    description: 'Ambiente aislado para testing',
  },
  {
    id: 'prod',
    name: 'Production',
    icon: 'cloud-done',
    envString: 'PROD',
    description: '⚠️ Ambiente de producción',
  },
];

const AVAILABLE_LANGUAGES: LanguageConfig[] = [
  { id: 'en', code: 'en', name: 'English', flag: '🇺🇸' },
  { id: 'es', code: 'es', name: 'Español', flag: '🇪🇸' },
];

// ============================================
// MARK: - Main Component
// ============================================

const App = () => {
  // State
  const [loading, setLoading] = useState(false);
  const [selectedEnvironment, setSelectedEnvironment] = useState('sandbox');
  const [selectedTheme, setSelectedTheme] = useState('default');
  const [selectedLanguage, setSelectedLanguage] = useState('es');
  const [showConfig, setShowConfig] = useState(false);

  // ============================================
  // MARK: - SDK Event Listeners
  // ============================================

  useEffect(() => {
    console.log('📡 Configurando listeners de eventos del SDK');

    const s1 = MilioSdkEmitter.addListener(
      'onTransactionSuccess',
      ({ message, amount }) => {
        console.log('✅ Evento onTransactionSuccess recibido:', {
          message,
          amount,
        });
        Alert.alert('✅ Transacción Exitosa', `Monto: $${amount}\n${message}`);
      },
    );

    const s2 = MilioSdkEmitter.addListener(
      'onTransactionCompleted',
      ({ amount, isAddition }) => {
        console.log('✅ Evento onTransactionCompleted recibido:', {
          amount,
          isAddition,
        });
        Alert.alert(
          '✅ Transacción Completada',
          `Monto: $${amount}\n${isAddition ? 'Suma' : 'Resta'}`,
        );
      },
    );

    const s3 = MilioSdkEmitter.addListener(
      'onTransactionError',
      ({ errorCode, errorMessage }) => {
        console.log('❌ Evento onTransactionError recibido:', {
          errorCode,
          errorMessage,
        });
        Alert.alert(
          '❌ Error en Transacción',
          `Código: ${errorCode}\n${errorMessage}`,
        );
      },
    );

    const s4 = MilioSdkEmitter.addListener('MilioSdkDidClose', () => {
      console.log(
        '🚪 Evento MilioSdkDidClose recibido - SDK cerrado por el usuario',
      );
    });

    return () => {
      console.log('🔌 Limpiando listeners de eventos del SDK');
      s1.remove();
      s2.remove();
      s3.remove();
      s4.remove();
    };
  }, []);

  // ============================================
  // MARK: - Initialization
  // ============================================

  useEffect(() => {
    const bootstrap = async () => {
      try {
        console.log('🚀 Iniciando bootstrap de la aplicación');

        await MilioSdk.initializeSdk();
        console.log('✅ SDK inicializado');

        // Aplicar configuración inicial
        await applyTheme(AVAILABLE_THEMES[0]);
        await MilioSdk.setEnvironment('SANDBOX');
        await MilioSdk.setLanguage('en');
        await MilioSdk.setFont('satoshi');

        // Configurar logo
        const logoUrl =
          'https://cdn-sandbox.milio.com.co/logo-acme.png';
        await MilioSdk.setLogoUrl(logoUrl);
        await MilioSdk.setWidthLogo(100);
        console.log('✅ Logo URL configurada:', logoUrl);
        console.log('✅ Bootstrap completado exitosamente');
      } catch (err: any) {
        console.error('❌ Error en bootstrap:', err);
        Alert.alert('Error al iniciar', err.message || String(err));
      }
    };

    bootstrap();
  }, []);

  // ============================================
  // MARK: - Configuration Handlers
  // ============================================

  const applyTheme = async (theme: ThemeConfig) => {
    try {
      await MilioSdk.setThemeColors({
        primaryColor: theme.primaryColor,
        secondaryColor: theme.secondaryColor,
        btnColorEnabled: theme.btnColorEnabled,
        btnColorDisabled: theme.btnColorDisabled,
        btnTextColorEnabled: theme.btnTextColorEnabled,
        btnTextColorDisabled: theme.btnTextColorDisabled,
        layoutBackgroundColor: theme.layoutBackgroundColor,
        textViewColor: theme.textViewColor,
        inputBorderColor: theme.inputBorderColor,
        textSelectionColor: theme.textSelectionColor,
      });
      setSelectedTheme(theme.id);
      console.log('🎨 Tema aplicado:', theme.name);
      Alert.alert('🎨 Tema Aplicado', theme.name);
    } catch (error) {
      console.error('❌ Error aplicando tema:', error);
    }
  };

  const changeEnvironment = async (env: EnvironmentConfig) => {
    try {
      await MilioSdk.setEnvironment(env.envString);
      setSelectedEnvironment(env.id);
      console.log('🌍 Ambiente cambiado a:', env.name);
      Alert.alert('🌍 Ambiente Cambiado', env.name);
    } catch (error) {
      console.error('❌ Error cambiando ambiente:', error);
    }
  };

  const changeLanguage = async (lang: LanguageConfig) => {
    try {
      await MilioSdk.setLanguage(lang.code);
      setSelectedLanguage(lang.code);
      console.log('🌐 Idioma cambiado a:', lang.name);
      Alert.alert('🌐 Idioma Cambiado', lang.name);
    } catch (error) {
      console.error('❌ Error cambiando idioma:', error);
    }
  };

  // ============================================
  // MARK: - Transaction Handlers
  // ============================================

  const openMilioPayManualCard = async () => {
    console.log('🚀 [openMilioPayManualCard] Iniciando...');

    console.log('MODULES ', MilioSdk);

    try {
			//Se requiere el argumento jsonData, este debe contener la información encriptada para la transacción
      const jsonData = "eyJhbGciOiJIU..."
      await MilioSdk.openMilioPayManualCard(jsonData);
      console.log('✅ [openMilioPayManualCard] SDK abierto exitosamente');
    } catch (error) {
      console.error('❌ [openMilioPayManualCard] Error general:', error);
      Alert.alert('Error', 'No se pudo ejecutar la operación');
    }
  };

  const handleTransferenciaInmediata = async () => {
    console.log('🚀 [openMilioPayAddFoundManualCard] Iniciando...');

    console.log('MODULES ', MilioSdk);

    try {
			//Se requiere el argumento jsonData, este debe contener la información encriptada para la transacción
      const jsonData = "eyJhbGciOiJIU..."
      await MilioSdk.openMilioPayAddFoundManualCard(jsonData);
      console.log('✅ [openMilioPayAddFoundManualCard] SDK abierto exitosamente');
    } catch (error) {
      console.error('❌ [openMilioPayAddFoundManualCard] Error general:', error);
      Alert.alert('Error', 'No se pudo ejecutar la operación');
    }
  };

  const handleRecargarBilletera = async () => {
    console.log('🚀 [openMilioPayRechargeWallet] Iniciando...');

    console.log('MODULES ', MilioSdk);

    try {
			//Se requiere el argumento jsonData, este debe contener la información encriptada para la transacción
      const jsonData = "eyJhbGciOiJIU..."
      await MilioSdk.openMilioPayRechargeWallet(jsonData);
      console.log('✅ [openMilioPayRechargeWallet] SDK abierto exitosamente');
    } catch (error) {
      console.error('❌ [openMilioPayRechargeWallet] Error general:', error);
      Alert.alert('Error', 'No se pudo ejecutar la operación');
    }
  };

  const handleAliasConfig = async () => {
    console.log('🚀 [openPaymentAliasConfiguration] Iniciando...');

    console.log('MODULES ', MilioSdk);

    try {
			//Se requiere el argumento jsonData, este debe contener la información encriptada para la  transacción
      const jsonData = "eyJhbGciOiJIU..."
      await MilioSdk.openPaymentAliasConfiguration(jsonData);
      console.log('✅ [openPaymentAliasConfiguration] SDK abierto exitosamente');
    } catch (error) {
      console.error('❌ [openPaymentAliasConfiguration] Error general:', error);
      Alert.alert('Error', 'No se pudo ejecutar la operación');
    }
  };

  const handleCrossBorderRequest = async () => {
    console.log('🚀 [openCrossBorderRequest] Iniciando...');

    console.log('MODULES ', MilioSdk);

    try {
			//Se requiere el argumento jsonData, este debe contener la información encriptada para la transacción
      const jsonData = "eyJhbGciOiJIU..."
      await MilioSdk.openCrossBorderRequest(jsonData);
      console.log('✅ [openCrossBorderRequest] SDK abierto exitosamente');
    } catch (error) {
      console.error('❌ [openCrossBorderRequest] Error general:', error);
      Alert.alert('Error', 'No se pudo ejecutar la operación');
    }
  };

  const handleSendingMoneyRequest = async () => {
    console.log('🚀 [openSendingMoneyRequest] Iniciando...');

    console.log('MODULES ', MilioSdk);

    try {
			//Se requiere el argumento jsonData, este debe contener la información encriptada para la transacción
      const jsonData = "eyJhbGciOiJIU..."
      await MilioSdk.openSendingMoneyRequest(jsonData);
      console.log('✅ [openSendingMoneyRequest] SDK abierto exitosamente');
    } catch (error) {
      console.error('❌ [openSendingMoneyRequest] Error general:', error);
      Alert.alert('Error', 'No se pudo ejecutar la operación');
    }
  };

  // ============================================
  // MARK: - Render
  // ============================================

  return (
    <>
      <SafeAreaView style={{ flex: 0, backgroundColor: '#0048ff' }} />
      <SafeAreaView style={styles.container}>
        <StatusBar backgroundColor="#f8f9fa" barStyle="dark-content" />

        {/* Header */}
        <View style={styles.headerBackground}>
          <View style={styles.header}>
            <Text style={styles.greeting}>Hola, Daniel</Text>
            <TouchableOpacity onPress={() => setShowConfig(!showConfig)}>
              <Icon name="settings" size={28} color="#ffffff" />
            </TouchableOpacity>
          </View>
          <Text style={styles.balanceText}>Saldo</Text>
          <Text style={styles.balance}>$ 7.979.010,00</Text>
          <View style={styles.card}>
            <Text style={styles.cardText}>VISA •••• 5850</Text>
            <Text style={styles.cardBrand}>milio</Text>
          </View>
        </View>

        {/* Scroll Content */}
        <ScrollView contentContainerStyle={styles.scrollContent}>
          {/* Configuration Panel */}
          {showConfig && (
            <View style={styles.configPanel}>
              {/* Environments */}
              <Text style={styles.configSectionTitle}>🌍 Ambiente</Text>
              {AVAILABLE_ENVIRONMENTS.map(env => (
                <TouchableOpacity
                  key={env.id}
                  style={[
                    styles.configItem,
                    selectedEnvironment === env.id && styles.configItemSelected,
                  ]}
                  onPress={() => changeEnvironment(env)}
                >
                  <Icon name={env.icon} size={20} color="#666" />
                  <Text style={styles.configItemText}>{env.name}</Text>
                  {selectedEnvironment === env.id && (
                    <Icon name="check-circle" size={20} color="#0048ff" />
                  )}
                </TouchableOpacity>
              ))}

              {/* Themes */}
              <Text style={[styles.configSectionTitle, { marginTop: 20 }]}>
                🎨 Tema
              </Text>
              {AVAILABLE_THEMES.map(theme => (
                <TouchableOpacity
                  key={theme.id}
                  style={[
                    styles.configItem,
                    selectedTheme === theme.id && styles.configItemSelected,
                  ]}
                  onPress={() => applyTheme(theme)}
                >
                  <Icon name={theme.icon} size={20} color="#666" />
                  <Text style={styles.configItemText}>{theme.name}</Text>
                  {selectedTheme === theme.id && (
                    <Icon name="check-circle" size={20} color="#0048ff" />
                  )}
                </TouchableOpacity>
              ))}

              {/* Languages */}
              <Text style={[styles.configSectionTitle, { marginTop: 20 }]}>
                🌐 Idioma
              </Text>
              {AVAILABLE_LANGUAGES.map(lang => (
                <TouchableOpacity
                  key={lang.id}
                  style={[
                    styles.configItem,
                    selectedLanguage === lang.code && styles.configItemSelected,
                  ]}
                  onPress={() => changeLanguage(lang)}
                >
                  <Text style={{ fontSize: 20 }}>{lang.flag}</Text>
                  <Text style={styles.configItemText}>{lang.name}</Text>
                  {selectedLanguage === lang.code && (
                    <Icon name="check-circle" size={20} color="#0048ff" />
                  )}
                </TouchableOpacity>
              ))}
            </View>
          )}

          {/* Transaction Actions */}
          <View style={styles.actions}>
            <TouchableOpacity
              style={styles.actionButton}
              onPress={openMilioPayManualCard}
            >
              <Text style={styles.actionText}>Transferencia manual</Text>
              <Icon name="credit-card" size={24} color="#666" />
            </TouchableOpacity>

            <TouchableOpacity
              style={styles.actionButton}
              onPress={handleTransferenciaInmediata}
            >
              <Text style={styles.actionText}>Transferencia inmediata</Text>
              <Icon name="flash-on" size={24} color="#666" />
            </TouchableOpacity>

            <TouchableOpacity
              style={styles.actionButton}
              onPress={handleRecargarBilletera}
            >
              <Text style={styles.actionText}>Recargar mi billetera</Text>
              <Icon name="account-balance-wallet" size={24} color="#666" />
            </TouchableOpacity>

            <TouchableOpacity
              style={styles.actionButton}
              onPress={handleAliasConfig}
            >
              <Text style={styles.actionText}>Tokenización de tarjetas</Text>
              <Icon name="credit-card" size={24} color="#666" />
            </TouchableOpacity>

            <TouchableOpacity
              style={styles.actionButton}
              onPress={handleCrossBorderRequest}
            >
              <Text style={styles.actionText}>Transferencia global</Text>
              <Icon name="public" size={24} color="#666" />
            </TouchableOpacity>

            <Text style={styles.sectionDivider}>Envío de Dinero</Text>

            <TouchableOpacity
              style={styles.actionButton}
              onPress={handleSendingMoneyRequest}
            >
              <Text style={styles.actionText}>Envío de dinero</Text>
              <Icon name="send" size={24} color="#666" />
            </TouchableOpacity>
          </View>

          {/* Transactions Section */}
          <Text style={styles.sectionTitle}>Transacciones</Text>
          <View style={styles.transactionsPlaceholder}>
            <View style={styles.skeleton} />
            <View style={styles.skeleton} />
          </View>
        </ScrollView>
      </SafeAreaView>
    </>
  );
};

// ============================================
// MARK: - Styles
// ============================================

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f8f9fa',
  },
  headerBackground: {
    backgroundColor: '#0048ff',
    padding: 20,
    paddingTop: 20,
    paddingBottom: 1,
    borderBottomLeftRadius: 25,
    borderBottomRightRadius: 25,
  },
  header: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
  },
  scrollContent: {
    paddingBottom: 40,
  },
  greeting: {
    fontSize: 22,
    fontWeight: 'bold',
    color: '#ffffff',
    fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
  },
  balanceText: {
    marginTop: 10,
    fontSize: 16,
    color: '#ffffff',
    fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
  },
  balance: {
    fontSize: 30,
    fontWeight: 'bold',
    color: '#ffffff',
    paddingBottom: 20,
    fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
  },
  card: {
    backgroundColor: '#000',
    borderRadius: 10,
    borderBottomLeftRadius: 0,
    borderBottomRightRadius: 0,
    padding: 25,
    paddingTop: 30,
    paddingBottom: 50,
    marginHorizontal: 5,
    marginTop: 10,
    flexDirection: 'row',
    justifyContent: 'space-between',
  },
  cardText: {
    fontSize: 18,
    color: '#fff',
    fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
  },
  cardBrand: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#fff',
    fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
  },
  configPanel: {
    backgroundColor: '#fff',
    margin: 20,
    padding: 20,
    borderRadius: 10,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 3,
    elevation: 3,
  },
  configSectionTitle: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#333',
    marginBottom: 10,
  },
  configItem: {
    flexDirection: 'row',
    alignItems: 'center',
    padding: 12,
    borderRadius: 8,
    marginBottom: 8,
    backgroundColor: '#f8f9fa',
  },
  configItemSelected: {
    backgroundColor: '#e3f2fd',
    borderWidth: 1,
    borderColor: '#0048ff',
  },
  configItemText: {
    flex: 1,
    marginLeft: 12,
    fontSize: 14,
    color: '#333',
  },
  actions: {
    marginTop: 20,
    paddingHorizontal: 20,
  },
  actionButton: {
    backgroundColor: '#fff',
    borderRadius: 10,
    padding: 30,
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: 10,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 3,
    elevation: 3,
  },
  actionText: {
    fontSize: 16,
    fontWeight: '500',
    color: '#333',
    fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
  },
  sectionDivider: {
    fontSize: 14,
    fontWeight: 'bold',
    color: '#999',
    textAlign: 'center',
    marginVertical: 15,
  },
  sectionTitle: {
    fontSize: 18,
    fontWeight: 'bold',
    marginVertical: 10,
    paddingHorizontal: 20,
    fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
  },
  transactionsPlaceholder: {
    backgroundColor: '#f0f0f0',
    padding: 20,
    borderRadius: 10,
    marginHorizontal: 20,
  },
  skeleton: {
    height: 20,
    backgroundColor: '#ddd',
    borderRadius: 5,
    marginBottom: 10,
  },
});

export default App;


Personalización del SDK con Colores

El SDK de Milio permite personalizar la apariencia de los elementos de la interfaz de usuario mediante la función setThemeColors. Esto permite adaptar el diseño del SDK a la identidad visual de cada aplicación.

🎨 ¿Qué puedes cambiar?

Al llamar a setThemeColors, puedes definir los siguientes colores:

PropiedadDescripción
primaryColorColor primario títulos.
secondaryColorColor para los botones secundario
btnColorEnabledColor del botón cuando está habilitado.
btnColorDisabledColor del botón cuando está deshabilitado.
btnTextColorEnabledColor del texto cuando el botón está habilitado.
btnTextColorDisabledColor del texto cuando el botón está deshabilitado.
layoutBackgroundColorColor de fondo del SDK.
textViewColorColor del texto en la interfaz del SDK.
inputBorderColorColor de los bordes
textSelectionColorColor de selección de los switch checkbox

📌 Ejemplo de personalización

En el código, la función setThemeColors se encarga de aplicar estos cambios:

const setThemeColors = async () => {
  try {
    const colors = {
      primaryColor:          '#fd135a',  
      secondaryColor:        '#fadc30', 
      btnColorEnabled:       '#641f5e', 
      btnColorDisabled:      '#D3D3D3',
      btnTextColorEnabled:   '#FFFFFF',
      btnTextColorDisabled:  '#888888',
      layoutBackgroundColor: '#fafafa', 
      textViewColor:         '#3498db', 
      inputBorderColor:      '#3498db', 
      textSelectionColor:    '#3498db'
    };

    await MilioSdk.setThemeColors(colors);
  } catch (error) {
    Alert.alert('Error', 'No se pudieron aplicar los colores');
  }
};

Montos Mínimos y Máximos en Recarga

En esta sección se configuran los límites de monto para el módulo de recarga de billetera por parámetros.

Configura los límites de monto mínimo y máximo permitidos para el módulo de recarga de billetera del SDK. Estos valores determinan el rango válido que un usuario puede ingresar al realizar una recarga.

minAmount : Monto mínimo permitido 1,0 por defecto
maxAmount : Monto máximo permitido 5.000,0 por defecto

Actualización de archivos
Agregar estos métodos en el archivo MilioSDKBridge.swift, después de setEnvironment o junto con los otros métodos de configuración:


/**
 * ✅ Configura los límites de monto para el módulo de recarga de billetera
 * 
 * @param minAmount Monto mínimo permitido (por defecto 1.0)
 * @param maxAmount Monto máximo permitido (por defecto 5000000.0)
*/

@objc func setWalletRechargeConfig(
   _ minAmount: NSNumber,
   maxAmount: NSNumber,
   resolver resolve: RCTPromiseResolveBlock,
   rejecter reject: RCTPromiseRejectBlock
) {
   let min = minAmount.doubleValue
   let max = maxAmount.doubleValue
  
   TransactionConfigSdk.setWalletRechargeConfig(
       minAmount: min,
       maxAmount: max
   )
  
   print("✅ iOS SDK: Wallet Recharge Config - Min: \(min), Max: \(max)")
   resolve("Wallet recharge config establecida: \(min) - \(max)")
}


USO DESDE REACT NATIVE (JavaScript/TypeScript)


import { NativeModules } from 'react-native';
const { MilioSdk } = NativeModules;

// Configurar montos personalizados
await MilioSdk.setWalletRechargeConfig(1.0, 5000.0);
// Mínimo: $50
// Máximo: $2,000,000


Logo corporativo

Prepara tu imagen PNG

Nuestro ImageView nativo ocupa 120 dp × 36 dp. Para que el logo se vea siempre nítido, el PNG deberá cumplir al menos con estas resoluciones (en píxeles) según la densidad de la pantalla:

DensidadFactorAncho (px)Alto (px)
mdpi544200
hdpi1.5×544300
xhdpi1088400
xxhdpi1632600
xxxhdpi2176800
 const url = "https://i.ibb.co/yBsc66yP/logo-milio.png";
      MilioSdk.setLogoUrl(url)
      .then(() => console.log("Logo URL seteada:", url))
      .catch(err => Alert.alert('SDK Error', err.message || err));

📌 Métodos Disponibles

🔐 Proceso de Encriptación y Seguridad en el SDK

El sistema de transferencias y recargas del SDK utiliza un mecanismo de encriptación de datos basado en criptografía de clave pública para garantizar la seguridad de la información en cada transacción.

🔑 1. ¿Por qué se usa una llave pública?

La llave pública, generada por un endpoint especializado, es la herramienta que permite cifrar la información antes de enviarla a través del SDK. Al encriptar los datos con esta llave, aseguramos que solo el servidor que tiene la llave privada correspondiente pueda descifrarlos, evitando accesos no autorizados o modificaciones en la información.

📲 2. Procesos que requieren encriptación con la llave pública

Cada una de las transacciones en el SDK debe pasar por este proceso de encriptación antes de ser enviada al servidor:

1️⃣ Transferencia con QR

Se escanea un código QR con los datos de la transacción.
La información se encripta con la llave pública antes de enviarla.
El servidor recibe los datos cifrados y los desencripta con su llave privada.

2️⃣ Transferencia Manual

El usuario ingresa manualmente los datos de la transacción (monto, cuenta destino, etc.).
Antes de enviarlos al SDK, los datos se cifran con la llave pública.
El servidor procesa la información tras desencriptarla.

3️⃣ Transferencia Inmediata

Similar a la transferencia manual, pero con una ejecución más rápida.
La información viaja encriptada con la llave pública para evitar manipulación en el proceso.

4️⃣ Recarga de Billetera

Para recargar fondos en la billetera digital, los datos (monto, origen, destino) se protegen con encriptación.
Solo el servidor, con su llave privada, podrá acceder a la información real y procesar la recarga.

Beneficios de este proceso de seguridad

  • Protección de datos sensibles en cada transacción.
  • Evita que terceros accedan o modifiquen la información.
  • Cumple con estándares de seguridad y auditoría en encriptación.

Cada vez que se realiza una transferencia o recarga en el SDK, los datos deben encriptarse con la llave pública antes de enviarse. Solo el servidor con la llave privada correcta podrá descifrarlos y procesar la transacción, garantizando un entorno seguro y confiable para el usuario.


Version 0.1.1

  • En esta versión se renombró la función setLogoWidth a setWidthLogo con el fin de estandarizar la nomenclatura entre iOS y Android.
  • Unificación del diseño de la interfaz en los módulos de recarga de billetera, transferencia manual y transferencia inmediata, garantizando coherencia visual y de experiencia de usuario en todo el flujo.

Especificación de cambios

Se deben actualizar las referencias a este método en los siguientes archivos:

  • ios/MilioSDKBridge.swift
  • ios/MilioSdkModule.m
  • App.tsx

  • ios/MilioSDKBridge.swift
  @objc func setWidthLogo(
        _ width: NSNumber,
        resolver resolve: RCTPromiseResolveBlock,
        rejecter reject: RCTPromiseRejectBlock
    ) {
        let widthValue = width.intValue
        TransactionConfigSdk.setWidthLogo(widthValue) 
        resolve("Logo width configurado: \(widthValue)")
    }

  • ios/MilioSdkModule.m
RCT_EXTERN_METHOD(setWidthLogo:(nonnull NSNumber *)width
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

  • App.tsx
 const logoUrl = "https://static";
 await MilioSdk.setLogoUrl(logoUrl);
 await MilioSdk.setLogoWidth(130); //Cambio aplicado

VERSION 0.1.5


📌 Documentación del SDK Milio para React Native (iOS)

1. Integración del SDK de iOS en React Native

✅ 1️⃣ Instalar la Dependencia del SDK con CocoaPods

Asegúrate de que CocoaPods esté instalado:

sudo gem install cocoapods
 

Luego, abre la carpeta ios y actualiza las dependencias:

pod deintegrate es opcional , este comando es para empezar desde cero con las dependencias nativas.

cd ios
pod deintegrate

Instalacion automatica

Si el SDK de Milio se distribuye a través de un repositorio privado de CocoaPods, agrégalo en tu Podfile (ios/Podfile):

source '[email protected]:miliopay/miliopods-specs.git'
source 'https://cdn.cocoapods.org/'

platform :ios, '14.0'
use_frameworks!

target 'SDKTestAppSpecsPrivate' do
  
  pod 'sdk-mobile-ios',
  :git => 'https://bitbucket.org/miliopay/sdk-mobile-ios.git',
  :tag => '0.1.5'
  
end


Finalmente, instala las dependencias:

pod install --repo-update

2. Crear el Bridge entre React Native y el SDK de iOS

Para exponer el SDK de iOS a React Native, crea el archivo:

   **ios/MilioSdkModule.m**
// ios/MilioSdk.m
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>

@interface RCT_EXTERN_MODULE(MilioSdk, RCTEventEmitter)

// ============================================
// MARK: - Inicialización
// ============================================
// ✅ CAMBIO: Ya no recibe token como parámetro
RCT_EXTERN_METHOD(initializeSdk:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

// ============================================
// MARK: - Configuración de Tema
// ============================================
RCT_EXTERN_METHOD(setThemeColors:(NSDictionary *)colorDict
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

// ============================================
// MARK: - Configuración de Logo
// ============================================
RCT_EXTERN_METHOD(setLogoUrl:(NSString *)url
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(clearLogoUrl:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(setWidthLogo:(nonnull NSNumber *)width
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

// ============================================
// MARK: - Configuración de Fuente
// ============================================
RCT_EXTERN_METHOD(setFont:(NSString *)fontOption
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

// ============================================
// MARK: - Configuración de Ambiente
// ============================================
RCT_EXTERN_METHOD(setEnvironment:(NSString *)environment
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

// ============================================
// MARK: - Configuración de Idioma
// ============================================
RCT_EXTERN_METHOD(setLanguage:(NSString *)languageCode
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

// ============================================
// MARK: - Métodos de Transacción
// ============================================

// QR - ✅ Solo recibe tokenTransaction
RCT_EXTERN_METHOD(openMilioSdkQr:(NSString *)tokenTransaction
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

// Recargar Manual - ✅ Solo recibe tokenTransaction
RCT_EXTERN_METHOD(openMilioPayManualCard:(NSString *)tokenTransaction
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

// Transferencia Inmediata - ✅ Solo recibe tokenTransaction
RCT_EXTERN_METHOD(openMilioPayAddFoundManualCard:(NSString *)tokenTransaction
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

// Recargar Billetera - ✅ Solo recibe tokenTransaction
RCT_EXTERN_METHOD(openMilioPayRechargeWallet:(NSString *)tokenTransaction
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

// Tokenización de Tarjetas - ✅ Solo recibe tokenTransaction
RCT_EXTERN_METHOD(openPaymentAliasConfiguration:(NSString *)tokenTransaction
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

// Transferencia Global (Cross-Border) - ✅ Solo recibe tokenTransaction
RCT_EXTERN_METHOD(openCrossBorderRequest:(NSString *)tokenTransaction
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

// Envío de Dinero (Outbound) - ✅ Solo recibe tokenTransaction
RCT_EXTERN_METHOD(openSendingMoneyRequest:(NSString *)tokenTransaction
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

// Envío de Dinero Local - ✅ Solo recibe tokenTransaction
RCT_EXTERN_METHOD(openSendingMoneyRequestLocal:(NSString *)tokenTransaction
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

// Envío Outbound Transfer SDK - ✅ Solo recibe tokenTransaction
RCT_EXTERN_METHOD(openOutboundTransferSdk:(NSString *)tokenTransaction
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

// ============================================
// MARK: - Debug
// ============================================
RCT_EXTERN_METHOD(debugAvailableImages:(NSString *)imageNameToTest)


RCT_EXTERN_METHOD(setWalletRechargeConfig:(NSNumber *)minAmount
                  maxAmount:(NSNumber *)maxAmount
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

+ (BOOL)requiresMainQueueSetup
{
    return YES;
}

@end

Ahora, crea el archivo Swift para implementar los métodos nativos:

3. Enlazar el Módulo en React Native

Nueva actualizacion del cliente React native iOS

Resumen de Mejoras en la Nueva Versión del Cliente Milio SDK

Inicialización Automática del SDK
El SDK se inicializa automáticamente al iniciar sesión con el token de autenticación.

Configuración Dinámica de Colores
Se aplica un tema visual personalizado desde React Native para adaptarse al branding del cliente.

Soporte Completo de Eventos del SDK
El cliente ahora puede recibir notificaciones en tiempo real:

  • Éxito o error en la transacción
  • Finalización completa de la operación
  • Cierre del SDK por el usuario
ios/MilioSdkModule.swift
import Foundation
import React
import UIKit
import sdk_mobile_ios

@objc(MilioSdk)
class MilioSDKBridge: RCTEventEmitter {

    private var delegateAdapter: DelegateAdapter?
    private var hasListeners = false

    // ============================================
    // MARK: - Setup
    // ============================================
    
    override static func requiresMainQueueSetup() -> Bool {
        return true
    }

    @objc override static func moduleName() -> String {
        return "MilioSdk"
    }

    override init() {
        super.init()
        // ✅ Inicializar delegate automáticamente
        initializeDelegate()
    }

    override func startObserving() {
        hasListeners = true
    }

    override func stopObserving() {
        hasListeners = false
    }

    override func supportedEvents() -> [String]! {
        return [
            "onTransactionError",
            "onTransactionSuccess",
            "onTransactionCompleted",
            "onTransactionClosed",
            "onTransactionBalanceUpdated",
            "MilioSdkDidClose",
            "MilioSdkDidUpdateBalance"
        ]
    }

    // ============================================
    // MARK: - Inicialización
    // ============================================
    
    private func initializeDelegate() {
        if delegateAdapter == nil {
            delegateAdapter = DelegateAdapter(bridge: self)
            TransactionConfigSdk.setTransactionCallback(delegateAdapter!)
            print("✅ iOS SDK: Delegate inicializado automáticamente")
        }
    }
    
    @objc func initializeSdk(
        _ resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        // ✅ CAMBIO: Ya no recibe token, solo inicializa el delegate
        initializeDelegate()
        print("✅ iOS SDK: Inicializado correctamente")
        resolve("SDK inicializado correctamente")
    }

    // ============================================
    // MARK: - Configuración de Tema
    // ============================================
    
    @objc func setThemeColors(
        _ colorDict: NSDictionary, 
        resolver resolve: RCTPromiseResolveBlock,
        rejecter reject: RCTPromiseRejectBlock
    ) {
        TransactionConfigSdk.setThemeColors(
            primaryColor: colorDict["primaryColor"] as? String ?? "#808080",
            secondaryColor: colorDict["secondaryColor"] as? String ?? "#242424",
            btnColorEnabled: colorDict["btnColorEnabled"] as? String ?? "#809BFF",
            btnColorDisabled: colorDict["btnColorDisabled"] as? String ?? "#D3D3D3",
            btnTextColorEnabled: colorDict["btnTextColorEnabled"] as? String ?? "#FFFFFF",
            btnTextColorDisabled: colorDict["btnTextColorDisabled"] as? String ?? "#888888",
            layoutBackgroundColor: colorDict["layoutBackgroundColor"] as? String ?? "#ffffff",
            textViewColor: colorDict["textViewColor"] as? String ?? "#242424",
            inputBorderColor: colorDict["inputBorderColor"] as? String ?? "#808080",
            textSelectionColor: colorDict["textSelectionColor"] as? String ?? "#809BFF"
        )
        print("✅ iOS SDK: Colores aplicados")
        resolve("Colores aplicados correctamente")
    }

    // ============================================
    // MARK: - Configuración de Logo
    // ============================================
    
  @objc func setLogoUrl(
      _ url: NSString,
      resolver resolve: RCTPromiseResolveBlock,
      rejecter reject: RCTPromiseRejectBlock
  ) {
      let str = (url as String).trimmingCharacters(in: .whitespacesAndNewlines)
       
      
      TransactionConfigSdk.setLogoURL(str)
      
      // ✅ LOG CRÍTICO 2: Verificar que se guardó
      if let saved = UserDefaults.standard.string(forKey: "SDK_CustomLogoURL") {
          print("   ✅ Verificación: Logo GUARDADO en UserDefaults: '\(saved)'")
      } else {
          print("   ❌ Verificación: Logo NO ENCONTRADO en UserDefaults")
      }
      
      print("✅ iOS SDK: Logo URL configurado: \(str)")
      resolve("Logo URL guardada correctamente")
  }
    
    @objc func clearLogoUrl(
        _ resolve: RCTPromiseResolveBlock,
        rejecter reject: RCTPromiseRejectBlock
    ) {
        TransactionConfigSdk.setLogoURL(nil)
        print("✅ iOS SDK: Logo removido")
        resolve("Logo URL removida correctamente")
    }
    
    @objc func setWidthLogo(
        _ width: NSNumber,
        resolver resolve: RCTPromiseResolveBlock,
        rejecter reject: RCTPromiseRejectBlock
    ) {
        let widthValue = width.intValue
        TransactionConfigSdk.setWidthLogo(widthValue)
        print("✅ iOS SDK: Logo width configurado: \(widthValue)")
        resolve("Logo width configurado: \(widthValue)")
    }
    
    // ============================================
    // MARK: - Configuración de Fuente
    // ============================================
    
    @objc func setFont(
        _ fontOption: NSString,
        resolver resolve: RCTPromiseResolveBlock,
        rejecter reject: RCTPromiseRejectBlock
    ) {
        let fontString = fontOption as String
        switch fontString.lowercased() {
        case "satoshi":
            TransactionConfigSdk.setFont(option: .satoshi)
        default:
            TransactionConfigSdk.setFont(option: .satoshi)
        }
        print("✅ iOS SDK: Font configurada: \(fontString)")
        resolve("Font configurada: \(fontString)")
    }
    
    // ============================================
    // MARK: - Configuración de Ambiente
    // ============================================
    
    @objc func setEnvironment(
        _ environment: NSString,
        resolver resolve: RCTPromiseResolveBlock,
        rejecter reject: RCTPromiseRejectBlock
    ) {
        TransactionConfigSdk.setEnvironment(environment as String)
        resolve("Url configurado: \(environment)")
    }

    // ============================================
    // MARK: - Configuración de Idioma
    // ============================================
    
    @objc func setLanguage(
        _ languageCode: NSString,
        resolver resolve: RCTPromiseResolveBlock,
        rejecter reject: RCTPromiseRejectBlock
    ) {
        TransactionConfigSdk.setLanguage(languageCode as String)
        print("✅ iOS SDK: Idioma configurado: \(languageCode)")
        resolve("Idioma configurado: \(languageCode)")
    }

    // ============================================
    // MARK: - Debug
    // ============================================
    
    @objc func debugAvailableImages(_ imageName: NSString) {
        TransactionConfigSdk.debugImagesInResourceBundle()
    }

    // ============================================
// MARK: - Configuración de Wallet Recharge
// ============================================

@objc func setWalletRechargeConfig(
    _ minAmount: NSNumber,
    maxAmount: NSNumber,
    resolver resolve: RCTPromiseResolveBlock,
    rejecter reject: RCTPromiseRejectBlock
) {
    let min = minAmount.doubleValue
    let max = maxAmount.doubleValue
    
    TransactionConfigSdk.setWalletRechargeConfig(
        minAmount: min,
        maxAmount: max
    )
    
    print("✅ iOS SDK: Wallet Recharge Config - Min: \(min), Max: \(max)")
    resolve("Wallet recharge config establecida: \(min) - \(max)")
}

    // ============================================
    // MARK: - Métodos de Transacción
    // ============================================

    @objc func openMilioSdkQr(
        _ tokenTransaction: NSString, 
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        openTransaction(
            type: .qr, 
            tokenTransaction: tokenTransaction as String, 
            resolve: resolve,
            reject: reject
        )
    }

    @objc func openMilioPayManualCard(
        _ tokenTransaction: NSString, 
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        openTransaction(
            type: .manual, 
            tokenTransaction: tokenTransaction as String, 
            resolve: resolve,
            reject: reject
        )
    }

    @objc func openMilioPayAddFoundManualCard(
        _ tokenTransaction: NSString, 
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        openTransaction(
            type: .addFunds, 
            tokenTransaction: tokenTransaction as String, 
            resolve: resolve,
            reject: reject
        )
    }

    @objc func openMilioPayRechargeWallet(
        _ tokenTransaction: NSString, 
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        openTransaction(
            type: .recharge, 
            tokenTransaction: tokenTransaction as String, 
            resolve: resolve,
            reject: reject
        )
    }
    
    @objc func openPaymentAliasConfiguration(
        _ tokenTransaction: NSString,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        openTransaction(
            type: .aliasConfiguration,
            tokenTransaction: tokenTransaction as String,
            resolve: resolve,
            reject: reject
        )
    }
    
    @objc func openCrossBorderRequest(
        _ tokenTransaction: NSString,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        openTransaction(
            type: .crossBorder,
            tokenTransaction: tokenTransaction as String,
            resolve: resolve,
            reject: reject
        )
    }

    @objc func openSendingMoneyRequest(
        _ tokenTransaction: NSString,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        openTransaction(
            type: .sendingMoney,
            tokenTransaction: tokenTransaction as String,
            resolve: resolve,
            reject: reject
        )
    }

    @objc func openSendingMoneyRequestLocal(
        _ tokenTransaction: NSString,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        openTransaction(
            type: .sendingMoneyLocal,
            tokenTransaction: tokenTransaction as String,
            resolve: resolve,
            reject: reject
        )
    }

    @objc func openOutboundTransferSdk(
        _ tokenTransaction: NSString,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        openTransaction(
            type: .outboundTransfer,
            tokenTransaction: tokenTransaction as String,
            resolve: resolve,
            reject: reject
        )
    }

    // ============================================
    // MARK: - Helper Methods
    // ============================================

    private enum TransactionType {
        case qr
        case manual
        case addFunds
        case recharge
        case aliasConfiguration
        case crossBorder
        case sendingMoney
        case sendingMoneyLocal
        case outboundTransfer
    }

    private func openTransaction(
        type: TransactionType,
        tokenTransaction: String,
        resolve: @escaping RCTPromiseResolveBlock,
        reject: @escaping RCTPromiseRejectBlock
    ) {
        // ✅ CAMBIO CRÍTICO: Ya no requiere authToken
        print("🚀 iOS SDK: Iniciando transacción tipo: \(type)")
        print("🔑 iOS SDK: Token de transacción: \(String(tokenTransaction.prefix(20)))...")

        // ✅ Asegurar que el delegate esté inicializado
        initializeDelegate()

        DispatchQueue.main.async { 
            guard
                let rootVC = UIApplication.shared.connectedScenes
                    .compactMap({ $0 as? UIWindowScene })
                    .flatMap({ $0.windows })
                    .first(where: { $0.isKeyWindow })?.rootViewController
            else {
                print("❌ iOS SDK: No se encontró rootViewController")
                reject("NO_VIEW_CONTROLLER", "No se encontró un controlador de vista válido", nil)
                return
            }

            // ✅ CAMBIO: Todas las llamadas ahora solo usan tokenTransaction
            switch type {
            case .qr:
                print("📱 iOS SDK: Abriendo QR Scanner")
                TransactionConfigSdk.openMilioSdkQr(
                    from: rootVC, 
                    tokenTransaction: tokenTransaction
                )
            case .manual:
                print("💳 iOS SDK: Abriendo Manual Card")
                TransactionConfigSdk.openMilioPayManualCard(
                    from: rootVC, 
                    tokenTransaction: tokenTransaction
                )
            case .addFunds:
                print("💰 iOS SDK: Abriendo Add Funds")
                TransactionConfigSdk.openMilioPayAddFoundManualCard(
                    from: rootVC, 
                    tokenTransaction: tokenTransaction
                )
            case .recharge:
                print("🔄 iOS SDK: Abriendo Recharge Wallet")
                TransactionConfigSdk.openMilioPayRechargeWallet(
                    from: rootVC, 
                    tokenTransaction: tokenTransaction
                )
            case .aliasConfiguration:
                print("🔗 iOS SDK: Abriendo Alias Configuration")
                TransactionConfigSdk.openPaymentAliasConfigurationSheet(
                    from: rootVC,
                    tokenTransaction: tokenTransaction
                )
            case .crossBorder:
                print("🌍 iOS SDK: Abriendo Cross Border")
                TransactionConfigSdk.openTransferGlobal(
                    from: rootVC,
                    tokenTransaction: tokenTransaction
                )
            case .sendingMoney:
                print("📤 iOS SDK: Abriendo Sending Money")
                TransactionConfigSdk.openSendingMoneyRequest(
                    from: rootVC,
                    tokenTransaction: tokenTransaction
                )
            case .sendingMoneyLocal:
                print("📍 iOS SDK: Abriendo Sending Money Local")
                TransactionConfigSdk.openSendingMoneyRequestLocal(
                    from: rootVC,
                    tokenTransaction: tokenTransaction
                )
            case .outboundTransfer:
                print("🚀 iOS SDK: Abriendo Outbound Transfer")
                TransactionConfigSdk.openOutboundTransferSdk(
                    from: rootVC,
                    tokenTransaction: tokenTransaction
                )
            }

            print("✅ iOS SDK: Transacción \(type) iniciada correctamente")
            resolve("Transacción \(type) iniciada correctamente")
        }
    }

    // ============================================
    // MARK: - Delegate Adapter Class
    // ============================================
    
    class DelegateAdapter: NSObject, MilioSDKDelegate {
        weak var bridge: MilioSDKBridge?

        init(bridge: MilioSDKBridge) {
            self.bridge = bridge
            super.init()
            print("✅ iOS SDK: DelegateAdapter creado")
        }

        func onTransactionError(errorCode: Int, errorMessage: String) {
            print("❌ iOS SDK Delegate: Error - Código: \(errorCode), Mensaje: \(errorMessage)")
            bridge?.sendEvent(
                withName: "onTransactionError",
                body: [
                    "errorCode": errorCode,
                    "errorMessage": errorMessage
                ]
            )
        }

        func onTransactionSuccess(amount: Double, message: String) {
            print("✅ iOS SDK Delegate: Transacción exitosa - Monto: \(amount), Mensaje: \(message)")
            bridge?.sendEvent(
                withName: "onTransactionSuccess",
                body: [
                    "amount": amount,
                    "message": message
                ]
            )
        }

        func onTransactionCompleted(amount: Double, isAddition: Bool) {
            print("✅ iOS SDK Delegate: Transacción completada - Monto: \(amount), Es adición: \(isAddition)")
            bridge?.sendEvent(
                withName: "onTransactionCompleted",
                body: [
                    "amount": amount,
                    "isAddition": isAddition
                ]
            )
        }

        func didCloseSdk() {
            print("🚪 iOS SDK Delegate: SDK cerrado")
            DispatchQueue.main.async {
                self.bridge?.sendEvent(withName: "MilioSdkDidClose", body: nil)
                self.bridge?.sendEvent(withName: "onTransactionClosed", body: nil)
            }
        }

        func didUpdateBalance(newBalance: Double) {
            print("💰 iOS SDK Delegate: Balance actualizado - Nuevo balance: \(newBalance)")
            bridge?.sendEvent(
                withName: "MilioSdkDidUpdateBalance",
                body: newBalance
            )
            bridge?.sendEvent(
                withName: "onTransactionBalanceUpdated",
                body: ["newBalance": newBalance]
            )
        }
    }
}


Codigo de javascript

Esto deja claro que:

  • Es un ejemplo funcional, no producción.
  • Explica el uso de initializeSdk, los colores, y los eventos.
  • Ayuda a otros devs a saber dónde personalizar e integrar.
import React, { useEffect, useState } from 'react';
import {
  SafeAreaView,
  ScrollView,
  View,
  Text,
  StyleSheet,
  TouchableOpacity,
  StatusBar,
  Alert,
  NativeEventEmitter,
  Platform
} from 'react-native';

import Icon from 'react-native-vector-icons/MaterialIcons';
import MilioSdk from './MilioSdk';

const MilioSdkEmitter = new NativeEventEmitter(MilioSdk);

// ============================================
// MARK: - Types & Interfaces
// ============================================

interface ThemeConfig {
  id: string;
  name: string;
  icon: string;
  primaryColor: string;
  secondaryColor: string;
  btnColorEnabled: string;
  btnColorDisabled: string;
  btnTextColorEnabled: string;
  btnTextColorDisabled: string;
  layoutBackgroundColor: string;
  textViewColor: string;
  inputBorderColor: string;
  textSelectionColor: string;
}

interface EnvironmentConfig {
  id: string;
  name: string;
  icon: string;
  envString: string;
  description: string;
}

interface LanguageConfig {
  id: string;
  code: string;
  name: string;
  flag: string;
}

// ============================================
// MARK: - Configuration Data
// ============================================

const AVAILABLE_THEMES: ThemeConfig[] = [
  {
    id: 'default',
    name: 'Default (Blue)',
    icon: 'palette',
    primaryColor: '#808080',
    secondaryColor: '#242424',
    btnColorEnabled: '#809BFF',
    btnColorDisabled: '#D3D3D3',
    btnTextColorEnabled: '#FFFFFF',
    btnTextColorDisabled: '#888888',
    layoutBackgroundColor: '#ffffff',
    textViewColor: '#242424',
    inputBorderColor: '#808080',
    textSelectionColor: '#809BFF'
  },
  {
    id: 'dark',
    name: 'Dark Mode',
    icon: 'brightness-3',
    primaryColor: '#CCCCCC',
    secondaryColor: '#FFFFFF',
    btnColorEnabled: '#1E1E1E',
    btnColorDisabled: '#3A3A3A',
    btnTextColorEnabled: '#FFFFFF',
    btnTextColorDisabled: '#888888',
    layoutBackgroundColor: '#121212',
    textViewColor: '#FFFFFF',
    inputBorderColor: '#444444',
    textSelectionColor: '#BB86FC'
  },
  {
    id: 'green',
    name: 'Green Nature',
    icon: 'eco',
    primaryColor: '#6B8E23',
    secondaryColor: '#2F4F2F',
    btnColorEnabled: '#32CD32',
    btnColorDisabled: '#90EE90',
    btnTextColorEnabled: '#FFFFFF',
    btnTextColorDisabled: '#808080',
    layoutBackgroundColor: '#F0FFF0',
    textViewColor: '#2F4F2F',
    inputBorderColor: '#6B8E23',
    textSelectionColor: '#32CD32'
  },
  {
    id: 'purple',
    name: 'Purple Dream',
    icon: 'stars',
    primaryColor: '#9370DB',
    secondaryColor: '#4B0082',
    btnColorEnabled: '#8A2BE2',
    btnColorDisabled: '#DDA0DD',
    btnTextColorEnabled: '#FFFFFF',
    btnTextColorDisabled: '#888888',
    layoutBackgroundColor: '#F8F4FF',
    textViewColor: '#4B0082',
    inputBorderColor: '#9370DB',
    textSelectionColor: '#8A2BE2'
  },
  {
    id: 'orange',
    name: 'Sunset Orange',
    icon: 'wb-sunny',
    primaryColor: '#FF8C00',
    secondaryColor: '#8B4513',
    btnColorEnabled: '#FF6347',
    btnColorDisabled: '#FFB6C1',
    btnTextColorEnabled: '#FFFFFF',
    btnTextColorDisabled: '#888888',
    layoutBackgroundColor: '#FFF5EE',
    textViewColor: '#8B4513',
    inputBorderColor: '#FF8C00',
    textSelectionColor: '#FF6347'
  }
];

const AVAILABLE_ENVIRONMENTS: EnvironmentConfig[] = [
  {
    id: 'dev',
    name: 'Development',
    icon: 'code',
    envString: 'DEV',
    description: 'Ambiente de desarrollo'
  },
  {
    id: 'qa',
    name: 'QA',
    icon: 'verified',
    envString: 'QA',
    description: 'Ambiente de pruebas (por defecto)'
  },
  {
    id: 'sandbox',
    name: 'Sandbox',
    icon: 'science',
    envString: 'SANDBOX',
    description: 'Ambiente aislado para testing'
  },
  {
    id: 'prod',
    name: 'Production',
    icon: 'cloud-done',
    envString: 'PROD',
    description: '⚠️ Ambiente de producción'
  }
];

const AVAILABLE_LANGUAGES: LanguageConfig[] = [
  { id: 'en', code: 'en', name: 'English', flag: '🇺🇸' },
  { id: 'es', code: 'es', name: 'Español', flag: '🇪🇸' },
  { id: 'pt', code: 'pt', name: 'Português', flag: '🇧🇷' },
  { id: 'fr', code: 'fr', name: 'Français', flag: '🇫🇷' }
];

// ============================================
// MARK: - Main Component
// ============================================

const App = () => {
  const [loading, setLoading] = useState(false);
  const [selectedEnvironment, setSelectedEnvironment] = useState('qa');
  const [selectedTheme, setSelectedTheme] = useState('default');
  const [selectedLanguage, setSelectedLanguage] = useState('es');
  const [showConfig, setShowConfig] = useState(false);

  // ============================================
  // MARK: - SDK Event Listeners
  // ============================================

  useEffect(() => {
    const s1 = MilioSdkEmitter.addListener('onTransactionSuccess', ({ message, amount }) => {
      Alert.alert('✅ Transacción Exitosa', `Monto: $${amount}\n${message}`);
    });

    const s2 = MilioSdkEmitter.addListener('onTransactionCompleted', ({ amount, isAddition }) => {
      Alert.alert('✅ Transacción Completada', `Monto: $${amount}\n${isAddition ? 'Suma' : 'Resta'}`);
    });

    const s3 = MilioSdkEmitter.addListener('onTransactionError', ({ errorCode, errorMessage }) => {
      Alert.alert('❌ Error en Transacción', `Código: ${errorCode}\n${errorMessage}`);
    });

    const s4 = MilioSdkEmitter.addListener('MilioSdkDidClose', () => {
      // SDK cerrado
    });

    return () => {
      s1.remove();
      s2.remove();
      s3.remove();
      s4.remove();
    };
  }, []);

  // ============================================
  // MARK: - Initialization
  // ============================================

  useEffect(() => {
    const bootstrap = async () => {
      try {
        await MilioSdk.initializeSdk();
        await applyTheme(AVAILABLE_THEMES[0]);
        await MilioSdk.setEnvironment("SANDBOX");
        await MilioSdk.setLanguage('es');
        await MilioSdk.setFont('satoshi');

        const logoUrl = "https://cdn-sandbox.milio.com.co/logo-acme.png";
        await MilioSdk.setLogoUrl(logoUrl);
        await MilioSdk.setWidthLogo(100);
        await MilioSdk.setWalletRechargeConfig(2.5, 72000000.0);
      } catch (err: any) {
        Alert.alert('Error al iniciar', err.message || String(err));
      }
    };

    bootstrap();
  }, []);

  // ============================================
  // MARK: - Configuration Handlers
  // ============================================

  const applyTheme = async (theme: ThemeConfig) => {
    try {
      await MilioSdk.setThemeColors({
        primaryColor: theme.primaryColor,
        secondaryColor: theme.secondaryColor,
        btnColorEnabled: theme.btnColorEnabled,
        btnColorDisabled: theme.btnColorDisabled,
        btnTextColorEnabled: theme.btnTextColorEnabled,
        btnTextColorDisabled: theme.btnTextColorDisabled,
        layoutBackgroundColor: theme.layoutBackgroundColor,
        textViewColor: theme.textViewColor,
        inputBorderColor: theme.inputBorderColor,
        textSelectionColor: theme.textSelectionColor
      });
      setSelectedTheme(theme.id);
      Alert.alert('🎨 Tema Aplicado', theme.name);
    } catch (error) {
      Alert.alert('Error', 'No se pudo aplicar el tema');
    }
  };

  const changeEnvironment = async (env: EnvironmentConfig) => {
    try {
      await MilioSdk.setEnvironment(env.envString);
      setSelectedEnvironment(env.id);
      Alert.alert('🌍 Ambiente Cambiado', env.name);
    } catch (error) {
      Alert.alert('Error', 'No se pudo cambiar el ambiente');
    }
  };

  const changeLanguage = async (lang: LanguageConfig) => {
    try {
      await MilioSdk.setLanguage(lang.code);
      setSelectedLanguage(lang.code);
      Alert.alert('🌐 Idioma Cambiado', lang.name);
    } catch (error) {
      Alert.alert('Error', 'No se pudo cambiar el idioma');
    }
  };

  // ============================================
  // MARK: - Transaction Handlers
  // ============================================

  const handleRecargarManual = async () => {
    try {
      // Reemplaza 'YOUR_TOKEN_HERE' con tu lógica de obtención de token
      const sdkToken = 'YOUR_TOKEN_HERE';
      await MilioSdk.openMilioPayManualCard(sdkToken);
    } catch (error) {
      Alert.alert('Error', 'No se pudo ejecutar la operación');
    }
  };

  const handleTransferenciaInmediata = async () => {
    try {
      const sdkToken = 'YOUR_TOKEN_HERE';
      await MilioSdk.openMilioPayAddFoundManualCard(sdkToken);
    } catch (error) {
      Alert.alert('Error', 'No se pudo ejecutar la operación');
    }
  };

  const handleRecargarBilletera = async () => {
    try {
      const sdkToken = 'YOUR_TOKEN_HERE';
      await MilioSdk.openMilioPayRechargeWallet(sdkToken);
    } catch (error) {
      Alert.alert('Error', 'No se pudo ejecutar la operación');
    }
  };

  const handleAliasConfig = async () => {
    try {
      const sdkToken = 'YOUR_TOKEN_HERE';
      await MilioSdk.openPaymentAliasConfiguration(sdkToken);
    } catch (error) {
      Alert.alert('Error', 'No se pudo ejecutar la operación');
    }
  };

  const handleCrossBorderRequest = async () => {
    try {
      const sdkToken = 'YOUR_TOKEN_HERE';
      await MilioSdk.openCrossBorderRequest(sdkToken);
    } catch (error) {
      Alert.alert('Error', 'No se pudo ejecutar la operación');
    }
  };

  const handleSendingMoneyRequest = async () => {
    try {
      const sdkToken = 'YOUR_TOKEN_HERE';
      await MilioSdk.openSendingMoneyRequest(sdkToken);
    } catch (error) {
      Alert.alert('Error', 'No se pudo ejecutar la operación');
    }
  };

  const handleSendingMoneyLocal = async () => {
    try {
      const sdkToken = 'YOUR_TOKEN_HERE';
      await MilioSdk.openSendingMoneyRequestLocal(sdkToken);
    } catch (error) {
      Alert.alert('Error', 'No se pudo ejecutar la operación');
    }
  };

  const handleOutboundTransfer = async () => {
    try {
      const sdkToken = 'YOUR_TOKEN_HERE';
      await MilioSdk.openOutboundTransferSdk(sdkToken);
    } catch (error) {
      Alert.alert('Error', 'No se pudo ejecutar la operación');
    }
  };

  // ============================================
  // MARK: - Render
  // ============================================

  return (
    <>
      <SafeAreaView style={{ flex: 0, backgroundColor: '#0048ff' }} />
      <SafeAreaView style={styles.container}>
        <StatusBar backgroundColor="#f8f9fa" barStyle="dark-content" />

        {/* Header */}
        <View style={styles.headerBackground}>
          <View style={styles.header}>
            <Text style={styles.greeting}>Hola, Daniel</Text>
            <TouchableOpacity onPress={() => setShowConfig(!showConfig)}>
              <Icon name="settings" size={28} color="#ffffff" />
            </TouchableOpacity>
          </View>
          <Text style={styles.balanceText}>Saldo</Text>
          <Text style={styles.balance}>$ 7.979.010,00</Text>
          <View style={styles.card}>
            <Text style={styles.cardText}>VISA •••• 5850</Text>
            <Text style={styles.cardBrand}>milio</Text>
          </View>
        </View>

        {/* Scroll Content */}
        <ScrollView contentContainerStyle={styles.scrollContent}>

          {/* Configuration Panel */}
          {showConfig && (
            <View style={styles.configPanel}>
              {/* Environments */}
              <Text style={styles.configSectionTitle}>🌍 Ambiente</Text>
              {AVAILABLE_ENVIRONMENTS.map((env) => (
                <TouchableOpacity
                  key={env.id}
                  style={[
                    styles.configItem,
                    selectedEnvironment === env.id && styles.configItemSelected
                  ]}
                  onPress={() => changeEnvironment(env)}
                >
                  <Icon name={env.icon} size={20} color="#666" />
                  <Text style={styles.configItemText}>{env.name}</Text>
                  {selectedEnvironment === env.id && (
                    <Icon name="check-circle" size={20} color="#0048ff" />
                  )}
                </TouchableOpacity>
              ))}

              {/* Themes */}
              <Text style={[styles.configSectionTitle, { marginTop: 20 }]}>🎨 Tema</Text>
              {AVAILABLE_THEMES.map((theme) => (
                <TouchableOpacity
                  key={theme.id}
                  style={[
                    styles.configItem,
                    selectedTheme === theme.id && styles.configItemSelected
                  ]}
                  onPress={() => applyTheme(theme)}
                >
                  <Icon name={theme.icon} size={20} color="#666" />
                  <Text style={styles.configItemText}>{theme.name}</Text>
                  {selectedTheme === theme.id && (
                    <Icon name="check-circle" size={20} color="#0048ff" />
                  )}
                </TouchableOpacity>
              ))}

              {/* Languages */}
              <Text style={[styles.configSectionTitle, { marginTop: 20 }]}>🌐 Idioma</Text>
              {AVAILABLE_LANGUAGES.map((lang) => (
                <TouchableOpacity
                  key={lang.id}
                  style={[
                    styles.configItem,
                    selectedLanguage === lang.code && styles.configItemSelected
                  ]}
                  onPress={() => changeLanguage(lang)}
                >
                  <Text style={{ fontSize: 20 }}>{lang.flag}</Text>
                  <Text style={styles.configItemText}>{lang.name}</Text>
                  {selectedLanguage === lang.code && (
                    <Icon name="check-circle" size={20} color="#0048ff" />
                  )}
                </TouchableOpacity>
              ))}
            </View>
          )}

          {/* Transaction Actions */}
          <View style={styles.actions}>

            <TouchableOpacity style={styles.actionButton} onPress={handleRecargarManual}>
              <Text style={styles.actionText}>Recargar manual</Text>
              <Icon name="credit-card" size={24} color="#666" />
            </TouchableOpacity>

            <TouchableOpacity style={styles.actionButton} onPress={handleTransferenciaInmediata}>
              <Text style={styles.actionText}>Transferencia inmediata</Text>
              <Icon name="flash-on" size={24} color="#666" />
            </TouchableOpacity>

            <TouchableOpacity style={styles.actionButton} onPress={handleRecargarBilletera}>
              <Text style={styles.actionText}>Recargar mi billetera</Text>
              <Icon name="account-balance-wallet" size={24} color="#666" />
            </TouchableOpacity>

            <TouchableOpacity style={styles.actionButton} onPress={handleAliasConfig}>
              <Text style={styles.actionText}>Tokenización de tarjetas</Text>
              <Icon name="credit-card" size={24} color="#666" />
            </TouchableOpacity>

            <TouchableOpacity style={styles.actionButton} onPress={handleCrossBorderRequest}>
              <Text style={styles.actionText}>Transferencia global</Text>
              <Icon name="public" size={24} color="#666" />
            </TouchableOpacity>

            <Text style={styles.sectionDivider}>Envío de Dinero</Text>

            <TouchableOpacity style={styles.actionButton} onPress={handleOutboundTransfer}>
              <Text style={styles.actionText}>Envío de dinero</Text>
              <Icon name="send" size={24} color="#666" />
            </TouchableOpacity>

            <TouchableOpacity style={styles.actionButton} onPress={handleSendingMoneyRequest}>
              <Text style={styles.actionText}>Envío Cross-Border</Text>
              <Icon name="flight-takeoff" size={24} color="#666" />
            </TouchableOpacity>

            <TouchableOpacity style={styles.actionButton} onPress={handleSendingMoneyLocal}>
              <Text style={styles.actionText}>Envío Local</Text>
              <Icon name="place" size={24} color="#666" />
            </TouchableOpacity>
          </View>

          {/* Transactions Section */}
          <Text style={styles.sectionTitle}>Transacciones</Text>
          <View style={styles.transactionsPlaceholder}>
            <View style={styles.skeleton} />
            <View style={styles.skeleton} />
          </View>
        </ScrollView>
      </SafeAreaView>
    </>
  );
};

// ============================================
// MARK: - Styles
// ============================================

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f8f9fa',
  },
  headerBackground: {
    backgroundColor: '#0048ff',
    padding: 20,
    paddingTop: 20,
    paddingBottom: 1,
    borderBottomLeftRadius: 25,
    borderBottomRightRadius: 25,
  },
  header: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
  },
  scrollContent: {
    paddingBottom: 40,
  },
  greeting: {
    fontSize: 22,
    fontWeight: 'bold',
    color: '#ffffff',
    fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
  },
  balanceText: {
    marginTop: 10,
    fontSize: 16,
    color: '#ffffff',
    fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
  },
  balance: {
    fontSize: 30,
    fontWeight: 'bold',
    color: '#ffffff',
    paddingBottom: 20,
    fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
  },
  card: {
    backgroundColor: '#000',
    borderRadius: 10,
    borderBottomLeftRadius: 0,
    borderBottomRightRadius: 0,
    padding: 25,
    paddingTop: 30,
    paddingBottom: 50,
    marginHorizontal: 5,
    marginTop: 10,
    flexDirection: 'row',
    justifyContent: 'space-between',
  },
  cardText: {
    fontSize: 18,
    color: '#fff',
    fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
  },
  cardBrand: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#fff',
    fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
  },
  configPanel: {
    backgroundColor: '#fff',
    margin: 20,
    padding: 20,
    borderRadius: 10,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 3,
    elevation: 3,
  },
  configSectionTitle: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#333',
    marginBottom: 10,
  },
  configItem: {
    flexDirection: 'row',
    alignItems: 'center',
    padding: 12,
    borderRadius: 8,
    marginBottom: 8,
    backgroundColor: '#f8f9fa',
  },
  configItemSelected: {
    backgroundColor: '#e3f2fd',
    borderWidth: 1,
    borderColor: '#0048ff',
  },
  configItemText: {
    flex: 1,
    marginLeft: 12,
    fontSize: 14,
    color: '#333',
  },
  actions: {
    marginTop: 20,
    paddingHorizontal: 20,
  },
  actionButton: {
    backgroundColor: '#fff',
    borderRadius: 10,
    padding: 30,
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: 10,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 3,
    elevation: 3,
  },
  actionText: {
    fontSize: 16,
    fontWeight: '500',
    color: '#333',
    fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
  },
  sectionDivider: {
    fontSize: 14,
    fontWeight: 'bold',
    color: '#999',
    textAlign: 'center',
    marginVertical: 15,
  },
  sectionTitle: {
    fontSize: 18,
    fontWeight: 'bold',
    marginVertical: 10,
    paddingHorizontal: 20,
    fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
  },
  transactionsPlaceholder: {
    backgroundColor: '#f0f0f0',
    padding: 20,
    borderRadius: 10,
    marginHorizontal: 20,
  },
  skeleton: {
    height: 20,
    backgroundColor: '#ddd',
    borderRadius: 5,
    marginBottom: 10,
  },
});

export default App;