iOS SDK

O SDK iOS do smart checkout permite integrar a experiência de pagamento de forma nativa, proporcionando maior controle sobre a jornada do usuário, melhor performance e suporte a recursos avançados como carteiras digitais e deep links.


🚀 Principais recursos

  • Callbacks para sucesso, erro e fechamento.
  • Compatível com Apple Pay.
  • Personalização visual do bottom sheet.
  • Suporte a deep link.

📋 Requisitos

  • iOS 13.4 ou superior
  • Xcode 14.0 ou superior

⚙️ Instalação

Adicione ao seu Podfile:

target 'App' do
  use_frameworks!

  pod 'BemobiSmartCheckout', :podspec => 'https://smartcheckout-sdk-repo.s3.us-east-1.amazonaws.com/iOS/0.2.1/BemobiSmartCheckout.podspec'
end

Instale as dependências:

pod install

💳 Configuração do Apple Pay

Para habilitar Apple Pay, adicione ao seu App.entitlements:

<key>com.apple.developer.in-app-payments</key>
<array>
  <string>merchant.seu.identificador</string>
</array>

🔗 Integração via deep link

Para receber callbacks do smart checkout via deep link, adicione ao seu App.entitlements:

<key>com.apple.developer.associated-domains</key>
<array>
    <string>applinks:smart-checkout.bemobi.com</string>
    <string>webcredentials:smart-checkout.bemobi.com</string>
</array>

Estrutura do deep link: https://smart-checkout.bemobi.com/callback/{partnerKey}

Importante: para ativar corretamente o deep link, informe ao suporte:

  • App ID (Team ID + Bundle ID)

🧠 Inicialização e uso

Inicializar a engine. No seu AppDelegate:

import UIKit
import BemobiSmartCheckout

@main
final class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil
    ) -> Bool {
        SmartCheckout.initializeEngine()
        return true
    }
}

Exibir o smart checkout

SmartCheckout.showBottomSheet(
    from: self, // UIViewController
    config: SmartCheckout.Config(
        environment: .production,
        sessionCode: "a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d",
        partnerKey: "abc-1234",
        deepLink: URL(string: "https://smart-checkout.bemobi.com/callback/abc-1234"),
        initialHeightFraction: 1.0,
        style: .flat
    ),
    callbacks: SmartCheckout.Callbacks(
        onSuccess: { result in
            print("Checkout completed successfully: \(result)")
        },
        onError: { error in
            print("Checkout error: \(error)")
        },
        onClose: {
            print("Checkout closed")
        }
    )
)

Chamar o checkout via WebView (bridge)

import UIKit
import WebKit
import BemobiSmartCheckout

class WebViewController: UIViewController, WKScriptMessageHandler {
    private var webView: WKWebView!
    private let sessionCode: String
    private let environment: String

