getHtml static method
Implementation
static String getHtml({
required String publicKey,
required int amount,
required bool sandbox,
String currency = 'xof',
required String companyName,
String? color,
String? logoUrl,
String? callbackUrl,
String? inlineSdkScript,
}) {
final cleanColor = color?.replaceAll('#', '') ?? 'green';
final safeCompanyName = _escapeHtml(companyName);
final safeLogoUrl = _escapeHtml(logoUrl ?? '');
final safeCallbackUrl = _escapeHtml(callbackUrl ?? '');
// Wrapped in a <script> tag so it can be dropped directly into <head>.
final sdkBlock =
inlineSdkScript != null ? '<script>\n$inlineSdkScript\n</script>' : '';
return '''
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Paiement Intram</title>
$sdkBlock
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f5f5f5;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
#loading {
text-align: center;
}
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: #29b3a6;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text {
color: #666;
font-size: 16px;
}
#error-container {
display: none;
background: #fff;
border-radius: 12px;
padding: 30px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
text-align: center;
max-width: 400px;
}
.error-icon { font-size: 50px; margin-bottom: 15px; }
.error-title { font-size: 20px; font-weight: 600; color: #e74c3c; margin-bottom: 10px; }
.error-message { color: #666; line-height: 1.6; margin-bottom: 20px; font-size: 15px; }
.retry-button {
background: #29b3a6;
color: white;
border: none;
padding: 12px 30px;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
}
</style>
</head>
<body>
<div id="loading">
<div class="spinner"></div>
<div class="loading-text">Initialisation du paiement...</div>
</div>
<div id="error-container">
<div class="error-icon">⚠️</div>
<div class="error-title">Erreur de paiement</div>
<div class="error-message" id="error-message"></div>
<button class="retry-button" onclick="retryPayment()">🔄 Réessayer</button>
</div>
<div id="widget-container"></div>
<script>
var config = {
public_key: '$publicKey',
amount: $amount,
sandbox: $sandbox,
currency: '$currency',
callback_url: '$safeCallbackUrl',
company: {
name: '$safeCompanyName',
template: 'default+',
color: '$cleanColor',
logo_url: '$safeLogoUrl'
}
};
var sdkLoaded = false;
var paymentInitialized = false;
var paymentDone = false;
var modalObserver = null;
var capturedModal = null;
function startModalObserver() {
var knownNodes = new Set(Array.from(document.body.children));
modalObserver = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === 1 && !knownNodes.has(node)) {
capturedModal = node;
}
});
mutation.removedNodes.forEach(function(node) {
if (node === capturedModal && !paymentDone) {
modalObserver.disconnect();
sendMessageToFlutter({
type: 'close_success',
cancelled: true,
timestamp: new Date().toISOString()
});
}
});
});
});
modalObserver.observe(document.body, { childList: true });
}
function showLoading() {
document.getElementById('loading').style.display = 'block';
document.getElementById('error-container').style.display = 'none';
document.getElementById('widget-container').style.display = 'none';
}
function hideLoading() {
document.getElementById('loading').style.display = 'none';
document.getElementById('widget-container').style.display = 'block';
}
function showError(message) {
document.getElementById('loading').style.display = 'none';
document.getElementById('widget-container').style.display = 'none';
document.getElementById('error-container').style.display = 'block';
document.getElementById('error-message').textContent = message || 'Une erreur est survenue';
}
function sendMessageToFlutter(message) {
try {
var messageStr = JSON.stringify(message);
if (window.FlutterChannel && window.FlutterChannel.postMessage) {
window.FlutterChannel.postMessage(messageStr);
}
if (window.flutter_inappwebview && window.flutter_inappwebview.callHandler) {
window.flutter_inappwebview.callHandler('paymentHandler', message);
}
if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.FlutterChannel) {
window.webkit.messageHandlers.FlutterChannel.postMessage(message);
}
console.log('📤 Message to Flutter:', message);
} catch (error) {
console.error('❌ Error sending message:', error);
}
}
function initIntramPayment() {
if (paymentInitialized) {
console.warn('⚠️ Payment already initialized');
return;
}
try {
console.log('🚀 Initializing payment...', config);
if (typeof intramOpenWidget === 'undefined') {
throw new Error('SDK Intram non chargé. Vérifiez votre connexion Internet.');
}
paymentInitialized = true;
sendMessageToFlutter({
type: 'loading',
message: 'Initialisation du widget...'
});
startModalObserver();
intramOpenWidget.init(config)
.then(function(response) {
paymentDone = true;
if (modalObserver) modalObserver.disconnect();
console.log(response, '****** responses');
var transactionId = response.transactionId || response.transaction_id || response.id || 'N/A';
sendMessageToFlutter({
type: 'close_success',
data: response,
transaction_id: transactionId,
timestamp: new Date().toISOString()
});
})
.catch(function(error) {
paymentDone = true;
if (modalObserver) modalObserver.disconnect();
console.error('❌ Payment error:', error);
paymentInitialized = false;
showError(error.message || 'Erreur lors du paiement');
sendMessageToFlutter({
type: 'error',
message: error.message || 'Unknown error',
code: 'PAYMENT_ERROR',
timestamp: new Date().toISOString()
});
});
} catch (error) {
console.error('❌ Init error:', error);
paymentInitialized = false;
showError(error.message);
sendMessageToFlutter({
type: 'error',
message: error.message,
code: 'INIT_ERROR',
timestamp: new Date().toISOString()
});
}
}
function retryPayment() {
console.log('🔄 Retrying payment...');
paymentInitialized = false;
document.getElementById('error-container').style.display = 'none';
showLoading();
setTimeout(initIntramPayment, 500);
}
window.onerror = function(message, source, lineno, colno, error) {
console.error('💥 Global error:', { message, source, lineno, colno, error });
sendMessageToFlutter({
type: 'error',
message: message.toString(),
code: 'GLOBAL_ERROR',
timestamp: new Date().toISOString()
});
return false;
};
function loadIntramSDK() {
// SDK may have been inlined by Flutter (pre-fetched to avoid WebView SSL issues).
if (typeof intramOpenWidget !== 'undefined') {
console.log('✅ Intram SDK loaded (inline)');
sdkLoaded = true;
sendMessageToFlutter({
type: 'ready',
message: 'SDK loaded successfully',
timestamp: new Date().toISOString()
});
setTimeout(initIntramPayment, 300);
return;
}
var script = document.createElement('script');
script.src = 'https://cdn.intram.org/sdk-javascript.js';
script.async = true;
script.onload = function() {
console.log('✅ Intram SDK loaded');
sdkLoaded = true;
sendMessageToFlutter({
type: 'ready',
message: 'SDK loaded successfully',
timestamp: new Date().toISOString()
});
setTimeout(initIntramPayment, 500);
};
script.onerror = function() {
console.error('❌ Failed to load Intram SDK');
showError('Impossible de charger le SDK Intram. Vérifiez votre connexion Internet.');
sendMessageToFlutter({
type: 'error',
message: 'Failed to load Intram SDK from CDN',
code: 'SDK_LOAD_ERROR',
timestamp: new Date().toISOString()
});
};
document.body.appendChild(script);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
loadIntramSDK();
});
} else {
loadIntramSDK();
}
</script>
</body>
</html>
''';
}