How to integrate Stripe with Angular
Integrating payment processing into a modern web application requires a balance between security, user experience, and developer efficiency.
With over 25 years of experience in software development and as the creator of CoreUI, I have integrated Stripe into numerous enterprise-grade Angular applications to handle complex billing cycles and one-time checkouts.
The most efficient and modern solution is to use the @stripe/stripe-js library together with Stripe Elements, which allows you to maintain PCI compliance while providing a fully customizable UI.
This approach ensures that sensitive card data never touches your server, leveraging Stripe’s secure infrastructure directly within your Angular components.
Use the official @stripe/stripe-js library to load the Stripe engine and mount Stripe Elements within your Angular components for secure payment collection.
1. Install Dependencies
The first step is to install the necessary packages. You need the Stripe.js loader to securely inject the Stripe script into your application and the type definitions for a better developer experience in TypeScript.
npm install @stripe/stripe-js
By using the loader, you ensure that the Stripe script is loaded asynchronously and only when needed, which helps maintain high performance in your Angular application.
2. Create a Stripe Service
I always recommend centralizing Stripe logic in a dedicated Angular service. This service handles the initialization of the Stripe instance and ensures that you are using a single instance throughout the application lifecycle.
import { Injectable } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { loadStripe, Stripe } from '@stripe/stripe-js'
import { firstValueFrom } from 'rxjs'
import { environment } from '../environments/environment'
@Injectable({
providedIn: 'root'
})
export class StripeService {
private stripePromise = loadStripe(environment.stripePublicKey)
constructor(private http: HttpClient) {}
async getStripe(): Promise<Stripe | null> {
return this.stripePromise
}
async createPaymentIntent(amount: number, currency: string): Promise<string> {
const response = await firstValueFrom(
this.http.post<{ clientSecret: string }>('/api/create-payment-intent', {
amount,
currency
})
)
return response.clientSecret
}
}
This service follows the singleton pattern, which is a standard practice we use in CoreUI Angular Dashboard Template for managing global utilities. It keeps your public API key in one place, handles backend communication for Payment Intents, and provides a clean interface for components to access the Stripe object.
3. Initialize Payment Elements
To collect payment details securely, you should use Stripe Elements. This creates a secure iframe hosted by Stripe, protecting your application from handling raw card data. The component fetches a clientSecret from your backend via the StripeService.
import { Component, OnInit, ViewChild, ElementRef } from '@angular/core'
import { CardModule, SpinnerModule, ButtonDirective } from '@coreui/angular'
import { StripeService } from './stripe.service'
import { Stripe, StripeElements, StripePaymentElement } from '@stripe/stripe-js'
@Component({
selector: 'app-checkout',
standalone: true,
imports: [CardModule, SpinnerModule, ButtonDirective],
templateUrl: './checkout.component.html'
})
export class CheckoutComponent implements OnInit {
@ViewChild('paymentElement') paymentElementRef!: ElementRef
private stripe!: Stripe
elements!: StripeElements
paymentElement!: StripePaymentElement
isLoading = true
constructor(private stripeService: StripeService) {}
async ngOnInit() {
const stripe = await this.stripeService.getStripe()
if (!stripe) return
this.stripe = stripe
// Fetch clientSecret from your backend
const clientSecret = await this.stripeService.createPaymentIntent(2999, 'usd')
this.elements = stripe.elements({ clientSecret })
this.paymentElement = this.elements.create('payment')
this.paymentElement.mount(this.paymentElementRef.nativeElement)
this.paymentElement.on('ready', () => {
this.isLoading = false
})
}
}
In this component, we use ViewChild to get a reference to the DOM element where Stripe will mount the payment interface. The createPaymentIntent call fetches the clientSecret from your backend, which is required to initialize the payment form securely.
4. Design the Checkout UI
For a professional look, wrap the Stripe Element in a CoreUI card and use a CoreUI button to trigger the payment. This ensures that your checkout form matches the rest of your administrative or user dashboard.
<c-card>
<c-card-body>
<div #paymentElement>
<!-- Stripe Element will be injected here -->
@if (isLoading) {
<div class='text-center'>
<c-spinner color='primary'></c-spinner>
<p>Loading payment form...</p>
</div>
}
</div>
<button
cButton
color='primary'
class='mt-3'
[disabled]='isLoading'
(click)='handlePayment()'
>
Pay Now
</button>
</c-card-body>
</c-card>
We utilize CoreUI Buttons and CoreUI Spinner to provide immediate visual feedback to the user while the Stripe iframe is loading. If you are starting a new project, you might find it useful to check how to create a new Angular project.
5. Handle Payment Submission
When the user clicks the payment button, you need to confirm the payment with Stripe. This process is asynchronous and involves a redirect or a synchronous confirmation depending on the payment method used (e.g., Card vs. SEPA).
async handlePayment() {
if (!this.stripe || !this.elements) return
const { error } = await this.stripe.confirmPayment({
elements: this.elements,
confirmParams: {
return_url: `${window.location.origin}/success`
}
})
if (error) {
console.error(error.message)
// Display error to your customer (e.g., using a CoreUI Alert)
}
// If no error, the user is redirected to return_url
}
The confirmPayment method handles everything from 3D Secure authentication to payment validation. It is crucial to provide a return_url so Stripe knows where to send the user after successful off-session authentication. Note there is no else branch — successful payments redirect automatically.
6. Formatting Currency for Display
When displaying the amount to be charged on your checkout page, it is a best practice to format it correctly according to the user’s locale. This builds trust and improves the conversion rate.
// Stripe amounts are in the smallest currency unit (e.g., cents)
const amount = 2999
const formattedAmount = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amount / 100)
// Result: '$29.99'
// For dynamic locale formatting
const formatPrice = (amount: number, currency: string, locale: string = 'en-US') => {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency.toUpperCase()
}).format(amount / 100)
}
console.log(formatPrice(2999, 'usd')) // '$29.99'
console.log(formatPrice(2999, 'eur', 'de')) // '29,99 €'
console.log(formatPrice(2999, 'gbp')) // '£29.99'
For more details on this, you can refer to my guide on how to format a number as currency in JavaScript. Always remember that Stripe handles amounts in the smallest currency unit (e.g., cents for USD), so divide by 100 before displaying.
Best Practice Note:
Always handle Stripe initialization in an Angular Service to avoid multiple script injections. In production environments, ensure you use loadStripe with your live publishable key and only serve your checkout page over HTTPS. The HttpClient must be available via provideHttpClient() in your application config. This is the same robust pattern we implement in CoreUI enterprise templates to ensure security and reliability across diverse payment workflows. Additionally, always verify the payment status on your backend using Stripe Webhooks to handle edge cases like a user closing the browser during redirect.



