Introduction

In the following article we will be looking into credit card payment and credit card design using the Stripe element. Stripe is one of the online and in-person payment processing systems. It's not open source. In the below sample, we are using the test version. We can do operations like creating a customer, payment, custom UI element design using the Stripe card element, etc.

In the below sample, I have used the stripe.js npm package for Stripe payment processing in Angular and Stripe REST APIs.

And for business logic, I have used the dot net in this article. I have defined back-end functionality.

Package Installation

Use the below npm command to install Stripe in Angular.

npm install @stripe/stripe-js

Payment infromation

Create a customer using card tokenization

Creates a new customer in the Stripe system, associating a payment source; basically, it will create a customer after getting card info from users.

In this sample I am using a modal popup to get the card info.

creditcard.component.html

In the below sample, we are loading the card number, expiry date and CVV using stripe element.

<div class="modal-body">
    <!-- Modal content area -->
    <h2>Enter Your Credit Card Information</h2> 
    <!-- Title -->

    <form (ngSubmit)="onSubmit()"> 
        <!-- Angular form submission handler -->

        <!-- Card Number -->
        <label for="card-number">Card Number</label> 
        <!-- Label for card number -->
        <div id="card-number" class="stripe-element"></div> 
        <!-- Stripe Element placeholder for Card Number -->

        <!-- Expiry Date -->
        <label for="card-expiry">Expiration Date</label> 
        <!-- Label for expiration date -->
        <div id="card-expiry" class="stripe-element"></div> 
        <!-- Stripe Element placeholder for Expiration Date -->

        <!-- CVC -->
        <label for="card-cvc">CVC</label> 
        <!-- Label for CVC -->
        <div id="card-cvc" class="stripe-element"></div> 
        <!-- Stripe Element placeholder for CVC -->

        <!-- Errors -->
        <div id="card-errors" role="alert"></div> 
        <!-- Placeholder to show card input errors -->

        <!-- Buttons -->
        <div class="modal-buttons"> 
            <!-- Button container -->
            <button type="button" (click)="vm.closeCardModal()">Cancel</button> 
            <!-- Button to cancel the modal -->
            <button type="submit" [disabled]="isSubmitting">Submit</button> 
            <!-- Submit button, disabled while `isSubmitting` is true -->
        </div>
    </form>
</div>

creditcard.component.scss

.modal-body {
    background-color: #f9f9f9;
    padding: 20px;
    border-radius: 8px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
    max-width: 400px;
    margin: auto;
}
.modal-content {
  background: #fff;
  padding: 20px;
  border-radius: 8px;
  width: 400px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  text-align: center;
}

.modal-buttons {
  display: flex;
  justify-content: space-between;
  margin-top: 20px;
}

.requiredIndicate {
color: red; font-weight: bold;
}
h2 {
    text-align: center;
    color: #333;
}

form {
    display: flex;
    flex-direction: column;
}
.error-message{
    font-size: 15px;
    color: #df0707;
    padding: 2px;
}
label {
    margin-top: 10px;
    color: #555;
}

input {
    padding: 10px;
    margin-top: 5px;
    border: 1px solid #ccc;
    border-radius: 4px;
}

button {
    margin-top: 20px;
    padding: 10px;
    border: none;
    border-radius: 4px;
    cursor: pointer;
}

button[type="submit"] {
    background-color: #28a745;
    color: white;
}

button[type="button"] {
    background-color: #dc3545;
    color: white;
}

button:hover {
    opacity: 0.9;
}
#card-errors {
  color: red;
  font-size: 0.9em;
  margin-top: 5px;
}

creditcard.component.ts

In Component, we are loading the Stripe element to our UI element using Id. Don't hardcode the publish and secret key in the front end. try to get from backend

import { AfterViewInit, Component, EventEmitter, OnInit, Output } from '@angular/core';
import { loadStripe, Stripe, StripeCardCvcElement, StripeCardExpiryElement, StripeCardNumberElement, StripeElements } from '@stripe/stripe-js';

@Component({
  selector: 'app-creditcard',
  templateUrl: './creditcard.component.html', // HTML template for the component
  styleUrls: ['./creditcard.component.scss'] // SCSS styles for the component
})
export class CreditcardComponent implements OnInit, AfterViewInit { 

  // Stripe API keys (publishable key for client-side usage)
  publishKey: string = ""; // Please replace your stripe publish key here.

