Aller au contenu

React Native

Le package @wajub/react-native, un hook, une interface de paiement et aucune WebView.

@wajub/react-native encaisse les paiements dans votre application React Native. Les champs Mobile Money sont natifs, les champs de carte proviennent de @stripe/stripe-react-native et le payeur ne voit jamais de WebView.

Version
1.1.1
Runtime
React 18+, React Native 0.74+
Frameworks
Android and iOS

Couvre

  • Paiements
  • Mobile Money
  • Cartes

Installer

npm install @wajub/react-native

Il fournit des sources TypeScript et déclare pusher-js comme seule dépendance réelle. Vous devez fournir trois dépendances homologues.

Dépendance homologuePlageUtilité
react>=18Toujours
react-native>=0.74Toujours
@stripe/stripe-react-native>=0.38.0Paiements par carte. Déclarée facultative

Stripe est facultatif, Mobile Money fonctionne donc sans lui

Sans @stripe/stripe-react-native, le package s'installe correctement et accepte les paiements Mobile Money. Seul l'onglet de carte en a besoin. Il nécessite aussi StripeProvider à la racine de votre application pour fonctionner.

Ajouter Stripe pour accepter les cartes
npm install @stripe/stripe-react-native

Obtenir un jeton depuis votre serveur

Aucun élément de ce package n'appelle /payments ni ne détient de clé secrète. Votre backend crée le paiement et renvoie authorization_token.

Votre endpoint et votre client
const response = await fetch('https://api.yourshop.com/checkout', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ cartId }),
});

const { authorization_token: token } = await response.json();

Afficher l'interface de paiement

usePayment fournit une fonction qui ouvre l'interface et une promesse qui renvoie le résultat.

Toute l'intégration
import { WajubProvider, usePayment } from '@wajub/react-native';

function Checkout({ token }: { token: string }) {
  const { present, PaymentSheet } = usePayment();

  const pay = async () => {
    const result = await present({ sessionToken: token });

    if ('cancelled' in result) return;

    switch (result.status) {
      case 'complete':
        navigation.navigate('OrderPaid', { ref: result.transaction.reference });
        break;
      case 'processing':
        setInstruction(result.instruction ?? 'Approve the payment on your phone.');
        break;
      case 'requires_action':
        // The sheet already opened the browser for you.
        break;
      case 'failed':
        Alert.alert('Payment failed', result.error.message);
        break;
    }
  };

  return (
    <>
      <Button title="Pay" onPress={pay} />
      {PaymentSheet}
    </>
  );
}

export default function App() {
  return (
    <WajubProvider>
      <Checkout token={token} />
    </WajubProvider>
  );
}

WajubProvider est facultatif. Sans lui, usePayment utilise la fabrique par défaut et le hook fonctionne partout. Ajoutez-le pour remplacer la fabrique de session à un seul endroit dans les tests.

Les quatre résultats

PaymentResult est une union discriminée par status. Un switch affine donc chaque branche.

statusRésultat
completePaiement réglé. transaction contient la référence
processingEnvoyé à l'opérateur. instruction contient le texte à afficher au payeur
requires_action3DS ou redirection bancaire. action_url a déjà été ouverte
failederror contient code, decline_code et retryable

present peut aussi renvoyer { cancelled: true }. C'est pourquoi la garde précède le switch.

Processing est le résultat normal pour Mobile Money

Un paiement Mobile Money renvoie rarement complete depuis l'interface. Le payeur doit encore approuver sur son téléphone. Vous recevez donc processing avec une instruction, comme composer un code USSD. Affichez ce texte, puis surveillez le statut ou attendez votre webhook.

Vos propres écrans à la place de l'interface

createSession renvoie le même objet que l'interface de paiement. Utilisez-le si le paiement doit s'intégrer à un design existant.

Lire la session, puis payer
import { createSession } from '@wajub/react-native';

const session = createSession(token);
const data = await session.loadSession();

// data.transaction.amount, data.transaction.currency, data.branding, data.locale
const operators = data.channels.filter(
  (c) => c.type.toLowerCase() === 'mobile_money' || c.type.toLowerCase() === 'mobile',
);

