Android ou iOS Nativo

Guia prático para integrar o checkout Pagaleve BNPL em apps nativos Android (Kotlin) e iOS (Swift) usando WebView. Focado na implementação do WebView e deep links.

Pré-requisitos

Android

  • Android Studio 4.0+
  • minSdkVersion 21+
  • Kotlin 1.8+ ou Java 8+
  • Conta merchant Pagaleve com credenciais de API

iOS

  • Xcode 14.0+
  • iOS 13.0+
  • Swift 5.0+
  • Conta merchant Pagaleve com credenciais de API

Implementação Android

Configuração do Projeto Android

Dependências (build.gradle - Module)

dependencies {
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
    implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0'
    implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
    implementation 'androidx.navigation:navigation-ui-ktx:2.7.6'

    // Networking
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
    implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'

    // JSON parsing
    implementation 'com.google.code.gson:gson:2.10.1'

    // Material Design
    implementation 'com.google.android.material:material:1.11.0'

    // Testing
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}

Configuração de Deep Links (AndroidManifest.xml)

<!-- app/src/main/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/Theme.PagaleveApp">

        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity
            android:name=".CheckoutActivity"
            android:exported="false" />

        <activity
            android:name=".SuccessActivity"
            android:exported="true"
            android:launchMode="singleTop">
            <intent-filter android:autoVerify="true">
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="your-android-app"
                      android:host="success" />
            </intent-filter>
        </activity>

        <activity
            android:name=".CancelActivity"
            android:exported="true"
            android:launchMode="singleTop">
            <intent-filter android:autoVerify="true">
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="your-android-app"
                      android:host="cancel" />
            </intent-filter>
        </activity>

    </application>
</manifest>

Recebendo a URL do Checkout

O checkout da Pagaleve deve ser criado no seu backend através da API. Uma vez criado, você receberá uma checkout_url que deve ser passada para o WebView do seu app Android ou iOS.

Exemplo de URL de checkout:

https://checkout.pagaleve.com.br/checkout/abc123...

Activities Android

MainActivity

// MainActivity.kt
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.button.MaterialButton
import com.google.android.material.textfield.TextInputEditText

class MainActivity : AppCompatActivity() {
    private lateinit var urlEditText: TextInputEditText
    private lateinit var openCheckoutButton: MaterialButton

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        initViews()
        setupClickListeners()
    }

    private fun initViews() {
        urlEditText = findViewById(R.id.urlEditText)
        openCheckoutButton = findViewById(R.id.openCheckoutButton)
    }

    private fun setupClickListeners() {
        openCheckoutButton.setOnClickListener {
            val checkoutUrl = urlEditText.text.toString().trim()

            if (checkoutUrl.isEmpty()) {
                Toast.makeText(this, "Por favor, insira a URL do checkout", Toast.LENGTH_SHORT).show()
                return@setOnClickListener
            }

            if (!isValidUrl(checkoutUrl)) {
                Toast.makeText(this, "URL inválida", Toast.LENGTH_SHORT).show()
                return@setOnClickListener
            }

            if (!checkoutUrl.contains("checkout.pagaleve.com.br")) {
                Toast.makeText(this, "Esta não parece ser uma URL da Pagaleve", Toast.LENGTH_LONG).show()
                return@setOnClickListener
            }

            val intent = Intent(this, CheckoutActivity::class.java)
            intent.putExtra("checkout_url", checkoutUrl)
            startActivity(intent)
        }
    }

    private fun isValidUrl(url: String): Boolean {
        return try {
            val uri = android.net.Uri.parse(url)
            uri.scheme != null && uri.host != null
        } catch (e: Exception) {
            false
        }
    }
}

CheckoutActivity (WebView)

// CheckoutActivity.kt
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.webkit.*
import androidx.appcompat.app.AppCompatActivity

class CheckoutActivity : AppCompatActivity() {
    private lateinit var webView: WebView

    @SuppressLint("SetJavaScriptEnabled")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_checkout)

        val checkoutUrl = intent.getStringExtra("checkout_url") ?: run {
            finish()
            return
        }

        webView = findViewById(R.id.webView)

        // Configure WebView settings
        val settings: WebSettings = webView.settings
        settings.javaScriptEnabled = true
        settings.domStorageEnabled = true
        settings.cacheMode = WebSettings.LOAD_NO_CACHE
        settings.mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW

        webView.webViewClient = object : WebViewClient() {
            override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
                val url = request?.url.toString()

                return when {
                    url.startsWith("your-android-app://success") -> {
                        val intent = Intent(this@CheckoutActivity, SuccessActivity::class.java)
                        startActivity(intent)
                        finish()
                        true
                    }
                    url.startsWith("your-android-app://cancel") -> {
                        val intent = Intent(this@CheckoutActivity, CancelActivity::class.java)
                        startActivity(intent)
                        finish()
                        true
                    }
                    else -> false
                }
            }

            override fun onPageStarted(view: WebView?, url: String?, favicon: android.graphics.Bitmap?) {
                super.onPageStarted(view, url, favicon)
                // Show loading indicator if needed
            }

            override fun onPageFinished(view: WebView?, url: String?) {
                super.onPageFinished(view, url)
                // Hide loading indicator if needed
            }

            override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
                super.onReceivedError(view, request, error)
                // Handle error
            }
        }

        webView.loadUrl(checkoutUrl)
    }

    override fun onBackPressed() {
        if (webView.canGoBack()) {
            webView.goBack()
        } else {
            super.onBackPressed()
        }
    }
}

