📌 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.0instalado. - ✅ CocoaPods (
pod --versiondeberí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(setLogoWidth:(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)
// ============================================
// 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.setLogoWidth(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
)
}
// ============================================
// MARK: - Helper Methods
// ============================================
private enum TransactionType {
case qr
case manual
case addFunds
case recharge
case aliasConfiguration
case crossBorder
case sendingMoney
}
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
)
}
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://ruta del logo de la empresa.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:
| Propiedad | Descripción |
|---|---|
primaryColor | Color primario títulos. |
secondaryColor | Color para los botones secundario |
btnColorEnabled | Color del botón cuando está habilitado. |
btnColorDisabled | Color del botón cuando está deshabilitado. |
btnTextColorEnabled | Color del texto cuando el botón está habilitado. |
btnTextColorDisabled | Color del texto cuando el botón está deshabilitado. |
layoutBackgroundColor | Color de fondo del SDK. |
textViewColor | Color del texto en la interfaz del SDK. |
inputBorderColor | Color de los bordes |
textSelectionColor | Color 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');
}
};
Representacion grafica de los colores
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:
| Densidad | Factor | Ancho (px) | Alto (px) |
|---|---|---|---|
| mdpi | 1× | 544 | 200 |
| hdpi | 1.5× | 544 | 300 |
| xhdpi | 2× | 1088 | 400 |
| xxhdpi | 3× | 1632 | 600 |
| xxxhdpi | 4× | 2176 | 800 |
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.
