Zum Inhalt springen

3-D Secure holt den Kunden von Ihrer Seite

Die Authentifizierung schickt den Browser zur Bank und zurück, und alles, was React hielt, ist weg. Wie der Zustand einer Zahlung eine Weiterleitung überlebt, und was Sie solange zeigen.

4 Min. Lesezeit

Ein Kunde klickt auf bezahlen. Seine Bank entscheidet, dass diese Zahlung eine Authentifizierung braucht. Der Browser verlässt Ihre Seite für eine Seite, die Sie nie gesehen haben, der Kunde bestätigt mit einer App oder einem Code, und der Browser kommt zurück.

In diesem Moment ist jeder React-Zustand, den Ihr Checkout hielt, verschwunden. Der Komponentenbaum wurde ausgehängt, die Hooks sind zurückgesetzt, die Variable mit der Information, um welche Bestellung es ging, existiert nicht. Das ist kein Fehler und in Ihrem Code ist nichts zu reparieren - eine Weiterleitung ist eine Navigation, und Navigationen zerstören Client-Zustand.

Der Checkout muss so gebaut sein, dass dieser Verlust nichts kostet.

Was existieren muss, bevor der Kunde geht

Bevor der Browser irgendwohin geht, muss der Server bereits alles halten, was nötig ist, um die Rückkehr zu verstehen. Praktisch heißt das: eine Bestellzeile, in einem ausstehenden Zustand, mit einer eigenen Kennung.

Diese Kennung geht an zwei Stellen. Sie geht in die Metadaten der Zahlung beim Anbieter, damit ein später eintreffendes Ereignis die Bestellung ohne Nachschlagetabelle findet. Und sie geht als Pfadsegment in die return_url:

return_url: `https://example.com/orders/${order.id}/confirm`

Ein Pfad und keine Query, denn ein Pfad ist eine Seite, die Sie kontrollieren, und eine Query ist ein Parameter, den der Kunde ändern kann. Diese URL wird nach einem Umweg über einen Dritten von einem Browser besucht, und alles, was sie trägt, sollte ein Verweis auf etwas sein, das der Server prüfen kann, statt einer Behauptung, der er glauben muss.

Nichts über Betrag, Status oder Kunde gehört in diese URL. Sie benennt eine Bestellung; was diese Bestellung bedeutet, entscheidet der Server.

Die Rückkehrseite liest die Datenbank

Die Bestätigungsroute ist eine Server-Komponente. Sie lädt die Bestellung über die ID im Pfad, prüft, dass sie der fragenden Person gehört, und zeigt, was die Zeile sagt.

export default async function Confirm({ params }) {
  const { id } = await params
  const session = await auth()
  const order = await db.order.findUnique({ where: { id } })
 
  if (!order || order.userId !== session?.user?.id) notFound()
 
  if (order.status === 'paid')   return <Receipt order={order} />
  if (order.status === 'failed') return <Retry order={order} />
  return <Pending order={order} />
}

Die Eigentumsprüfung ist nicht optional. Bestellnummern landen in Browserverläufen, in geteilten Links, in Support-Tickets, und eine ID allein ist keine Berechtigung, einen Beleg mit der Adresse eines anderen zu sehen.

Beachten Sie, worauf die Seite nicht schaut: auf die Query-Parameter, die der Anbieter auf dem Rückweg angehängt hat. Manche davon tragen einen Status und helfen bei der Wahl der ersten Meldung, aber sie kamen durch einen Browser und sie entscheiden nicht, ob die Bestellung bezahlt ist.

Der dritte Zustand ist der entscheidende

Die meisten Teams bauen zwei Ausgänge, Erfolg und Fehlschlag, und entdecken den dritten in Produktion. Der dritte ist: Die Zahlung ist echt, der Kunde ist da, und Ihre Datenbank kennt die Antwort noch nicht, weil der Webhook nicht angekommen ist.

Meist dauert das ein paar Sekunden. Gelegentlich Minuten, und für diesen Fall muss der Bildschirm gestaltet sein.

