Ad

How To Integrate Stripe In Android Using Firebase

- 1 answer

User functionality required example - User makes payment of $100 + first time fee. 6 months later another payment of $100 is automatically charged. With payments stopping after n number of total payments or until user manually stops.

Following this tutorial - https://stripe.com/docs/mobile/android

Since I'm using firebase firestore for my backend I have created a token and charge by writing to the database, since I couldn't find any tutorial outlining a proper method that uses this combination. Is this how it should be done or is there a better way?

I have set up in my payment activity:

stripe = new Stripe(getApplicationContext(), "PUBLISHABLE_KEY_HIDDEN");

        confirmButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                cardToSave = mCardInputWidget.getCard();

                // Add details such as full name and address to card
                fillCardFields();

                if (!cardToSave.validateCard()) {
                    Toast.makeText(getApplicationContext(), "Card is invalid", Toast.LENGTH_LONG).show();
                    return;
                }
                stripe.createToken(
                        cardToSave,
                        new TokenCallback() {
                            public void onSuccess(Token token) {
                                // Send token to your server
                                DocumentReference tokenRef;
                                tokenRef = db.collection("stripe_customers").document(mAuth.getUid()).collection("tokens").document();

                                tokenRef.set(token).addOnSuccessListener(new OnSuccessListener<Void>() {
                                    @Override
                                    public void onSuccess(Void aVoid) {
                                        Toast.makeText(getApplicationContext(), "Token successfully added to database", Toast.LENGTH_LONG).show();
                                    }
                                }).addOnFailureListener(new OnFailureListener() {
                                    @Override
                                    public void onFailure(@NonNull Exception e) {
                                        Toast.makeText(getApplicationContext(), e.getLocalizedMessage(), Toast.LENGTH_LONG).show();
                                    }
                                });

                                DocumentReference chargeRef;
                                chargeRef = db.collection("stripe_customers").document(mAuth.getUid()).collection("charges").document();
                                Map<String, Object> amount = new HashMap<>();

                                //Test amount of 5 - to be replaced with a variable
                                amount.put("amount", 5);
                                chargeRef.set(amount);
                                chargeRef.update("amount", 5).addOnFailureListener(new OnFailureListener() {
                                    @Override
                                    public void onFailure(@NonNull Exception e) {
                                        Toast.makeText(getApplicationContext(), e.getLocalizedMessage(), Toast.LENGTH_LONG).show();
                                        Log.e("chargeRef", e.getLocalizedMessage());
                                    }
                                });
                            }
                            public void onError(Exception error) {
                                // Show localized error message
                                Toast.makeText(getApplicationContext(),
                                        error.getLocalizedMessage(),
                                        Toast.LENGTH_LONG
                                ).show();
                            }
                        }
                );
            }
        });

Using this tutorial for firebase server functions using firestore - https://github.com/firebase/functions-samples/tree/Node-8/stripe

