Stripe Intent API with Angular and ExpressJS for SCA and 3DS Compliant Sites

Mohammad Al Kalaleeb
7 min readJan 10, 2021

--

When building E-Commerce websites we use a third party payment provider. in my case I was afraid of any vendor lock-in and this is why we choose to build our own checkout page and not to relay on any redirections when it comes to checking out.

Stay tuned for Symfony Version at yes-soft.de

The Stack

While we mainly use Symfony for most of our projects, we wanted to try to create a TS stack from the bottom up.

The Stack from Back to end is:

  • MySQL as a DB Server
  • ExpressJS as the API (With TypeORM and TypeDI, and RoutingControllers for TypeScript Support) cuz… why not 😉
  • Angular as the frontend.

It is worth noting that I encountered a similar problems setting up the stack with Vue.JS also for some of you. in this documents I will provide a high level overview and how to set things up in code. so you can take what you need.

Old Sources API Method in Stripe

So, even currently Sources API is in the research I did was the most used in the case of angular, and the flow was as follows:

Problems and Changes

As of 1 — Jan 2021 This methods doesn’t work anymore for payments over 30 Euros in the EU. the new Law implement a new payment system in which Stripe can’t withdraw directly from the customer. the customer now need to confirm the payment using a method of several available.

Stripe Sources API Flow

The New Payment API

Understanding how to deal with this was really hard since I didn’t find a clear flow on how stuff should work. So I tested stuff and found that the new system works as following:

Stripe Elements API Flow

The Code

OK, again I’m using Angular and ExpressJS. So, here we go

ExpressJS API

Setup

I used https://github.com/typestack/routing-controllers/ to start a project.

Added

npm i --save stripe

And then I coded the following, well I didn’t do it like that, but this is a summery 😉

// File: OrdersController.ts

// I know that this needs to be deconstructed into multiple function, however this is a blog so deal with it :), any way sorry for the performance issues :)

// Create Orders in the Database
// This is equivlent to app.post('/orders', (request, response) => {}) in JS
@ResponseSchema(OrderResponse, {isArray: true})
@Post()
@OnUndefined(400)
@Authorized()
async createOrder(@CurrentUser() userId: string, @Body() body: any): Promise<OrderModel[]> {
// This should be here to group multiple products together, this assembles a Bill id.
const orderId = v1();

// fixing Side effect of jsonDecode
if (!Array.isArray(body)) {
body = [body];
}

// Mapping Products into orders, this can be achieved automatically using class-transformer
const newOrders: OrderModel[] = [];
let totalPrice = 0;
let user: UserModel;
for (let i of body.orders) {
const order = new OrderModel();
order.color = i.color;
order.size = i.size;
order.qty = -1 * i.qty;
order.orderId = orderId;
// This saves the data into the DB, however you want to do this it is OK
const newOrder = await this.orderManager.create(order, userId, i.productId);

// I'm using a DB relation in typeORM, this why I can grab the price like this
// If you have a quantity associated with it, multiply the price by it, and if you have a
// Discount system well, I don't know :)
totalPrice += newOrder.product.price;
user = newOrder.user;
if (newOrder) {
newOrders.push(newOrder);
}
}

const intent = new StripeIntentModel();
intent.customerName = body.holderName;
intent.customerEmail = body.holderEmail;
intent.amount = totalPrice * 100; // Conversion for Cents, this is in Euros
intent.paymentMethod = body.method

const intentResponse = await this.stripeManager.createPaymentIntent(intent, user);

// Now save the token for the orders, this will let us know what order is in question when Stripe informs us about the completed intent.
for (let o of newOrders) {
o.paymentId = intentResponse.data.object.id;
o.clientSecret = intentResponse.data.object.client_secret;
}

return intentResponse;
}

Now, in the managers it comes like:

// Stripe Manager

private stripeClient = new Stripe(this.stripeKey,
{apiVersion: '2020-08-27'}
);

public async createPaymentIntent(newIntent: StripeIntentModel, user: UserModel) {
try {
return this.stripeClient.paymentIntents.create({
amount: newIntent.amount,
currency: 'eur',
customer: user.stripeCustomerId,
receipt_email: newIntent.customerEmail,
shipping: {
name: newIntent.customerName,
address: {
country: user.profile.country,
state: user.profile.state,
line1: user.profile.shippingAddress01,
line2: user.profile.shippingAddress02,
postal_code: user.profile.zipCode
}
},
payment_method_types: [
'card',
'giropay'
],
});
} catch (e) {
console.log(e);
return undefined;
}
}

Now that the API returns the intent we have we can move to Angular to make stuff happens

Angular 11 Stripe Elements Setup

First we create the Project, Obviously

ng new checkout-project

then we implement the product and orders system, this is your homework.

next we start adding stripe:

yarn add -D @types/stripe-v3 # If using yarn like me
npm i @types/stripe-v3 --save-dev # If you are an NPM person

Next we add stripe into the main index.html as follows:

<!-- index.html -->
<script src="https://js.stripe.com/v3/"></script>
<script type="text/javascript">
const stripe =
Stripe('pk_***'); // You know the deal, copy past but from Stripe this tyme
const elements = stripe.elements();
</script>

Now we add the typings into typings.d.ts, without this you will get a compilation error so this is