Fragen Sie die Bestellzeile eine Weile nach - eine Client-Komponente, die über zehn oder fünfzehn Sekunden ein paarmal nachschaut, deckt fast jeden realen Fall ab. Dann hören Sie auf. Eine Seite, die ewig dreht, lehrt den Kunden, dass etwas kaputt ist, und als Nächstes zahlt er noch einmal, was aus einem langsamen Webhook eine Doppelbelastung und eine Erstattung macht.

Der Warte-Bildschirm sollte also, wenn das Nachfragen aufgibt, klar sagen, dass die Zahlung eingegangen ist und die Bestätigung abgeschlossen wird, die Bestellnummer zeigen und eine E-Mail zusagen. Dieser Text leistet echte Arbeit: Er ist der Unterschied zwischen einem Kunden, der wartet, und einem, der zweimal zahlt.

Nur der Zustand in der URL überlebt

Dahinter steht ein allgemeineres Prinzip, das weit über Zahlungen hinausgeht, und dies ist das schärfste Beispiel dafür.

Alles, was die Anwendung nach einer Navigation braucht, die sie nicht kontrolliert, muss dort leben, wo die Navigation nicht hinkommt: im URL-Pfad, in einem Cookie oder in der Datenbank. React-Zustand zählt nicht. sessionStorage zählt nicht, denn der Kunde kommt womöglich in einem anderen Tab zurück, auf einem anderen Gerät, über einen Link in einer E-Mail. Eine Komponente, die die Antwort hält, zählt nicht, denn sie wird nicht eingehängt sein.

Praktisch schiebt das den Checkout-Zustand auf den Server, wo App Router ihn ohnehin haben will. Die Caching-Frage hat dann eine feste Antwort: Diese Seiten werden nie gecacht. Eine Bestellbestätigung ist kundenspezifisch und ändert sich unter Ihnen; cookies() in der Route zu lesen macht sie dynamisch, und hier ist das genau das gewünschte Verhalten.

Der Rest des Checkouts - wer den Preis berechnet, wer die Bestellung anlegen darf, wo der Webhook landet - ist dieselbe Aufteilung des Vertrauens, und die Weiterleitung ist schlicht der Moment, in dem sie geprüft wird.

Unsere Handelsprojekte behandeln diesen Bildschirm als Teil der Zahlungsarbeit und nicht der Designarbeit, denn er ist das Letzte, was ein Kunde sieht, bevor er entscheidet, ob das Geld durchgegangen ist.

Verwandte Fragen

Passiert das bei jeder Zahlung?
Nein, und genau deshalb wird es übersehen. Die Authentifizierung verlangt die Bank des Kunden nach Betrag, Karte, Land und eigener Risikoeinschätzung, also kann ein Entwickler mit einer Karte in einem Land den ganzen Checkout bauen, ohne die Weiterleitung je zu sehen. In Produktion taucht sie dann bei einem erheblichen Teil der europäischen Zahlungen auf.
Können wir den Warenkorb in localStorage halten und danach wiederherstellen?
Für den Inhalt des Warenkorbs ist das eine vertretbare Bequemlichkeit. Für alles, was über das Ergebnis entscheidet, nicht - es lebt in einem Browser und fehlt, wenn derselbe Kunde den Bestätigungslink auf dem Laptop öffnet, und weil etwas, das der Kunde bearbeiten kann, kein Nachweis darüber ist, was er gezahlt hat.
Was soll die Rückkehrseite zeigen, wenn die Zahlung noch verarbeitet wird?
Die Wahrheit, und einen Weg nach vorn. Ein kurzes Nachfragen auf der Bestellzeile deckt den häufigen Fall ab, in dem der Webhook binnen Sekunden landet; danach soll die Seite aufhören zu drehen und sagen, dass die Bestätigung unterwegs ist, mit Bestellnummer und E-Mail. Ein endloser Spinner ist, wie ein Kunde beschließt, die Zahlung sei gescheitert, und noch einmal zahlt.
Gilt das nur für Stripe?
Überhaupt nicht. Jede weiterleitungsbasierte Methode hat dieselbe Form - iDEAL, Bancontact, die meisten Überweisungsverfahren, lokale Systeme am Golf und in der Türkei, und 3-D Secure auf Karten überall. Schickt der Ablauf den Browser irgendwohin und erwartet ihn zurück, gilt alles hier.

Zurück zu allen Artikeln