An overview of my journey implementing multiple payment processing methods for Duffl, including the rationale behind these decisions and the challenges encountered along the way. If you don’t know what Duffl is, check this project out.
During the initial stages of our startup, customers used what we dubbed our "Hacky Venmo" method to pay for orders.
To explain this process briefly, a customer looking to fulfill their order would be instructed to go on either Venmo or Cash App and send us the correct amount.
Meanwhile on our side, a cleverly devised function subscribed to an email service webhook would wait for and extract payment information from the successful transaction email and then attempt to fulfill the corresponding order.
This approach proved highly effective for a startup catering to a college-aged user demographic that preferred Venmo over traditional credit card payments. However, as our business expanded, we encountered various limitations. For starters, we could not afford to rely on the stability of email servers for our sole payment processor. Secondly, as we later found out from our account being temporarily frozen, the process itself violated Venmo’s terms and services. It became clear that we urgently needed a robust payment processing solution capable of scaling with our growing business.
A snippet of the email parser service. As you can see we were locating information by using the email’s CSS as an anchor. Talk about flaky!
As our user base expanded to faculty, graduate students, and members of the nearby communities, we hypothesized that customers who order with credit cards would convert at a higher rate than customers who we redirect to another app.
January 7th, 2021. History was made. Our first successful credit card test order ever! I took UI design inspiration from our big brother Doordash.
I debuted credit cards into our payment processing options by integrating Stripe. Low fees and YC credits made it the obvious financial choice. Great documentation and a straightforward Javascript SDK made the integration a hassle-free experience. Unique keys in the header of requests called idempotency keys prevented duplicate transactions and improved reliability without much work on our end.
Overview of how Stripe integrates into our system.
With the introduction of alcohol and 21+ products into our store, we needed a high-risk transaction (HRT) processor. Between higher fees and sparse documentation, choosing the right HRT processor proved much less straightforward than deciding on Stripe. After weighing factors such as UI integration flexibility and dashboard extensibility we settled on Authorize.net.
Overview of how Authorize.net integrates into our system.
The OrderAttempt class prevents duplicate transactions, an issue that idempotency keys are normally supposed to cover.
To make use of free YC credits and broaden payment alternatives, I introduced Braintree to support Venmo (this time legally), Apple Pay, and Google Pay.
Overview of how all processors integrates into our system.
After implementation, we began receiving numerous bug reports regarding a mobile issue. With some detective work, I traced the issue to a difference in mobile browser behavior. Normally, we expect that selecting the Venmo option would send users from the mobile web view to the Venmo app. Upon confirmation in the app, users would be redirected back to the web page, where a one-time nonce would be ingested, guiding them to the payment pending page. However, some mobile browsers would open a new tab instead of returning to the original, thereby clearing the state necessary to complete the flow. To fix this I persisted the nonce in the URL params and conditionally ran a separate flow when this URL nonce was present.
With conditional logic increasing with each new solution, so did the need for a payment processor abstraction we could easily interface with on the backend. As a solution, I introduced a CustomerCard model with columns for stripe_payment_method_id,braintree_payment_method_token, andauthorizenet_payment_profile_id to represent a single card to display on the UI. Following PCP compliance, these cards are uniquely identified bylast_four_digits, expiration_month, andexpiration_year. When the user reaches the payment screen, a call to the /cards endpoint returns an array of available cards tied to a user for the UI to display. Users are also able to set a default payment method.
The CustomerCard model that represents a single card to be displayed on the UI.
I had to decide between two versions of the card collection UI: one using some creative CSS and one more traditional. Though I was proud of the former, the majority of feedback and votes leaned towards the latter, with users providing valid reasoning that sensitive data such as credit card information should adhere to standard conventions as much as possible.
First card collection UI design.
Second card collection UI design.
Imagine this scenario.
A few days ago you saved a card and placed an order. Today, you are placing an order with a 21+ item, but you have not created an Authorize.net card yet. You land on the review order page and see that not only is the card you previously saved no longer shown, but a brand new UI is trying to collect the same card information again.
You can imagine this would be a confusing experience.
With an abstraction of different processors onto fields of a CustomerCard class, we can address this scenario without offering more explanation than necessary. If a user checks out with a 21+ item, but their card cannot handle HRT, we still display the card as an option. If they select that card option, we direct that user to a “Verify Card” flow that is designed to look visually identical to the “Add Card” UI (except for terms and conditions text). Under the hood, we are using this information to create an HRT profile that later gets associated with the authorizenet_payment_profile_id column on theirCustomerCard object.
Cards will display even if a user has not saved a card with an HRT processor yet.
Accomplishing this meant achieving visual parity across different processor UI implementation libraries. I remember spending a good bit of time figuring out the regex to match Stripe’s out-the-box card number input field handling. In the end, just as a customer card abstraction on the backend simplifies logic, abstracting away the processors on the UI contributes to a more streamlined user journey.
Not too shabby for a company that once operated solely on Venmo.