    init(sessionCode: String, environment: String) {
        self.sessionCode = sessionCode
        self.environment = environment
        super.init(nibName: nil, bundle: nil)
        
        // Inicializa a engine se ainda não foi feito no AppDelegate
        SmartCheckout.initializeEngine()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func postMessage(_ message: [String: Any]) {
        let jsonData = try! JSONSerialization.data(withJSONObject: message, options: [])
        let jsonString = String(data: jsonData, encoding: .utf8)!
        let script = "window.postMessage(\(jsonString), \"*\");"

        DispatchQueue.main.async {
            self.webView.evaluateJavaScript(script, completionHandler: nil)
        }
    }

    private func generateHtml() -> String {
        return """
        <!DOCTYPE html>
        <html>

        <head>
          <meta charset="utf-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
          <style>
            :root {
              --white: #ffffff;
              --blue-start: #2e7cf6;
              --blue-end: #1f63e5;
              --blue-shadow: rgba(31, 99, 229, 0.28);
              --gray-very-light: #f8fafc;
              --gray-light: #e5e7eb;
              --gray-divider: #f1f5f9;
              --gray-very-dark: #111827;
              --slate-deep: #0f172a;
              --slate-muted: #64748b;
              --shadow-soft: rgba(0, 0, 0, .04);
              --green-deep: #166534;
              --green-wash: #ecfdf5;
              --green-border: #bbf7d0;
              --red-deep: #7f1d1d;
              --red-wash: #fef2f2;
              --red-border: #fecaca;
              --indigo-deep: #1e3a8a;
              --indigo-wash: #eff6ff;
              --indigo-border: #bfdbfe;
            }

            body {
              display: flex;
              margin: max(24px, env(safe-area-inset-top, 24px)) 24px max(24px, env(safe-area-inset-bottom, 24px));
              gap: 24px;
              font-family: ui-monospace, monospace;
              font-size: 14px;
              flex-direction: column;
            }

            button {
              appearance: none;
              display: inline-flex;
              align-items: center;
              justify-content: center;
              padding: 14px 20px;
              min-width: 220px;
              font-size: 16px;
              font-weight: 700;
              letter-spacing: .2px;
              line-height: 1;
              color: var(--white);
              background: linear-gradient(180deg, var(--blue-start) 0%, var(--blue-end) 100%);
              border: none;
              border-radius: 12px;
              box-shadow: 0 6px 16px var(--blue-shadow);
              user-select: none;
              transition: transform .04s ease, filter .12s ease;
            }

            button:active {
              transform: translateY(1px) scale(0.99);
              filter: brightness(96%);
            }

            pre {
              margin: 6px 0 0;
              padding: 8px;
              background: var(--gray-very-light);
              border: 1px solid var(--gray-light);
              border-radius: 6px;
              overflow: auto;
              font-size: 12px;
              line-height: 1.5;
            }

            #log {
              padding: 12px;
              border-radius: 6px;
              max-height: 60vh;
              overflow: auto;
              background: var(--white);
              color: var(--gray-very-dark);
              border: 1px solid var(--gray-light);
              box-shadow: 0 2px 10px var(--shadow-soft);
            }

            #log #empty {
              padding: 16px;
              text-align: center;
              color: var(--slate-muted);
              background: var(--gray-very-light);
              border: 1px dashed var(--gray-light);
              border-radius: 8px;
              margin: 8px;
            }

            #log .item {
              border-bottom: 1px solid var(--gray-divider);
              padding: 10px 6px;
            }

            #log .item:last-child {
              border-bottom: 0;
            }

            #log .head {
              display: flex;
              gap: 8px;
              align-items: baseline;
              margin-bottom: 6px;
            }

            #log .time {
              color: var(--slate-muted);
              font-size: 12px;
            }

            #log .message {
              white-space: pre-wrap;
              margin-left: 2px;
            }

            .badge {
              font-size: 11px;
              font-weight: 700;
              padding: 2px 8px;
              border-radius: 999px;
              border: 1px solid var(--gray-light);
              background: var(--gray-very-light);
              color: var(--slate-deep);
            }

            .success .badge {
              color: var(--green-deep);
              background: var(--green-wash);
              border-color: var(--green-border);
            }

            .error .badge {
              color: var(--red-deep);
              background: var(--red-wash);
              border-color: var(--red-border);
            }

            .info .badge {
              color: var(--indigo-deep);
              background: var(--indigo-wash);
              border-color: var(--indigo-border);
            }

            .meta {
              display: flex;
              flex-direction: column;
              gap: 4px;
              color: var(--slate-deep);
            }

            .meta .label {
              font-weight: 700;
              text-transform: uppercase;
            }

            .meta .value {
              word-break: break-all;
            }
          </style>
        </head>

        <body>
          <div class="meta">
            <div class="label">Session Code</div>
            <div class="value">\(sessionCode)</div>
          </div>

          <div class="meta">
            <div class="label">Environment</div>
            <div class="value">\(environment)</div>
          </div>

          <button onclick="openSmartCheckout()">Open Smart Checkout SDK</button>

          <div id="log">
            <div id="empty">
              No events yet. Trigger a checkout to see logs here.
            </div>
          </div>

          <script>
            Object.defineProperty(window, 'smartCheckoutLoaded', {
              get() {
                const bridge = window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.startPaymentFlow;
                return !!bridge && typeof bridge.postMessage === 'function';
              },
            });

            const EVENT_LOG_MAP = {
              'checkout:action': { level: 'info', message: 'Custom action' },
              'checkout:success': { level: 'success', message: 'Payment completed successfully' },
              'checkout:error': { level: 'error', message: 'Payment error' },
              'checkout:close': { level: 'info', message: 'Checkout closed' },
            };

            function appendLog({ level, message, payload }) {
              const empty = document.getElementById('empty');
              if (empty) empty.remove();

              const item = document.createElement('div');
              item.className = ['item', level].join(' ');

              const head = document.createElement('div');
              head.className = 'head';

              const badge = document.createElement('span');
              badge.className = 'badge';
              badge.textContent = level.replace(/^\\w/, char => char.toUpperCase());

              const time = document.createElement('span');
              time.className = 'time';
              time.textContent = new Date().toLocaleTimeString();

              const text = document.createElement('div');
              text.className = 'message';
              text.textContent = message;

              head.appendChild(badge);
              head.appendChild(time);
              item.appendChild(head);
              item.appendChild(text);

              if (payload) {
                const pre = document.createElement('pre');
                pre.textContent = JSON.stringify(payload, null, 2);
                item.appendChild(pre);
              }

              const log = document.getElementById('log');
              log.prepend(item);
              log.scrollTop = 0;
            }

            function openSmartCheckout() {
              if (window.smartCheckoutLoaded) {
                const paymentData = {
                  sessionCode: "\(sessionCode)",
                  environment: "\(environment)"
                };

                window.webkit.messageHandlers.startPaymentFlow.postMessage(JSON.stringify(paymentData));
              } else {
                appendLog({ level: 'error', message: 'Smart Checkout bridge is not ready' });
              }
            }

            window.addEventListener('message', (event) => {
              const { type, payload } = event.data;
              const log = EVENT_LOG_MAP[type];
              if (log) appendLog({ ...log, payload });
            });
          </script>
        </body>

        </html>
        """
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        let contentController = WKUserContentController()
        contentController.add(self, name: "startPaymentFlow")

        let configuration = WKWebViewConfiguration()
        configuration.userContentController = contentController

        webView = WKWebView(frame: self.view.bounds, configuration: configuration)
        webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        webView.loadHTMLString(generateHtml(), baseURL: nil)

        view.addSubview(webView)
    }

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        if message.name == "startPaymentFlow" {
            let jsonString = message.body as! String
            let jsonData = jsonString.data(using: .utf8)!
            let jsonObject = try! JSONSerialization.jsonObject(with: jsonData, options: []) as! [String: String]
            let sessionCode = jsonObject["sessionCode"]!
            let environment = jsonObject["environment"]!

            SmartCheckout.setCustomActionHandler { payload in
                print("Custom action received: \(payload)")
                self.postMessage(["type": "checkout:action", "payload": payload])
            }

            SmartCheckout.showBottomSheet(
                from: self,
                config: SmartCheckout.Config(
                    environment: SmartCheckout.Environment(rawValue: environment.lowercased())!,
                    sessionCode: sessionCode,
                    initialHeightFraction: 0.9
                ),
                callbacks: SmartCheckout.Callbacks(
                    onSuccess: { result in
                        print("Checkout completed successfully: \(result)")
                        self.postMessage(["type": "checkout:success", "payload": result])
                    },
                    onError: { error in
                        print("Checkout error: \(error)")
                        self.postMessage(["type": "checkout:error", "payload": error])
                    },
                    onClose: {
                        print("Checkout closed")
                        self.postMessage(["type": "checkout:close"])
                    }
                )
            )
        }
    }
}