SuccessActivity

// SuccessActivity.kt
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.button.MaterialButton

class SuccessActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_success)

        val backButton = findViewById<MaterialButton>(R.id.backButton)
        backButton.setOnClickListener {
            val intent = Intent(this, MainActivity::class.java)
            intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
            startActivity(intent)
            finish()
        }
    }
}

CancelActivity

// CancelActivity.kt
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.button.MaterialButton

class CancelActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_cancel)

        val tryAgainButton = findViewById<MaterialButton>(R.id.tryAgainButton)
        val backButton = findViewById<MaterialButton>(R.id.backButton)

        tryAgainButton.setOnClickListener {
            val intent = Intent(this, MainActivity::class.java)
            intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
            startActivity(intent)
            finish()
        }

        backButton.setOnClickListener {
            val intent = Intent(this, MainActivity::class.java)
            intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
            startActivity(intent)
            finish()
        }
    }
}

Layouts Android

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp"
    android:gravity="center">

    <ImageView
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:src="@drawable/ic_payment"
        android:layout_marginBottom="24dp"
        android:tint="@color/blue" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Pagaleve Checkout"
        android:textAlignment="center"
        android:textSize="24sp"
        android:textStyle="bold"
        android:layout_marginBottom="16dp" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Insira a URL do checkout recebida do seu backend"
        android:textAlignment="center"
        android:textSize="16sp"
        android:textColor="@color/gray"
        android:layout_marginBottom="32dp" />

    <com.google.android.material.textfield.TextInputLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="24dp">

        <com.google.android.material.textfield.TextInputEditText
            android:id="@+id/urlEditText"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="https://checkout.pagaleve.com.br/checkout/..."
            android:inputType="textUri"
            android:minLines="3"
            android:gravity="top" />

    </com.google.android.material.textfield.TextInputLayout>

    <com.google.android.material.button.MaterialButton
        android:id="@+id/openCheckoutButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Abrir Checkout" />

</LinearLayout>

activity_checkout.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <WebView
        android:id="@+id/webView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

Implementação iOS

Configuração do Projeto iOS

Info.plist Configuration

<!-- ios/Runner/Info.plist -->
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLName</key>
        <string>com.yourcompany.yourapp</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>your-ios-app</string>
        </array>
    </dict>
</array>

<!-- Allow arbitrary loads for development -->
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>

Modelo Simples para URL iOS

// Models/CheckoutURL.swift
import Foundation

struct CheckoutURL {
    let url: String

    var isValid: Bool {
        guard let url = URL(string: url) else { return false }
        return url.scheme != nil && url.host != nil
    }

    var isPagaleveURL: Bool {
        return url.contains("checkout.pagaleve.com.br")
    }
}

View Controllers iOS

ViewController Principal

// ViewControllers/ViewController.swift
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var urlTextView: UITextView!
    @IBOutlet weak var openCheckoutButton: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }

    private func setupUI() {
        title = "Pagaleve Checkout"

        // Configure text view
        urlTextView.layer.borderColor = UIColor.systemGray4.cgColor
        urlTextView.layer.borderWidth = 1
        urlTextView.layer.cornerRadius = 8
        urlTextView.font = UIFont.systemFont(ofSize: 16)
        urlTextView.text = "https://checkout.pagaleve.com.br/checkout/..."
        urlTextView.textColor = UIColor.placeholderText

        urlTextView.delegate = self
    }

    @IBAction func openCheckoutButtonTapped(_ sender: UIButton) {
        guard let urlText = urlTextView.text?.trimmingCharacters(in: .whitespacesAndNewlines),
              !urlText.isEmpty,
              urlText != "https://checkout.pagaleve.com.br/checkout/..." else {
            showAlert(message: "Por favor, insira a URL do checkout")
            return
        }

        guard isValidURL(urlText) else {
            showAlert(message: "URL inválida")
            return
        }

        guard urlText.contains("checkout.pagaleve.com.br") else {
            showAlert(message: "Esta não parece ser uma URL da Pagaleve")
            return
        }

        presentCheckoutWebView(url: urlText)
    }

    private func isValidURL(_ string: String) -> Bool {
        guard let url = URL(string: string) else { return false }
        return url.scheme != nil && url.host != nil
    }

    private func presentCheckoutWebView(url: String) {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        if let checkoutVC = storyboard.instantiateViewController(withIdentifier: "CheckoutViewController") as? CheckoutViewController {
            checkoutVC.checkoutUrl = url
            present(checkoutVC, animated: true)
        }
    }

    private func showAlert(message: String) {
        let alert = UIAlertController(title: "Aviso", message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default))
        present(alert, animated: true)
    }
}

