React Native / Expo

Integração Pagaleve - React Native / Expo

Visão Geral

Guia prático para integrar o checkout Pagaleve BNPL em apps React Native/Expo usando WebView. Focado na implementação do WebView e deep links para uma experiência fluida.

Pré-requisitos

  • React Native 0.60+ ou Expo SDK 45+
  • Conta merchant Pagaleve com credenciais de API
  • react-native-webview instalado
  • Deep linking configurado

Instalação de Dependências

Para React Native CLI

npm install react-native-webview
# iOS
cd ios && pod install

Para Expo

npx expo install react-native-webview

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 componente WebView do seu app React Native.

Exemplo de URL de checkout:

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

Configuração de Deep Links

Para Expo Router

  1. Configure app.json:
{
  "expo": {
    "scheme": "your-app-scheme",
    "slug": "your-app-slug",
    "name": "Your App Name"
  }
}
  1. Layout Principal:
// app/_layout.tsx
import { Stack } from "expo-router";

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen name="index" options={{ title: "Checkout" }} />
      <Stack.Screen name="checkout" options={{ title: "Payment" }} />
      <Stack.Screen name="success" options={{ title: "Payment Success" }} />
      <Stack.Screen name="cancel" options={{ title: "Payment Failed" }} />
    </Stack>
  );
}

Para React Navigation

// navigation/AppNavigator.tsx
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";

const Stack = createNativeStackNavigator();

const linking = {
  prefixes: ["your-app://"],
  config: {
    screens: {
      Success: "success",
      Cancel: "cancel",
    },
  },
};

export default function AppNavigator() {
  return (
    <NavigationContainer linking={linking}>
      <Stack.Navigator>
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="Checkout" component={CheckoutScreen} />
        <Stack.Screen name="Success" component={SuccessScreen} />
        <Stack.Screen name="Cancel" component={CancelScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

Implementação do WebView

Componente de Checkout WebView

// components/CheckoutWebView.tsx
import React from "react";
import { View, Alert, ActivityIndicator } from "react-native";
import WebView from "react-native-webview";

interface CheckoutWebViewProps {
  checkoutUrl: string;
  onNavigationStateChange?: (navState: any) => void;
}

export default function CheckoutWebView({
  checkoutUrl,
  onNavigationStateChange,
}: CheckoutWebViewProps) {
  const handleError = (syntheticEvent: any) => {
    const { nativeEvent } = syntheticEvent;
    console.error("WebView Error:", nativeEvent);

    Alert.alert(
      "Erro de Carregamento",
      "Não foi possível carregar a página de pagamento. Tente novamente.",
      [{ text: "OK" }]
    );
  };

  const handleHttpError = (syntheticEvent: any) => {
    const { nativeEvent } = syntheticEvent;
    console.error("WebView HTTP Error:", nativeEvent);

    Alert.alert(
      "Erro de Conexão",
      `Erro HTTP ${nativeEvent.statusCode}. Verifique sua conexão.`,
      [{ text: "OK" }]
    );
  };

  return (
    <View style={{ flex: 1 }}>
      <WebView
        source={{ uri: checkoutUrl }}
        startInLoadingState={true}
        renderLoading={() => (
          <View
            style={{ flex: 1, justifyContent: "center", alignItems: "center" }}
          >
            <ActivityIndicator size="large" />
          </View>
        )}
        incognito={true}
        allowsBackForwardNavigationGestures={true}
        originWhitelist={["https://*"]}
        onError={handleError}
        onHttpError={handleHttpError}
        onNavigationStateChange={onNavigationStateChange}
        style={{ flex: 1 }}
        // Configurações de segurança adicionais
        mixedContentMode="never"
        allowsInlineMediaPlayback={false}
        mediaPlaybackRequiresUserAction={true}
        javaScriptCanOpenWindowsAutomatically={false}
      />
    </View>
  );
}

Tela de Checkout (Expo Router)

// app/checkout.tsx
import React from "react";
import { View, Text } from "react-native";
import { useLocalSearchParams, useRouter } from "expo-router";
import CheckoutWebView from "../components/CheckoutWebView";

export default function CheckoutScreen() {
  const { checkoutUrl } = useLocalSearchParams<{ checkoutUrl: string }>();
  const router = useRouter();

  const handleNavigationStateChange = (navState: any) => {
    const { url } = navState;

    // Detectar deep links de retorno
    if (url.includes("your-app://success")) {
      router.replace("/success");
    } else if (url.includes("your-app://cancel")) {
      router.replace("/cancel");
    }
  };

  if (!checkoutUrl) {
    return (
      <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
        <Text>URL do checkout não fornecida</Text>
      </View>
    );
  }

  return (
    <CheckoutWebView
      checkoutUrl={checkoutUrl}
      onNavigationStateChange={handleNavigationStateChange}
    />
  );
}

Telas de Resultado

Tela de Sucesso

// app/success.tsx
import React from "react";
import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
import { useRouter } from "expo-router";

export default function SuccessScreen() {
  const router = useRouter();

  return (
    <View style={styles.container}>
      <View style={styles.content}>
        <Text style={styles.icon}>✅</Text>
        <Text style={styles.title}>Pagamento Concluído!</Text>
        <Text style={styles.message}>
          Seu pagamento foi processado com sucesso. Você receberá uma
          confirmação por email.
        </Text>

        <TouchableOpacity
          style={styles.button}
          onPress={() => router.replace("/")}
        >
          <Text style={styles.buttonText}>Voltar ao Início</Text>
        </TouchableOpacity>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#f5f5f5",
  },
  content: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    padding: 20,
  },
  icon: {
    fontSize: 64,
    marginBottom: 20,
  },
  title: {
    fontSize: 24,
    fontWeight: "bold",
    color: "#2e7d32",
    marginBottom: 16,
    textAlign: "center",
  },
  message: {
    fontSize: 16,
    color: "#666",
    textAlign: "center",
    lineHeight: 24,
    marginBottom: 32,
  },
  button: {
    backgroundColor: "#2e7d32",
    paddingHorizontal: 32,
    paddingVertical: 12,
    borderRadius: 8,
  },
  buttonText: {
    color: "white",
    fontSize: 16,
    fontWeight: "600",
  },
});

Tela de Cancelamento

// app/cancel.tsx
import React from "react";
import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
import { useRouter } from "expo-router";

export default function CancelScreen() {
  const router = useRouter();

  return (
    <View style={styles.container}>
      <View style={styles.content}>
        <Text style={styles.icon}>❌</Text>
        <Text style={styles.title}>Pagamento Cancelado</Text>
        <Text style={styles.message}>
          O pagamento foi cancelado ou não pôde ser processado. Tente novamente
          ou escolha outro método de pagamento.
        </Text>

        <View style={styles.buttonContainer}>
          <TouchableOpacity
            style={[styles.button, styles.primaryButton]}
            onPress={() => router.back()}
          >
            <Text style={styles.primaryButtonText}>Tentar Novamente</Text>
          </TouchableOpacity>

          <TouchableOpacity
            style={[styles.button, styles.secondaryButton]}
            onPress={() => router.replace("/")}
          >
            <Text style={styles.secondaryButtonText}>Voltar ao Início</Text>
          </TouchableOpacity>
        </View>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#f5f5f5",
  },
  content: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    padding: 20,
  },
  icon: {
    fontSize: 64,
    marginBottom: 20,
  },
  title: {
    fontSize: 24,
    fontWeight: "bold",
    color: "#d32f2f",
    marginBottom: 16,
    textAlign: "center",
  },
  message: {
    fontSize: 16,
    color: "#666",
    textAlign: "center",
    lineHeight: 24,
    marginBottom: 32,
  },
  buttonContainer: {
    width: "100%",
    gap: 12,
  },
  button: {
    paddingHorizontal: 32,
    paddingVertical: 12,
    borderRadius: 8,
    alignItems: "center",
  },
  primaryButton: {
    backgroundColor: "#1976d2",
  },
  secondaryButton: {
    backgroundColor: "transparent",
    borderWidth: 1,
    borderColor: "#666",
  },
  primaryButtonText: {
    color: "white",
    fontSize: 16,
    fontWeight: "600",
  },
  secondaryButtonText: {
    color: "#666",
    fontSize: 16,
    fontWeight: "600",
  },
});

