Recurring payments with Stripe in Ionic 4 application

October 2019

It is strongly recommended to have read the part one of this tutorial to understand this one.

To manage recurring payments, you need to setup a payment intent (same process that for single payment), then display the stripe form to get user credit card detail, but this time when form is submitting instead of calling stripe.handleCardPayment, we will handleCardSetup. Within the result, we would have to associate our user with a customer in stripe system, and then save his payment method.

Setup a payment intent with Ionic and Django backend

When our ionic view initialize, we ask our django backend, to set up a payment intent

if (this.apiService.networkConnected) {
      this.apiService.getSetupIntentMethod().subscribe((results) => {
        // console.log("INTENT RECUP ==>")
        // console.log(JSON.stringify(results)) 
        this.paymentIntent = results;
      })
    }
    else {
      this.apiService.showNoNetwork()
    }

Setting up a payment intent, means that we will not use it to pay now, but later.

@api_view(['GET'])
def setupIntentMethod(request):
    stripe.api_key = STRIPE_API_KEY
    setup_intent = stripe.SetupIntent.create(
        usage='off_session',  # The default usage is off_session
    )
    return JsonResponse(setup_intent, safe=False)

We save the results of the api call in a variable into our ionic application, and then we setup our stripe form as in previous tutorial. The main difference will be managing the form once submitted.

if (!this.submitDone) {
          this.submitDone = true
          let clientSecret = this.paymentIntent["client_secret"]
          this.stripe.handleCardSetup(
            clientSecret, this.card, {
              payment_method_data: {
                billing_details: {
                  name: this.form.cardName,
                  address: {
                    postal_code: this.form.zipCode,
                  },
                  email: this.userManager.currentUser.email,
                  phone: this.userManager.currentUser.phoneNumber
                },
              }
            }
          ).then((result) => {
            // Handle result.error or result.paymentIntent
            // console.log("APRES SUBMIT RESULTAT")
            // console.log(JSON.stringify(result))
            if (result.error) {
              // Display error.message in your UI.
              //this.apiService.stopLoading()
              var errorElement = document.getElementById('card-errors');
              errorElement.textContent = result.error.message;
              this.submitDone = false;
            } else {
              this.createPaymentMethod(result)
            }
          })
        }