extension ViewController: UITextViewDelegate {
    func textViewDidBeginEditing(_ textView: UITextView) {
        if textView.textColor == UIColor.placeholderText {
            textView.text = ""
            textView.textColor = UIColor.label
        }
    }

    func textViewDidEndEditing(_ textView: UITextView) {
        if textView.text.isEmpty {
            textView.text = "https://checkout.pagaleve.com.br/checkout/..."
            textView.textColor = UIColor.placeholderText
        }
    }
}

CheckoutViewController (WebView)

// ViewControllers/CheckoutViewController.swift
import UIKit
import WebKit

class CheckoutViewController: UIViewController {

    @IBOutlet weak var webView: WKWebView!
    @IBOutlet weak var activityIndicator: UIActivityIndicatorView!

    var checkoutUrl: String?

    override func viewDidLoad() {
        super.viewDidLoad()
        setupWebView()
        loadCheckoutUrl()
    }

    private func setupWebView() {
        // Configure WebView for security
        let configuration = WKWebViewConfiguration()
        configuration.websiteDataStore = WKWebsiteDataStore.nonPersistent()

        webView = WKWebView(frame: view.bounds, configuration: configuration)
        webView.navigationDelegate = self
        webView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(webView)

        // Setup constraints
        NSLayoutConstraint.activate([
            webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            webView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])

        // Add close button
        navigationItem.leftBarButtonItem = UIBarButtonItem(
            barButtonSystemItem: .close,
            target: self,
            action: #selector(closeButtonTapped)
        )
    }

    @objc private func closeButtonTapped() {
        dismiss(animated: true)
    }

    private func loadCheckoutUrl() {
        guard let urlString = checkoutUrl,
              let url = URL(string: urlString) else {
            showError(message: "URL inválida")
            return
        }

        let request = URLRequest(url: url)
        webView.load(request)
    }

    private func showError(message: String) {
        let alert = UIAlertController(title: "Erro", message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
            self.dismiss(animated: true)
        })
        present(alert, animated: true)
    }
}

extension CheckoutViewController: WKNavigationDelegate {
    func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
        activityIndicator.startAnimating()
        activityIndicator.isHidden = false
    }

    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        activityIndicator.stopAnimating()
        activityIndicator.isHidden = true
    }

    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        guard let url = navigationAction.request.url else {
            decisionHandler(.allow)
            return
        }

        let urlString = url.absoluteString

        if urlString.hasPrefix("your-ios-app://success") {
            dismiss(animated: true) {
                self.presentSuccessScreen()
            }
            decisionHandler(.cancel)
        } else if urlString.hasPrefix("your-ios-app://cancel") {
            dismiss(animated: true) {
                self.presentCancelScreen()
            }
            decisionHandler(.cancel)
        } else {
            decisionHandler(.allow)
        }
    }

    private func presentSuccessScreen() {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        if let successVC = storyboard.instantiateViewController(withIdentifier: "SuccessViewController") as? SuccessViewController {
            if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
               let window = windowScene.windows.first {
                window.rootViewController?.present(successVC, animated: true)
            }
        }
    }

    private func presentCancelScreen() {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        if let cancelVC = storyboard.instantiateViewController(withIdentifier: "CancelViewController") as? CancelViewController {
            if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
               let window = windowScene.windows.first {
                window.rootViewController?.present(cancelVC, animated: true)
            }
        }
    }
}

SuccessViewController

// ViewControllers/SuccessViewController.swift
import UIKit

class SuccessViewController: UIViewController {

    @IBOutlet weak var successImageView: UIImageView!
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var messageLabel: UILabel!
    @IBOutlet weak var backButton: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }

    private func setupUI() {
        successImageView.image = UIImage(systemName: "checkmark.circle.fill")
        successImageView.tintColor = .systemGreen

        titleLabel.text = "Pagamento Concluído!"
        titleLabel.textColor = .systemGreen

        messageLabel.text = "Seu pagamento foi processado com sucesso.\nVocê receberá uma confirmação por email."

        backButton.setTitle("Voltar ao Início", for: .normal)
    }

    @IBAction func backButtonTapped(_ sender: UIButton) {
        dismiss(animated: true)
    }
}

CancelViewController

// ViewControllers/CancelViewController.swift
import UIKit

class CancelViewController: UIViewController {

    @IBOutlet weak var cancelImageView: UIImageView!
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var messageLabel: UILabel!
    @IBOutlet weak var tryAgainButton: UIButton!
    @IBOutlet weak var backButton: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }

    private func setupUI() {
        cancelImageView.image = UIImage(systemName: "xmark.circle.fill")
        cancelImageView.tintColor = .systemRed

        titleLabel.text = "Pagamento Cancelado"
        titleLabel.textColor = .systemRed

        messageLabel.text = "O pagamento foi cancelado ou não pôde ser processado.\nTente novamente ou escolha outro método de pagamento."

        tryAgainButton.setTitle("Tentar Novamente", for: .normal)
        backButton.setTitle("Voltar ao Início", for: .normal)
    }

    @IBAction func tryAgainButtonTapped(_ sender: UIButton) {
        dismiss(animated: true)
    }

    @IBAction func backButtonTapped(_ sender: UIButton) {
        dismiss(animated: true)
    }
}