  // Stripe objects
  stripe: Stripe | null = null; // Stripe instance
  elements: StripeElements | null = null; // Stripe Elements instance
  cardNumber: StripeCardNumberElement | null = null; // Card Number Element
  cardExpiry: StripeCardExpiryElement | null = null; // Expiry Date Element
  cardCvc: StripeCardCvcElement | null = null; // CVC Element

  isSubmitting: boolean = false; // Prevent duplicate submissions

  @Output() tokenGenerated = new EventEmitter<string>(); // Emit the generated token to parent component

  constructor() { }

  ngOnInit(): void {  
    // Component initialization logic (if needed)
  }

  async ngAfterViewInit() {
    // Load the Stripe instance with the publishable key
    this.stripe = await loadStripe(this.publishKey);

    if (this.stripe) {
      // Initialize Stripe Elements
      this.elements = this.stripe.elements();

      // Define the custom styles for Stripe Elements
      const style = {
        base: {
          color: '#32325d', // Default text color
          fontFamily: '"Roboto", sans-serif', // Font family
          fontSize: '16px', // Font size
          '::placeholder': {
            color: '#aab7c4', // Placeholder text color
          },
        },
        invalid: {
          color: '#fa755a', // Invalid input text color
          iconColor: '#fa755a', // Invalid icon color
        },
      };

      // Create and mount the Card Number Element
      this.cardNumber = this.elements.create('cardNumber', { style });
      this.cardNumber.mount('#card-number');

      // Create and mount the Expiry Date Element
      this.cardExpiry = this.elements.create('cardExpiry', { style });
      this.cardExpiry.mount('#card-expiry');

      // Create and mount the CVC Element
      this.cardCvc = this.elements.create('cardCvc', { style });
      this.cardCvc.mount('#card-cvc');

      // Attach event listeners to handle real-time validation errors
      this.cardNumber.on('change', this.displayError.bind(this));
      this.cardExpiry.on('change', this.displayError.bind(this));
      this.cardCvc.on('change', this.displayError.bind(this));
    }
  }

  displayError(event: any) {
    // Handle real-time validation errors
    const displayError = document.getElementById('card-errors');
    if (displayError) {
      if (event.error) {
        displayError.textContent = event.error.message; // Show error message
      } else {
        displayError.textContent = ''; // Clear error message
      }
    }
  }

  async onSubmit() {
    // Ensure Stripe and card elements are initialized
    if (!this.stripe || !this.cardNumber) {
      console.error('Stripe or card elements not initialized');
      return;
    }

    this.isSubmitting = true; // Set submission state to true

    // Create a token for the card details
    const { token, error } = await this.stripe.createToken(this.cardNumber);

    if (error) {
      // Display error message if tokenization fails
      const displayError = document.getElementById('card-errors');
      if (displayError && error.message) {
        displayError.textContent = error.message;
      }
    } else {
      // Emit the token to the parent component
      this.tokenGenerated.emit(token?.id);

      // Close the modal (assumes `vm.closeCardModal()` is defined)
      this.vm.closeCardModal();
    }

    this.isSubmitting = false; // Reset submission state
  }
}

Sample Output of UI Element

Output of UI Element

Modal Popup Open/Close

ngx-bootstrap npm package was used to open and close the modal. I have used a modal popup in the payment component to open and close the modal, so I have included it in payment.module.ts and declartion included.

package installation

npm i ngx-bootstrap

Include ModalModule in required module like below,

payment.module.ts

@NgModule({
  declarations: [PaymentComponent,CreditcardComponent],
  imports: [
    CommonModule,ReactiveFormsModule,FormsModule,NgToggleModule,
    PaymentRoutingModule, ModalModule.forRoot(),
  ]
})

payment.component.ts

Used to generate customer from the emitted output function creditcard.component.ts it will trigger customer generation.

openCardModal(): Used to open the modal popup with creditcard.component.ts.

 constructor(private modalService: BsModalService,private paymentService:PaymentService)  
  {
    
  }
openCardModal = () => {
  // Opens the CreditcardComponent as a modal
  this.modalRef = this.modalService.show(CreditcardComponent, {
    animated: true, // Enables animation for the modal
    keyboard: false, // Disables closing the modal via the ESC key
    backdrop: 'static', // Prevents clicking outside the modal to close it
    initialState: {
      vm: this.vm, // Passes a custom `vm` object to the modal component's input
    },
  });

  // Subscribes to the `tokenGenerated` EventEmitter from the modal component
  this.modalRef.content.tokenGenerated.subscribe((token: string) => {
    this.createCustomerForToken(token); // Calls the function to handle token processing
  });
};