Very Important

declare var module: NodeModule;
declare var stripe: any;
declare var elements: any;
declare var gtag; // For Google Analytics

interface NodeModule {
id: string;
}

Now, that we can use stripe, Lets create a stripe elements component.

To make this accessible we will split this into 3 stages. and here it is:

  1. Submit the Order
  2. mount Stripe Elements
  3. confirm payment intent

So, let us get started. I will be using a page component called StripePaymentComponent, you can generate one using the CLI, rout to it and follow me.

to learn how to use HttpClient I recommend using the official documentation, cuz the document is long enough that I can't include the details here

// First we Submit the order we intent to buy
// imports here

@Component({
selector: 'app-stripe-payment',
templateUrl: './stripe-payment.component.html',
styleUrls: ['./stripe-payment.component.scss']
})
export class StripePaymentComponent implements OnInit, AfterViewInit, OnDestroy {
// Orders the user wants to buy, of course you have data here right?
orders: Order[] = [];


// In case something happens
cardError: string;

// This is for billing info
paymentForm = new FormGroup({
email: new FormControl(''),
name: new FormControl('')
});

// This is the container from the card data
elementCard: any;

// We will mount elements to this
@ViewChild('paymentInfo') paymentView: ElementRef;
cardHandler = this.onChange.bind(this);

// This is the response from creating the order
intent: IntentResponse;

cardStyle = {
// I didn't include the styles from simplicity
};

constructor(
private cd: ChangeDetectorRef,
private orderService: OrdersService,
private router: Router,
) {
}

ngOnInit(): void {
}

ngAfterViewInit(): void {
this.elementCard = elements.create('card', {
cardStyle: this.cardStyle
});
this.elementCard.mount(this.paymentView.nativeElement);
}

ngOnDestroy(): void {
this.elementCard.unmount();
}


// ********** The intersting part is here **********
async submitOrder(): Promise<void> {

// Step 1, Create the Order, and get the intent
// By the way I'm using Observable.asPromise() to make await happen, you can use subscribe if you want
const intentResponse = await this.ordersService.createOrder(
// Map this to suite the API you have, for me however
{
holderName: this.paymentForm.get('name').value,
holderEmail: this.paymentForm.get('email').value,
orders: this.orders
});

// Step 2, Confirm the intent, automatically in my case
const confirmationResponse = await stripe.confirmCardPayment(
intentResponse.client_secret,
{
payment_method: {
card: this.elementCard,
},
}
);

// in Case error happens you have confirmationResponse to know what is it
}

private onChange({ error, bankName }): void {
this.bankName = bankName;
if (error) {
this.cardError = error.message;
} else {
this.cardError = null;
}
this.cd.detectChanges();
}
}

and in HTML it goes:

<!-- File: stripe-payment.component.html --><!-- Orders Section Should go here --><!-- Reactive Forms is the one i like, with angular material -->
<form [formGroup]="paymentForm" class="checkout example example5">
<div class="form-row inline">
<mat-form-field class="col">
<mat-label>
Name
</mat-label>
<input id="name" matInput formControlName="name" placeholder="Jenny Rosene" required>
</mat-form-field>
</div>
<div class="form-row inline">
<mat-form-field class="col">
<mat-label>
Email Address
</mat-label>
<input id="email" formControlName="email" type="email" placeholder="jenny.rosen@example.com" matInput required>
</mat-form-field>
</div>
<div>
<!-- Don't Touch this -->
<div id="card-info" #paymentInfo></div>
<!-- End -->
<button id="submit-button" (click)="submitOrder()">
Start Payment of &euro;<!-- Orders Total Due here -->
</button>
<mat-error id="card-errors" role="alert" *ngIf="cardError">
<mat-icon style="color: #f44336">cancel</mat-icon>
&nbsp;{{ cardError }}
</mat-error>
</div>
</form>

OK, now we are almost done. we have successfully captured the payment. what remains is to let the Backend know when the payment is done. we can do that by setting up a webhook in stripe and then we use it to confirm the payment as follows.

Stripe Integration with webhooks

  1. Login to stripe dashboard
  2. move to developers > webhooks section, this is https://dashboard.stripe.com/webhooks
  3. add endpoint in Endpoints receiving events from your account
  4. add the link to the API, for me https://mydomain.com/confirm-payment
  5. change the event type to: payment_intent.succeeded

Then move back into the backend API and add the following endpoint

// file StripeController.ts@Post('/confirm-payment')
async confirmPayment (@Body() body: any) {
return this.stripeManager.markPaid(body.data.object.id);
}

and in StripeManager.ts

// file StripeManager.tsasync markPaid(id: string) {
const orders = await this.orderService.findByPaymentId(id);

const affectedOrders: OrderModel[] = [];
for (let o of orders) {
o.paid = true;
affectedOrders.push(o)
}

const commitOrdersResponse = await this.orderService.updateBatch(affetedOrders);

return commitOrdersResponse;
}

and Voilà, Everything works.

have a great day and see you later 👋

--

--

Mohammad Al Kalaleeb
Mohammad Al Kalaleeb

Written by Mohammad Al Kalaleeb

Android to the core, AI and Machine Learning Enthusiast.

No responses yet