I copied the files from github into my project (is that how it's done?)

When I run my app and press the confirmButton a token is created firestore "stripe_customers/uid/tokens" an error field is generated and the error is "Missing required param: source." and cvc is null and cvc check is unchecked. What can I do to fix this/make payments and subscriptions work.

When I open https://firebase-id-hidden.firebaseapp.com/ it shows fields such as the following under "Credit Cards":

{{ source.brand }} …{{ source.last4 }} (exp. {{ source.exp_month }}/{{ source.exp_year }}) …

/public/index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Cloud Functions for Firebase (Stripe example)</title>
    <script src="https://js.stripe.com/v2/"></script>
    <script src="https://unpkg.com/vue/dist/vue.js"></script>
    <link rel="stylesheet" target="_blank" rel="nofollow noreferrer" href="https://cdn.firebase.com/libs/firebaseui/3.5.2/firebaseui.css"/>
</head>
<body>
<div class="container">
    <div id="app">
        <div id="firebaseui-auth-container"></div>
        <div id="loader">&hellip;</div>
        <div v-if="currentUser">
            <h2>Hello {{ currentUser.email }},</h2>
            <button v-on:click="signOut">Sign out</button>
            <div v-if="stripeCustomerInitialized">
                <h3>Credit Cards</h3>
                <ul>
                    <li v-for="source in sources">
                <span v-if="source.id">
                  {{ source.brand }} &hellip;{{ source.last4 }}
                  (exp. {{ source.exp_month }}/{{ source.exp_year }})
                </span>
                        <span v-else>&hellip;</span>
                    </li>
                </ul>
                <div>
                    <h4>New</h4>
                    <div>
                        <label>
                            Number <input v-model="newCreditCard.number">
                        </label>
                    </div>
                    <div>
                        <label>
                            CCV <input v-model="newCreditCard.cvc">
                        </label>
                    </div>
                    <div>
                        <label>
                            Exp
                            <input v-model="newCreditCard.exp_month" size="2"> /
                            <input v-model="newCreditCard.exp_year" size="4">
                        </label>
                    </div>
                    <div>
                        <label>
                            Zip <input v-model="newCreditCard.address_zip">
                        </label>
                    </div>
                    <div>
                        <button v-on:click="submitNewCreditCard">Add</button>
                        {{ newCreditCard.error }}
                    </div>
                </div>
                <h3>Charges</h3>
                <ul>
                    <li v-for="(charge, id) in charges">
                        {{ charge.amount }}
                        <span v-if="charge.error">
                  {{ charge.error }}
                </span>
                        <span v-else-if="charge.outcome">
                  {{ charge.outcome.seller_message }}
                  {{ charge.source.brand }} &hellip;{{ charge.source.last4 }}
                  (exp. {{ charge.source.exp_month }}/{{ charge.source.exp_year }})
                </span>
                        <span v-else>&hellip;</span>
                    </li>
                </ul>
                <h4>New</h4>
                <div>
                    <label>
                        Card
                        <select v-model="newCharge.source">
                            <option :value="null">Default payment method</option>
                            <option v-for="(source, id) in sources" v-bind:value="source.id"
                                    v-if="source.id">
                                {{ source.brand }} &hellip;{{ source.last4 }}
                                (exp. {{ source.exp_month }}/{{ source.exp_year }})
                            </option>
                        </select>
                    </label>
                </div>
                <div>
                    <label>
                        Amount <input v-model="newCharge.amount">
                    </label>
                </div>
                <div>
                    <button v-on:click="submitNewCharge">Charge</button>
                    {{ newCharge.error }}
                </div>
            </div>
            <div v-else>&hellip;</div>
        </div>
    </div>
</div>

<!-- Import and configure the Firebase SDK -->
<!-- These scripts are made available when the app is served or deployed on Firebase Hosting -->
<!-- If you do not serve/host your project using Firebase Hosting see https://firebase.google.com/docs/web/setup -->
<script src="/__/firebase/5.9.1/firebase-app.js"></script>
<script src="/__/firebase/5.9.1/firebase-auth.js"></script>
<script src="/__/firebase/5.9.1/firebase-firestore.js"></script>
<script src="/__/firebase/init.js"></script>

<!-- Import Firebase UI -->
<script src="https://cdn.firebase.com/libs/firebaseui/3.5.2/firebaseui.js"></script>

<script>
      firebase.initializeApp({
        apiKey: "HIDDEN",
        authDomain: "HIDDEN",
        databaseURL: "HIDDEN",
        storageBucket: "HIDDEN",
        messagingSenderId: "HIDDEN"
      });
      Stripe.setPublishableKey('HIDDEN');
      var firebaseUI = new firebaseui.auth.AuthUI(firebase.auth());
      var firebaseAuthOptions = {
        callbacks: {
          signInSuccess: (currentUser, credential, redirectUrl) => { return false; },
          uiShown: () => { document.getElementById('loader').style.display = 'none'; }
        },
        signInFlow: 'popup',
        signInSuccessUrl: '/',
        signInOptions: [ firebase.auth.GoogleAuthProvider.PROVIDER_ID ],
        tosUrl: '/'
      };
      firebase.auth().onAuthStateChanged(firebaseUser => {
        if (firebaseUser) {
          document.getElementById('loader').style.display = 'none';
          app.currentUser = firebaseUser;
          app.listen();
        } else {
          firebaseUI.start('#firebaseui-auth-container', firebaseAuthOptions);
          app.currentUser = null;
        }
      });
      var app = new Vue({
        el: '#app',
        data: {
          currentUser: null,
          sources: {},
          stripeCustomerInitialized: false,
          newCreditCard: {
            number: '4242424242424242',
            cvc: '111',
            exp_month: 1,
            exp_year: 2020,
            address_zip: '00000'
          },
          charges: {},
          newCharge: {
            source: null,
            amount: 2000
          }
        },
        ready: () => {
        },
        methods: {
          listen: function() {
            firebase.firestore().collection('stripe_customers').doc(`${this.currentUser.uid}`).onSnapshot(snapshot => {
              this.stripeCustomerInitialized = (snapshot.data() !== null);
            }, () => {
              this.stripeCustomerInitialized = false;
            });
            firebase.firestore().collection('stripe_customers').doc(`${this.currentUser.uid}`).collection('sources').onSnapshot(snapshot => {
             let newSources = {};
              snapshot.forEach(doc => {
                const id = doc.id;
                newSources[id] = doc.data();
              })
              this.sources = newSources;
            }, () => {
              this.sources = {};
            });
            firebase.firestore().collection('stripe_customers').doc(`${this.currentUser.uid}`).collection('charges').onSnapshot(snapshot => {
            let newCharges = {};
             snapshot.forEach(doc => {
               const id = doc.id;
               newCharges[id] = doc.data();
             })
             this.charges = newCharges;
            }, () => {
              this.charges = {};
            });
          },
          submitNewCreditCard: function() {
            Stripe.card.createToken({
              number: this.newCreditCard.number,
              cvc: this.newCreditCard.cvc,
              exp_month: this.newCreditCard.exp_month,
              exp_year: this.newCreditCard.exp_year,
              address_zip: this.newCreditCard.address_zip
            }, (status, response) => {
              if (response.error) {
                this.newCreditCard.error = response.error.message;
              } else {
                firebase.firestore().collection('stripe_customers').doc(this.currentUser.uid).collection('tokens').add({token: response.id}).then(() => {
                  this.newCreditCard = {
                    number: '',
                    cvc: '',
                    exp_month: 1,
                    exp_year: 2017,
                    address_zip: ''
                  };
                });
              }
            });
          },
          submitNewCharge: function() {
            firebase.firestore().collection('stripe_customers').doc(this.currentUser.uid).collection('charges').add({
              source: this.newCharge.source,
              amount: parseInt(this.newCharge.amount)
            });
          },
          signOut: function() {
            firebase.auth().signOut()
          }
        }
      });


</script>
</body>
</html>

/functions/index.js

/**
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
'use strict';

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
//Causes error during deploy
//const logging = require('@google-cloud/logging')();
const stripe = require('stripe')(functions.config().stripe.token);
const currency = functions.config().stripe.currency || 'AUD';

// [START chargecustomer]
// Charge the Stripe customer whenever an amount is written to the Realtime database
exports.createStripeCharge = functions.firestore.document('stripe_customers/{userId}/charges/{id}').onCreate(async (snap, context) => {
      const val = snap.data();
      try {
        // Look up the Stripe customer id written in createStripeCustomer
        const snapshot = await admin.firestore().collection(`stripe_customers`).doc(context.params.userId).get()
        const snapval = snapshot.data();
        const customer = snapval.customer_id
        // Create a charge using the pushId as the idempotency key
        // protecting against double charges
        const amount = val.amount;
        const idempotencyKey = context.params.id;
        const charge = {amount, currency, customer};
        if (val.source !== null) {
          charge.source = val.source;
        }
        const response = await stripe.charges.create(charge, {idempotency_key: idempotencyKey});
        // If the result is successful, write it back to the database
        return snap.ref.set(response, { merge: true });
      } catch(error) {
        // We want to capture errors and render them in a user-friendly way, while
        // still logging an exception with StackDriver
        console.log(error);
        await snap.ref.set({error: userFacingMessage(error)}, { merge: true });
        return reportError(error, {user: context.params.userId});
      }
    });
// [END chargecustomer]]

// When a user is created, register them with Stripe
exports.createStripeCustomer = functions.auth.user().onCreate(async (user) => {
  const customer = await stripe.customers.create({email: user.email});
  return admin.firestore().collection('stripe_customers').doc(user.uid).set({customer_id: customer.id});
});

// Add a payment source (card) for a user by writing a stripe payment source token to Realtime database
exports.addPaymentSource = functions.firestore.document('/stripe_customers/{userId}/tokens/{pushId}').onCreate(async (snap, context) => {
  const source = snap.data();
  const token = source.token;
  if (source === null){
    return null;
  }

  try {
    const snapshot = await admin.firestore().collection('stripe_customers').doc(context.params.userId).get();
    const customer =  snapshot.data().customer_id;
    const response = await stripe.customers.createSource(customer, {source: token});
    return admin.firestore().collection('stripe_customers').doc(context.params.userId).collection("sources").doc(response.fingerprint).set(response, {merge: true});
  } catch (error) {
    await snap.ref.set({'error':userFacingMessage(error)},{merge:true});
    return reportError(error, {user: context.params.userId});
  }
});

// When a user deletes their account, clean up after them
exports.cleanupUser = functions.auth.user().onDelete(async (user) => {
  const snapshot = await admin.firestore().collection('stripe_customers').doc(user.uid).get();
  const customer = snapshot.data();
  await stripe.customers.del(customer.customer_id);
  return admin.firestore().collection('stripe_customers').doc(user.uid).delete();
});

// To keep on top of errors, we should raise a verbose error report with Stackdriver rather
// than simply relying on console.error. This will calculate users affected + send you email
// alerts, if you've opted into receiving them.
// [START reporterror]
function reportError(err, context = {}) {
  // This is the name of the StackDriver log stream that will receive the log
  // entry. This name can be any valid log stream name, but must contain "err"
  // in order for the error to be picked up by StackDriver Error Reporting.
  const logName = 'errors';
  const log = logging.log(logName);

  // https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/MonitoredResource
  const metadata = {
    resource: {
      type: 'cloud_function',
      labels: {function_name: process.env.FUNCTION_NAME},
    },
  };

  // https://cloud.google.com/error-reporting/reference/rest/v1beta1/ErrorEvent
  const errorEvent = {
    message: err.stack,
    serviceContext: {
      service: process.env.FUNCTION_NAME,
      resourceType: 'cloud_function',
    },
    context: context,
  };

  // Write the error log entry
  return new Promise((resolve, reject) => {
    log.write(log.entry(metadata, errorEvent), (error) => {
      if (error) {
       return reject(error);
      }
      return resolve();
    });
  });
}
// [END reporterror]

// Sanitize the error message for the user
function userFacingMessage(error) {
  return error.type ? error.message : 'An error occurred, developers have been alerted';
}

/functions/package.json

{
  "name": "stripe-functions",
  "description": "Stripe Firebase Functions",
  "dependencies": {
    "@google-cloud/logging": "^4.5.1",
    "firebase-admin": "~7.2.0",
    "firebase-functions": "^2.2.1",
    "stripe": "^6.28.0"
  },
  "devDependencies": {
    "eslint": "^5.6.1",
    "eslint-plugin-promise": "^4.1.1"
  },
  "scripts": {
    "lint": "./node_modules/.bin/eslint --max-warnings=0 .",
    "serve": "firebase serve --only functions",
    "shell": "firebase experimental:functions:shell",
    "start": "npm run shell",
    "deploy": "firebase deploy --only functions",
    "logs": "firebase functions:log"
  },
  "engines": {
    "node": "8"
  },
  "private": true
}
Ad

Answer

The error was caused by a outdated variable name in /functions/index.js

For the addPaymentSouce function in index.js, replace the line: const token = source.token; with const token = source.id;

Ad
source: stackoverflow.com
Ad