createCustomerForToken(token) method

This method processes the token emitted by the modal and uses it to create a customer by calling the paymentService

createCustomerForToken(token: string) {
  // Calls the `createCustomer` method in the payment service
  this.paymentService
    .createCustomer(
      this.paymentService.GenerateCustomer(
        this.cardDetails.Name, // Name of the cardholder
        this.cardDetails.CardNumber, // Card number (masked or partial if passed)
        token // Token generated from Stripe
      )
    )
    .subscribe((data: any) => {
      // You can handle the response here, e.g., show success messages
    });
}

closeCardModal Method

This method closes the modal when the operation is complete or canceled.

closeCardModal = () => {
  this.modalRef?.hide(); // Hides the modal if `modalRef` exists
};

OnInit Method

This method used to initialize the vm's property and method

ngOnInit(): void {   
  this.vm = {
    cardHolderName: null,  // Placeholder for the cardholder's name input field (starts with null)
    cardNumber: null,      // Placeholder for the credit card number (starts with null)
    expiryDate: null,      // Placeholder for the card's expiry date (starts with null)
    cvv: null,             // Placeholder for the CVV input field (starts with null)
    
    // Methods to open and close the card modal
    openCardModal: this.openCardModal,  // Reference to the method that opens the credit card modal
    closeCardModal: this.closeCardModal, // Reference to the method that closes the modal
  };
}

payment.service.ts

Generate an object using the generate customer, and for REST APIs, I am using the previous article. Create customers with a tokenization block to generate customers based on tokens.

var apiUrl = ""; should be end point of the service.
 return {
    Email: Email,                     // Use the passed 'Email' parameter
    Name: Name,                       // Use the passed 'Name' parameter
    Phone: "1234567890",               // Replace with the actual phone number if needed
    Description: "Customer for test",  // Replace with an appropriate description if needed
    SourceID: token                    // The token passed from payment process
  };

public createCustomer(customer: any) {
  return this.apiHandler.post(apiUrl + "/create-customer-with-tokenisation", customer);
}
 public GeneratePaymentIntent(amount:number,offSession:boolean,paymentMethodID:string)
  {
    return {
      amount:amount,
      paymentMethodID: paymentMethodID,
      customerId: null,
      offSession:offSession
    };
  }
  public createPaymentIntent(paymentMethod:any)
  {
    return this.apiHandler.post(apiUrl + "/create-payment-intent",paymentMethod);
  }
  public createPayment(amount:any)
  {
    return this.apiHandler.post(apiUrl + "/create-payment-intent-without-paymentmethod",{ amount: amount });
  }

Pay using a card with the customer and payment method

Payment template used to pay amount using credit card.

<ng-template #paymentTemplate>
  <div class="modal-header">
    <h3 class="modal-title">Payment Information</h3>
  </div>
  <div class="modal-body">
    <div class="modal-content">
      <form (submit)="handlePayment($event)">
        <label>
          Without PaymentMethod:
          <ng-toggle  [(ngModel)]="isPaymentMethod" name="toggle" required />
        </label>
        <label>
          Card Holder Name:
          <input type="text" [(ngModel)]="cardHolderName" name="cardHolderName" required />
        </label>
        <label>
          Amount:
          <input type="text" [(ngModel)]="amount" name="amount" required />
        </label>
        <div id="pay-card-element" class="card-element"><!-- Stripe Card Element --></div>
        <div id="pay-card-errors" role="alert"></div>
        <div class="pay-modal-buttons">
          <button type="button" class="btn btn-failure" (click)="closeCardModal()">Cancel</button>
          <button type="submit" class="btn btn-success" [disabled]="loading">Pay {{ amount | currency }}</button>
        </div>
      </form>
    </div>
  </div>
</ng-template>

payment.component.ts

In payment component, variable declaration and constructor, Don't hard code publish key and secret key in html side try to get from server side.

// Declaring a dynamic object for storing values related to the modal and card details.
vm: any = {}; 

// Secret key for server-side Stripe API communication (should not be exposed on the client-side).
secretKey: string = ""; 

// Publishable key used for client-side Stripe communication.
publishKey: string = ""; 