Exemplo de Implementação Completa

Tela Principal

// app/index.tsx
import React, { useState } from "react";
import {
  View,
  Text,
  TextInput,
  TouchableOpacity,
  StyleSheet,
  Alert,
} from "react-native";
import { useRouter } from "expo-router";

export default function HomeScreen() {
  const [checkoutUrl, setCheckoutUrl] = useState("");
  const router = useRouter();

  const handleOpenCheckout = () => {
    if (!checkoutUrl.trim()) {
      Alert.alert("Erro", "Por favor, insira a URL do checkout");
      return;
    }

    // Validar se é uma URL válida
    try {
      new URL(checkoutUrl);
    } catch {
      Alert.alert("Erro", "URL inválida");
      return;
    }

    router.push({
      pathname: "/checkout",
      params: { checkoutUrl: checkoutUrl.trim() },
    });
  };

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Pagaleve Checkout</Text>
      <Text style={styles.subtitle}>
        Insira a URL do checkout recebida do seu backend
      </Text>

      <TextInput
        style={styles.input}
        placeholder="https://checkout.pagaleve.com.br/checkout/..."
        value={checkoutUrl}
        onChangeText={setCheckoutUrl}
        autoCapitalize="none"
        autoCorrect={false}
        multiline
      />

      <TouchableOpacity style={styles.button} onPress={handleOpenCheckout}>
        <Text style={styles.buttonText}>Abrir Checkout</Text>
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
    justifyContent: "center",
    backgroundColor: "#f5f5f5",
  },
  title: {
    fontSize: 28,
    fontWeight: "bold",
    textAlign: "center",
    marginBottom: 16,
    color: "#333",
  },
  subtitle: {
    fontSize: 16,
    textAlign: "center",
    marginBottom: 32,
    color: "#666",
    lineHeight: 22,
  },
  input: {
    borderWidth: 1,
    borderColor: "#ddd",
    padding: 16,
    borderRadius: 8,
    fontSize: 16,
    backgroundColor: "white",
    marginBottom: 24,
    minHeight: 80,
    textAlignVertical: "top",
  },
  button: {
    backgroundColor: "#1976d2",
    padding: 16,
    borderRadius: 8,
    alignItems: "center",
  },
  buttonText: {
    color: "white",
    fontSize: 16,
    fontWeight: "600",
  },
});