const result = await session.payMobileMoney({
  channel_slug: operators[0].slug,
  phone: '+237670000000',
  country: 'CM',
});
MéthodeFonction
loadSession(forceRefresh?)Montant, devise, canaux, image de marque et locale. Mis en cache après le premier appel
getSdkConfig()Paramètres du prestataire par canal, dont la clé publique Stripe
payMobileMoney(input)Envoie le push à l'opérateur
payCard(slug, paymentMethodId, name?)Traite une PaymentMethod Stripe déjà créée
process(channel, data)Solution brute pour tous les canaux
handleRedirectAction(result)Ouvre une URL 3DS ou bancaire dans le navigateur système
watchStatus(onUpdate, intervalMs?)S'abonne et renvoie la fonction de désabonnement
cancel()Abandonne la session et renvoie l'URL de redirection
cardChannelSlug()Canal de carte de la session chargée, s'il existe

Suivre le résultat

watchStatus renvoie sa propre fonction de désabonnement
useEffect(() => {
  const unsubscribe = session.watchStatus((result) => {
    if (result.status === 'complete') setOrderState('paid');
    if (result.status === 'failed') setOrderState('failed');
  });

  return unsubscribe;
}, [session]);

Il s'abonne avec Pusher lorsque la session contient des paramètres de temps réel. Sinon, il se replie sur un polling toutes les cinq secondes. Vous n'avez rien à choisir.

Cartes sans l'interface de paiement

La clé publique provient de la session, pas de votre code, car elle diffère entre la sandbox et le mode live, ainsi qu'entre les marchands.

StripeProvider à la racine, clé issue de la session
import { StripeProvider } from '@stripe/stripe-react-native';
import { createSession, StripeCardSection } from '@wajub/react-native';

function CardCheckout({ token }: { token: string }) {
  const session = useMemo(() => createSession(token), [token]);
  const [publishableKey, setKey] = useState<string | null>(null);
  const [name, setName] = useState('');

  useEffect(() => {
    session.loadSession().then(async () => {
      const config = await session.getSdkConfig();
      const slug = session.cardChannelSlug();
      setKey(slug ? (config.channels[slug]?.publishable_key ?? null) : null);
    });
  }, [session]);

  if (!publishableKey) return <ActivityIndicator />;

  return (
    <StripeProvider publishableKey={publishableKey}>
      <StripeCardSection
        cardholderName={name}
        onCardholderNameChange={setName}
        submitting={false}
        error={null}
        onPay={async (paymentMethodId) => {
          const slug = session.cardChannelSlug()!;
          const result = await session.payCard(slug, paymentMethodId, name);
          await session.handleRedirectAction(result);
        }}
      />
    </StripeProvider>
  );
}

Erreurs

Toute erreur générée par la session est une WajubError. Le résultat failed contient la même structure sous forme d'objet simple.

La même erreur à deux endroits
import { WajubError } from '@wajub/react-native';

try {
  const result = await session.payMobileMoney(input);
  if (result.status === 'failed') {
    // result.error is a plain WajubErrorShape, not a thrown instance
    showRetry(result.error.message, result.error.retryable);
  }
} catch (error) {
  if (error instanceof WajubError && error.type === 'rate_limit_error') {
    showRetryIn(error.retry_after_seconds ?? 60);
    return;
  }
  throw error;
}
ChampUtilité
typeapi_error, authentication_error, invalid_request_error, payment_error ou rate_limit_error
codeMotif lisible par la machine
decline_codePrésent sur une réponse 402, motif du refus du prestataire
retryableIndique si une nouvelle tentative doit être proposée
paramChamp fautif lors d'une erreur de validation
correlation_idValeur à communiquer au support
retry_after_secondsAttente imposée par une limite de requêtes

Tests

L'origine de l'API est une constante. Vous ne pouvez donc pas rediriger son URL de base. Configurez plutôt une clé de sandbox sur votre backend. Le jeton généré place tout le flux dans la sandbox, interface comprise. Les numéros qui produisent chaque résultat figurent dans Scénarios de test.

Pour un test unitaire, createSession accepte un second argument, un PayClient, que vous pouvez simuler.

Que pensez-vous de ce contenu ?