// ViewChild decorator for referencing the modal template.
@ViewChild('paymentTemplate') paymentModal!: TemplateRef<any>;

// The modal reference to control its visibility and behavior.
modalRef!: BsModalRef;

// Stripe object initialized for Stripe.js integration (holds the Stripe instance).
private stripe: Stripe | null = null;

// The card element used for securely collecting card details.
private cardElement: any;

// States for loading, error messages, and cardholder's name.
public loading = false;
public error = '';
public cardHolderName = '';

// Amount to be processed for payment.
public amount = 1;

// A flag to track if it's a payment method or another type of payment action.
isPaymentMethod: boolean = true;  

// Constructor injecting modal service for modal control and payment service for backend communication.
constructor(private modalService: BsModalService, private paymentService: PaymentService) {}

// ngOnInit lifecycle hook, currently not used but reserved for any initialization logic.
ngOnInit(): void {   
  // Any initialization logic goes here if needed.
}

loadStripe() method

This method initializes Stripe Elements for securely collecting payment details.

async loadStripe() {
  // Load Stripe.js with the public key for client-side operations.
  this.stripe = await loadStripe(this.publishKey); 

  // Check if Stripe was successfully initialized.
  if (this.stripe) {
    // Initialize Stripe Elements, which will be used to securely collect card details.
    const elements = this.stripe.elements();

    // Create a 'card' element for collecting card details, hiding postal code field.
    this.cardElement = elements.create('card', {
      hidePostalCode: true
    });

    // Mount the card element onto the HTML element with id 'pay-card-element' to display it on the page.
    this.cardElement.mount('#pay-card-element');
  } else {
    // Log an error if Stripe is not initialized properly.
    console.error('Stripe is not initialized');
  }
}

handlePayment() Method

This method handles the payment process, triggered when the user submits the payment form

async handlePayment(event: Event) {
  // Prevent default form submission behavior.
  event.preventDefault();

  // Set loading state to true to show a loading spinner or message.
  this.loading = true;

  // Check if Stripe is initialized before proceeding.
  if (this.stripe) {
    
    // Call the backend to create a payment, passing the amount to charge.
    this.paymentService.createPayment(this.amount).subscribe(async (data: any) => {
      
      // Ensure Stripe is initialized before proceeding with payment confirmation.
      if (this.stripe) {
        // Use Stripe to confirm the payment using the card details and secret key.
        const { error } = await this.stripe.confirmCardPayment(this.secretKey, {
          payment_method: {
            card: this.cardElement, // The card element that holds the user's card information.
            billing_details: {
              name: this.cardHolderName, // The name of the cardholder.
            },
          },
        });

        // Handle error if payment confirmation fails.
        if (error) {
          this.error = error.message ? error.message : 'An unknown error occurred';
          this.loading = false; // Reset loading state.
        } else {
          this.error = ''; // Clear any previous errors.
          this.loading = false; // Reset loading state.
        }
      } else {
        // Error handling if Stripe is not initialized.
        this.error = 'Stripe is not initialized';
        this.loading = false; // Reset loading state.
      }
    });
    
  } else {
    // Error handling if Stripe is not initialized.
    this.error = 'Stripe is not initialized';
    this.loading = false; // Reset loading state.
  }
}

openpaymentModal() Method

This method opens the payment modal where the user can enter payment details.

openpaymentModal = () => {
  // Show the modal using the BsModalService, passing the initial state with the modal data.
  this.modalRef = this.modalService.show(this.paymentModal, {
    animated: true, // Enable animations for the modal.
    keyboard: false, // Disable closing the modal via the keyboard.
    backdrop: 'static', // Prevent closing the modal by clicking outside.
    initialState: {
      vm: this.vm // Pass initial data (vm) to the modal component.
    }
  });
  
  // Call loadStripe to initialize Stripe and display the payment form inside the modal.
  this.loadStripe();
}

Full code snippet of payment.compoent.ts

import { trigger, state, style, transition, animate } from '@angular/animations';
import { Component, OnInit, TemplateRef, ViewChild, ViewEncapsulation } from '@angular/core';
import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
import { CreditcardComponent } from '../creditcard/creditcard.component';
import { PaymentService } from './payment.service';
import { loadStripe, Stripe } from '@stripe/stripe-js';