Registrar um handler para ações customizadas

SmartCheckout.setCustomActionHandler { payload in
    print("Custom action received: \(payload)")
}

Receber o deep link e iniciar o smart checkout. No seu AppDelegate:

import UIKit
import BemobiSmartCheckout

@main
final class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
        if let controller = UIApplication.shared.windows.first?.rootViewController {
            let config = SmartCheckout.Config(
                environment: .production,
                deepLink: url
            )
            let callbacks = SmartCheckout.Callbacks(
                onSuccess: { result in
                    print("Checkout completed successfully: \(result)")
                },
                onError: { error in
                    print("Checkout error: \(error)")
                },
                onClose: {
                    print("Checkout closed")
                }
            )
            SmartCheckout.showBottomSheet(
                from: controller,
                config: config,
                callbacks: callbacks
            )
            return true
        }
        return false
    }
}

🧠 Funcionalidades

  • SmartCheckout.initializeEngine(): inicializa a engine necessária para o fluxo do checkout.
  • SmartCheckout.showBottomSheet(from: UIViewController, config: Config, callbacks: Callbacks): exibe o checkout em um bottom sheet.
  • SmartCheckout.setCustomActionHandler(_ handler: @escaping ([String: Any]) -> Void): registra um handler para ações customizadas disparadas pelo fluxo do checkout.

⚙️ Configuração

A configuração do checkout é feita via SmartCheckout.Config:

  • environment: ambiente (.production, .sandbox, .development).
  • sessionCode: código da sessão (opcional).
  • partnerKey: identificador do parceiro usado no deep link para identificar a origem.
  • deepLink: URL que abre diretamente uma página específica do checkout (opcional).
  • initialHeightFraction: altura inicial do bottom sheet (opcional, 0.0 a 1.0).
  • style: estilo visual (.rounded, .flat)

🔔 Callbacks

  • onSuccess([String: Any]): sucesso no pagamento.
  • onError([String: Any]): erro no fluxo.
  • onClose(): fechamento do bottom sheet.

🧠 Boas práticas

  • Inicialize o SDK apenas uma vez no ciclo de vida do app.
  • Utilize deep link para garantir retorno confiável.
  • Sempre trate erros e fechamento do checkout.
  • Use Webhook como fonte oficial de status da transação.
  • Valide o fluxo em diferentes versões de iOS.

🎯 Resumo

O SDK iOS do smart checkout é a forma mais completa de integração mobile, permitindo uma experiência de pagamento fluida, nativa e altamente personalizável, com suporte a Apple Pay e deep linking.


Did this page help you?