If the form has not already been submitted, we use the stripe.handleCardSetup() method and then if we get a result we call the createPaymentMethod() otherwise we display the error message received.

 createPaymentMethod(result) {
    // The setup has succeeded. Display a success message.
    let params = {
      "refUser": this.userManager.currentUser.id,
      "paymentMethodId": result["setupIntent"]["payment_method"],
      "zipCode": this.form.zipCode
    }
    if (this.apiService.networkConnected) {
      this.apiService.showLoading()
      this.apiService.createPaymentMethod(params).subscribe((reponse) => {

The createPaymentMethod() will call an api in our backend and passing the:

refUser The identifier of our user
paymentMethodId The identifier of the payment setup that we received from stripe method handleCardSetup()
zipCode The zip code of the user that we asked in the stripe form

Create a payment method with Stripe into a Django backend

On our api createpaymentmethod we will check if this user is already associated with a customer in the stripe platform.
To know this information, we will create two fields into our AppUser django models, the first one, will be the stripeCustomerId and the second one, will be the stripePaymentMethodId

stripeCustomerId = pgcrypto.EncryptedCharField(max_length=100, default='NONE', blank=True)
stripePaymentMethodId = pgcrypto.EncryptedCharField(max_length=100, default='NONE', blank=True) 

Using the field stripeCustomerId we can know if we have already associated the user with a stripe customer. If not, we create a customer with stripe (using the email of our user) and then we save its id in our user model

user = AppUser.objects.get(id=refUser)
if user.stripeCustomerId=="NONE":
	customer = stripe.Customer.create(email=user.email)
    logger.info("on creer customer %s"%customer)
    user.stripeCustomerId=customer["id"]

Now we need to attach the payment method id to this customer and we can save the payment method id into our AppUser model.

paymentMethod = stripe.PaymentMethod.attach(paymentMethodId, customer=user.stripeCustomerId)
logger.info("on a paymentMethod %s" % paymentMethod)
user.stripePaymentMethodId = paymentMethodId
user.save()

We could stop here since now our AppUser is associated with a stripe customer and a payment method. But in my Django models, i have created a CreditCard model to be able to display the credit card information in the Ionic application.
So i will use stripe to get the credit card detail information and save it in my CreditCard models.
During all the process, i catch the exceptions that can occured to send them back to the ionic application.

Here is the full code of the createpaymentmethod

@api_view(['POST'])
def createpaymentmethod(request):
    if request.method == "POST":
        print("ON RECOIT DATA")
        #TODO : change for PROD
        stripe.api_key = STRIPE_API_KEY
        paymentMethod=None
        try:
            refUser = request.data["refUser"]
            paymentMethodId = request.data["paymentMethodId"]
            cvc = ""
            lastDigit = ""
            month = ""
            year = ""
            zipCode = request.data["zipCode"]
            user = AppUser.objects.get(id=refUser)
            if user.stripeCustomerId=="NONE":
                customer = stripe.Customer.create(email=user.email)
                logger.info("on creer customer %s"%customer)
                user.stripeCustomerId=customer["id"]
            paymentMethod = stripe.PaymentMethod.attach(paymentMethodId, customer=user.stripeCustomerId)
            logger.info("on a paymentMethod %s" % paymentMethod)
            user.stripePaymentMethodId = paymentMethodId
            user.save()
            # retrieve card details
            details = stripe.PaymentMethod.retrieve(paymentMethodId)
            if details:
                logger.info("Details card %s"%details)
                card = details["card"]
                lastDigit = card["last4"]
                month = card["exp_month"]
                year = card["exp_year"]
            #delete if credit card alreay exists
            cards = CreditCard.objects.filter(refUser=refUser)
            for card in cards:
                card.delete()
            #Create credit card
            creditCard = CreditCard()
            creditCard.refUser = user
            creditCard.cvc = cvc
            creditCard.lastDigit = lastDigit
            creditCard.month = month
            creditCard.year = year
            creditCard.zipCode = zipCode
            creditCard.save()
            newdict = {'status': 'OK',"stripeCustomerId":user.stripeCustomerId,"lastDigit":lastDigit}
            return JsonResponse(newdict)
        except stripe.error.CardError as e:
            logger.error(e)
            body = e.json_body
            err = body.get('error', {})
            newdict = {'status': 'KO', 'message': err.get('message'),'code': err.get('code')}
            return JsonResponse(newdict)
        except Exception as e:
            logger.error(e)
            newdict = {'status': 'KO'}
            return JsonResponse(newdict)
    else:
        return HttpResponse(status=501)

Back to our ionic application, we can analyse the response of our api call and react appropriatly


  createPaymentMethod(result) {
    // The setup has succeeded. Display a success message.
    let params = {
      "refUser": this.userManager.currentUser.id,
      "paymentMethodId": result["setupIntent"]["payment_method"],
      "zipCode": this.form.zipCode
    }
    if (this.apiService.networkConnected) {
      this.apiService.showLoading()
      this.apiService.createPaymentMethod(params).subscribe((reponse) => {
        this.apiService.stopLoading()
        console.log(reponse)
        if (reponse) {
          if (reponse["status"] == "OK") {
            let card = {
              "lastDigit": reponse["lastDigit"],
              "cardName": this.form.cardName,
              "zipCode": this.form.zipCode
            }
            this.userManager.currentUser.stripePaymentMethodId = result["payment_method"];
            this.userManager.currentUser.stripeCustomerId = reponse["stripeCustomerId"];
            this.userManager.saveUser()
            this.userManager.saveCard(card)
         	//Continue your workflow in ionic application here
          }
          else {
            console.log(reponse)
            let message = reponse["message"];
            if (message != undefined) {
              let code = reponse["code"]
              console.log("Message " + message + " Code " + code)
              this.errorPaymentMessage(message)
            }
            else {
              this.errorPaymentServer()
            }
          }
        }
        else {
          console.log(reponse)
          this.errorPaymentServer()
        }
      })
    }
    else {
      this.apiService.showNoNetwork()
    }
  }

If everything is OK, we save the payment method id and stripeCustomerId in our local user object and we can continue the workflow of our ionic application (going to home page, ...) or we display message if an error occurs.

Pay with a saved payment method into our Ionic application

Now that our user is associated with a payment method in our backend, at any moment in our workflow, we can ask for a payment without asking for credit card detail again. We just need to display the amount to pay and a pay button.
To manage the payment we just need to create a specific api and passing the refUser identifier and the amount to pay.
Using the refUser we can obtain the stripePaymentMethodId and stripeCustomerId and pass it to stripe to process the payment.

intent = stripe.PaymentIntent.create(amount=int(PAYMENT_AMOUT),
                    payment_method = stripePaymentMethodId,
                    confirm=True,
                    customer =  stripeCustomerId,
                    currency='eur',
                )

Be careful you need to check the status of the payment, because errors can happened or with some credit card, banks are asking that the user identify himself each time.

if intent["status"]=="succeeded":

If status is succeeded, the payment has been done without problem and you can confirm to your ionic application.

If status is requires_action, that means that some action is required from the user and you need to inform your ionic application about this, and returning a client_secret token

elif intent["status"] == "requires_action":
	logger.info(intent)
	secret = intent["client_secret"]

Into our ionic application, when the api indicates that a required action is due, you need to call stripe again with the client_secret that the api returns.

confirmWithStripe(){
    console.log("==== Confirm with Stripe Secret "+this.secretKey+" PaymentMethod "+this.userManager.currentUser.stripePaymentMethodId)
    this.stripe.handleCardPayment(
      this.secretKey,   {
        payment_method:this.userManager.currentUser.stripePaymentMethodId
        } 
    ).then((result) => {
      console.log(result)
      if (result.error){
        //Cancel payment
        this.cancelPayment()
      }
      else{
        //Confirm payment
        let paymentIntent = result["paymentIntent"]
        let paymentId=paymentIntent["id"]
        let params = {
          "refUser":this.userManager.currentUser.id,
          "paymentId":paymentId,
          "jsonStripe": JSON.stringify(result)
        }
        console.log(params)
        this.apiService.confirmPayment(params).subscribe((done)=>{
          //console.log("Done")
          if (done){
         	//continue workflow
          } 
        })
      }
    })
  }

Stripe will manage the required action and returns a result. If an error occured, the payment can't be done, otherwise everything is ok, you can confirm the payment to your backend and continue the process.