@Component({
  selector: 'app-payment',
  standalone: false,
  encapsulation:ViewEncapsulation.None,
  templateUrl: './payment.component.html',
  styleUrl: './payment.component.scss'
   
})
export class PaymentComponent implements OnInit{
  
  vm: any={}; 
  secretKey:string ="sk_test_51Q2TApRw3m8aFNMV8RYvLabAEmwYOwHtiNWXrP1pR88IlVeuItDrhoxbb80YcCm45BoN9ncle0Mw84wv74Vc4rV700Y0gwbdh7";
  publishKey:string ="pk_test_51Q2TApRw3m8aFNMVKcnlDcs0EH3pu0EuhPpKyYe99eaBXE2ex9Nd1aSkCi4dxZQ3jBZwjlyrUAU30RN8nuGth6dv00HGMKk8vH"; 
 
  
  @ViewChild('paymentTemplate') paymentModal !: TemplateRef<any> ;
  modalRef!: BsModalRef;
  private stripe: Stripe | null = null;
private cardElement: any;
public loading = false;
public error = '';
public cardHolderName = '';
public amount = 1;
 isPaymentMethod:boolean =true;  
  constructor(private modalService: BsModalService,private paymentService:PaymentService)  
  {
    
  }

  ngOnInit(): void {   
    this.vm = {
      cardHolderName: null,
      cardNumber: null,
      expiryDate: null,
      cvv: null,
      openCardModal: this.openCardModal,
      closeCardModal: this.closeCardModal,
    };
  }
  

  openCardModal=()=> {
    this.modalRef = this.modalService.show(CreditcardComponent,{ animated: true,
      keyboard: false,
      backdrop : 'static',  initialState: {
        vm: this.vm
      }});
  this.modalRef.content.tokenGenerated.subscribe((token: string) => {   
  this.createCustomerForToken(token);
  });
  }
  createCustomerForToken(token: string) 
  {
    this.paymentService.createCustomer(this.paymentService.GenerateCustomer('', '', token)).subscribe((data: any) => {
       
    });
}
  closeCardModal=()=> {
    this.modalRef?.hide();
  } 
  
async loadStripe() {
  this.stripe = await loadStripe(this.publishKey); 
  if (this.stripe) {
    const elements = this.stripe.elements();
    this.cardElement = elements.create('card', {
      hidePostalCode: true
    });
    this.cardElement.mount('#pay-card-element');
  } else {
    console.error('Stripe is not initialized');
  }
}

async handlePayment(event: Event) {
  event.preventDefault();
  this.loading = true;

  if (this.stripe) {
    if(!this.isPaymentMethod){
      const { paymentMethod, error } = await this.stripe.createPaymentMethod({
        type: 'card',
        card: this.cardElement,
        billing_details: { name: this.cardHolderName },
      });

      if (error) {
        const displayError = document.getElementById('pay-card-errors');
      if (displayError && error.message) {
        displayError.textContent = error.message;
        this.loading = false;
      }
      } else {
        // Send payment method to backend
       
        this.makePayment(paymentMethod.id);
      }
    }
    else
    {
      this.paymentService.createPayment(this.amount).subscribe(async (data: any) => {
     
      
      if (this.stripe) {
        const { error } = await this.stripe.confirmCardPayment(this.secretKey, {
          payment_method: {
            card: this.cardElement,
            billing_details: {
              name: this.cardHolderName,
            },
          },
        });

        if (error) {
          this.error = error.message ? error.message : 'An unknown error occurred';
          this.loading = false;
        } else {
          this.error = '';
          this.loading = false;
        }
      } else {
        this.error = 'Stripe is not initialized';
        this.loading = false;
      }
    });
    }
  } else {
    this.error = 'Stripe is not initialized';
    this.loading = false;
  }

  
}

makePayment(paymentMethodId: string) {
  this.paymentService.createPaymentIntent(this.paymentService.GeneratePaymentIntent(this.amount, false, paymentMethodId)).subscribe((data: any) => {
     
  });
}
openpaymentModal=()=> {
  this.modalRef = this.modalService.show(this.paymentModal,{ animated: true,
    keyboard: false,
    backdrop : 'static',  initialState: {
      vm: this.vm
    }});
    this.loadStripe();
}
}

Sample UI

Sample UI

Conclusion

This approach enables the creation of a robust payment system integrated with Stripe, giving you the ability to scale and manage payments efficiently within your application.

Attached is the sample code.