0

Am trying to add Stripe to my android app with Firebase. I set the publishable key in gradle.properties and builtype in gradle.build then call in application as BuildConfig.PublishableKey

Every time I try to add card a dialog pops up with this warning...

unexpected char 0x0a at 9 header value: bearer pk_test_xxxxkeyxxxx

Any ideas? Am using prebuilt UI from stripe too (presentPaymentMethodSelection() ) following their Firebase mobile payments android


var RC_SIGN_IN = 1
class MainActivityStripe : AppCompatActivity() {
    private var currentUser: FirebaseUser? = null
    private lateinit var paymentSession: PaymentSession
    private lateinit var selectedPaymentMethod: PaymentMethod
    private val stripe: Stripe by lazy { Stripe(applicationContext,
        PaymentConfiguration.getInstance(applicationContext).publishableKey) }


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main_stripe)
        currentUser = FirebaseAuth.getInstance().currentUser
        payButton.isEnabled = false

        loginButton.setOnClickListener {
            // login to firebase
            val providers = arrayListOf(
                AuthUI.IdpConfig.EmailBuilder().build())

            startActivityForResult(
                AuthUI.getInstance()
                    .createSignInIntentBuilder()
                    .setAvailableProviders(providers)
                    .build(),
                RC_SIGN_IN
            )
        }

        payButton.setOnClickListener {
            confirmPayment(selectedPaymentMethod.id!!)
        }

        paymentmethod.setOnClickListener {
            Toast.makeText(applicationContext, PaymentConfiguration.getInstance(applicationContext).publishableKey, Toast.LENGTH_LONG).show()
            // Create the customer session and kick start the payment flow
            paymentSession.presentPaymentMethodSelection()
        }

        showUI()
    }

    private fun confirmPayment(paymentMethodId: String) {

        val paymentCollection = Firebase.firestore
            .collection("stripe_customers").document(currentUser?.uid?:"")
            .collection("payments")

        // Add a new document with a generated ID
        paymentCollection.add(hashMapOf(
            "amount" to 8800,
            "currency" to "cad"
        ))
            .addOnSuccessListener { documentReference ->
                Log.d("paymentsss", "DocumentSnapshot added with ID: ${documentReference.id}")
                documentReference.addSnapshotListener { snapshot, e ->
                    if (e != null) {
                        Log.w("paymentsss", "Listen failed.", e)
                        return@addSnapshotListener
                    }

                    if (snapshot != null && snapshot.exists()) {
                        Log.d("paymentsss", "Current data: ${snapshot.data}")
                        val clientSecret = snapshot.data?.get("client_secret")
                        Log.d("paymentsss", "Create paymentIntent returns $clientSecret")
                        clientSecret?.let {
                            stripe.confirmPayment(this, ConfirmPaymentIntentParams.createWithPaymentMethodId(
                                paymentMethodId,
                                (it as String)
                            ))

                            checkoutSummary.text = "Thank you for your payment"
                            Toast.makeText(applicationContext, "Payment Done!!", Toast.LENGTH_LONG).show()
                        }
                    } else {
                        Log.e("paymentsss", "Current payment intent : null")
                        payButton.isEnabled = true
                    }
                }
            }
            .addOnFailureListener { e ->
                Log.w("paymentsss", "Error adding document", e)
                payButton.isEnabled = true
            }
    }

    private fun showUI() {
        currentUser?.let {
            loginButton.visibility = View.INVISIBLE

            greeting.visibility = View.VISIBLE
            checkoutSummary.visibility = View.VISIBLE
            payButton.visibility = View.VISIBLE
            paymentmethod.visibility = View.VISIBLE

            greeting.text = "Hello ${it.displayName}"

            setupPaymentSession()
        }?: run {
            // User does not login
            loginButton.visibility = View.VISIBLE

            greeting.visibility = View.INVISIBLE
            checkoutSummary.visibility = View.INVISIBLE
            paymentmethod.visibility = View.INVISIBLE
            payButton.visibility = View.INVISIBLE
            payButton.isEnabled = false

        }
    }

    private fun setupPaymentSession () {
        // Setup Customer Session
        CustomerSession.initCustomerSession(this, FirebaseEphemeralKeyProvider())
        // Setup a payment session
        paymentSession = PaymentSession(this, PaymentSessionConfig.Builder()
            .setShippingInfoRequired(false)
            .setShippingMethodsRequired(false)
            .setBillingAddressFields(BillingAddressFields.None)
            .setShouldShowGooglePay(true)
            .build())

        paymentSession.init(
            object: PaymentSession.PaymentSessionListener {
                override fun onPaymentSessionDataChanged(data: PaymentSessionData) {
                    Log.d("PaymentSession1", "11PaymentSession has changed: $data")
                    Log.d("PaymentSession11", "1111 ${data.isPaymentReadyToCharge} <> ${data.paymentMethod}")

                    if (data.isPaymentReadyToCharge) {
                        Log.d("PaymentSession2", "222Ready to charge");
                        payButton.isEnabled = true

                        data.paymentMethod?.let {
                            Log.d("PaymentSession3", "333PaymentMethod $it selected")
                            paymentmethod.text = "${it.card?.brand} card ends with ${it.card?.last4}"
                            selectedPaymentMethod = it
                        }
                    }
                }

                override fun onCommunicatingStateChanged(isCommunicating: Boolean) {
                    Log.d("PaymentSession4",  "444isCommunicating $isCommunicating")
                }

                override fun onError(errorCode: Int, errorMessage: String) {
                    Log.e("PaymentSession5",  "555onError: $errorCode, $errorMessage")
                }
            }
        )

    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        if (requestCode == RC_SIGN_IN) {
            val response = IdpResponse.fromResultIntent(data)

            if (resultCode == Activity.RESULT_OK) {
                currentUser = FirebaseAuth.getInstance().currentUser

                Log.d("Login", "User ${currentUser?.displayName} has signed in.")
                Toast.makeText(applicationContext, "Welcome ${currentUser?.displayName}", Toast.LENGTH_SHORT).show()
                showUI()
            } else {
                Log.d("Login", "Signing in failed!")
                Toast.makeText(applicationContext, response?.error?.message?:"Sign in failed", Toast.LENGTH_LONG).show()
            }
        } else {
            paymentSession.handlePaymentData(requestCode, resultCode, data ?: Intent())
        }
    }

}**
'use strict';
  const functions = require('firebase-functions');
  const admin = require('firebase-admin');

  const { Logging } = require('@google-cloud/logging');
  const logging = new Logging({
    projectId: process.env.GCLOUD_PROJECT,
  });

  admin.initializeApp();

  const stripe = require('stripe')(functions.config().stripe.secret, {
     apiVersion: '2020-03-02',
   });

  exports.createEphemeralKey = functions.https.onCall(async (data, context) => {
      // Checking that the user is authenticated.
      if (!context.auth) {
        // Throwing an HttpsError so that the client gets the error details.
        throw new functions.https.HttpsError(
          'failed-precondition',
          'The function must be called while authenticated!'
        );
      }

      const uid = context.auth.uid;
      try {
            if (!uid) throw new Error('Not authenticated!');
                // Get stripe customer id
            const customer = (
              await admin.firestore().collection('stripe_customers').doc(uid).get()
            ).data().customer_id;
            const key = await stripe.ephemeralKeys.create(
              { customer: customer },
              { apiVersion: data.api_version }
            );
            return key;
      } catch (error) {
            throw new functions.https.HttpsError('internal', error.message);
      }
  });

  exports.createStripeCustomer = functions.auth.user().onCreate(async (user) => {
        const customer = await stripe.customers.create({
          email: user.email,
          metadata: { firebaseUID: user.uid },
        });

        await admin.firestore().collection('stripe_customers').doc(user.uid).set({
          customer_id: customer.id,
        });
        return;
  });

  exports.createStripePayment = functions.firestore
    .document('stripe_customers/{userId}/payments/{pushId}')
    .onCreate(async (snap, context) => {

    const { amount, currency } = snap.data();

    try {
      // Look up the Stripe customer id.
      const customer = (await snap.ref.parent.parent.get()).data().customer_id;

      // Create a charge using the pushId as the idempotency key
      // to protect against double charges.
      const idempotencyKey = context.params.pushId;
      const payment = await stripe.paymentIntents.create(
        {
          amount,
          currency,
          customer,
        },
        { idempotencyKey }
      );

      // If the result is successful, write it back to the database.
      await snap.ref.set(payment);
    } 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 });
      await reportError(error, { user: context.params.userId });
    }
  });

    const updatePaymentRecord = async (id) => {

          // Retrieve the payment object to make sure we have an up to date status.
          const payment = await stripe.paymentIntents.retrieve(id);
          const customerId = payment.customer;

          // Get customer's doc in Firestore.
          const customersSnap = await admin
            .firestore()
            .collection('stripe_customers')
            .where('customer_id', '==', customerId)
            .get();

          if (customersSnap.size !== 1) throw new Error('User not found!');

          // Update record in Firestore
          const paymentsSnap = await customersSnap.docs[0].ref
            .collection('payments')
            .where('id', '==', payment.id)
            .get();

          if (paymentsSnap.size !== 1) throw new Error('Payment not found!');

          await paymentsSnap.docs[0].ref.set(payment);
    };

    exports.cleanupUser = functions.auth.user().onDelete(async (user) => {
          const dbRef = admin.firestore().collection('stripe_customers');
          const customer = (await dbRef.doc(user.uid).get()).data();
          await stripe.customers.del(customer.customer_id);
          // Delete the customers payments & payment methods in firestore.
          const snapshot = await dbRef
            .doc(user.uid)
            .collection('payment_methods')
            .get();
          snapshot.forEach((snap) => snap.ref.delete());
          await dbRef.doc(user.uid).delete();
          return;
    });

    exports.handleWebhookEvents = functions.https.onRequest(async (req, resp) => {
      const relevantEvents = new Set([
        'payment_intent.succeeded',
        'payment_intent.processing',
        'payment_intent.payment_failed',
        'payment_intent.canceled',
      ]);

      let event;

      try {
        event = stripe.webhooks.constructEvent(
          req.rawBody,
          req.headers['stripe-signature'],
          functions.config().stripe.webhooksecret
        );
      } catch (error) {
        console.error('❗️ Webhook Error: Invalid Secret');
        resp.status(401).send('Webhook Error: Invalid Secret');
        return;
      }

      if (relevantEvents.has(event.type)) {
        try {
          switch (event.type) {
            case 'payment_intent.succeeded':
            case 'payment_intent.processing':
            case 'payment_intent.payment_failed':
            case 'payment_intent.canceled':{
              const id = event.data.object.id;
              await updatePaymentRecord(id);
              break;
            }
            default:
              throw new Error('Unhandled relevant event!');
          }
        } catch (error) {
          console.error(
            `❗️ Webhook error for [${event.data.object.id}]`,
            error.message
          );
          resp.status(400).send('Webhook handler failed. View Function logs.');
          return;
        }
      }

      // Return a response to Stripe to acknowledge receipt of the event.
      resp.json({ received: true });
    });

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';
}
buildTypes.each {
    it.buildConfigField 'String', 'PublishableKey', stripePublishableKey
}

import android.util.Log
import com.google.firebase.functions.FirebaseFunctionsException
import com.google.firebase.functions.ktx.functions
import com.google.firebase.ktx.Firebase
import com.stripe.android.EphemeralKeyProvider
import com.stripe.android.EphemeralKeyUpdateListener

class FirebaseEphemeralKeyProvider: EphemeralKeyProvider {

    override fun createEphemeralKey(
        apiVersion: String,
        keyUpdateListener: EphemeralKeyUpdateListener
    ) {
        val data = hashMapOf(
            "api_version" to apiVersion
        )

        // User firebase to call the functions
        Firebase.functions
            .getHttpsCallable("createEphemeralKey")
            .call(data)
            .continueWith { task ->
                if (!task.isSuccessful) {
                    val e = task.exception
                    if (e is FirebaseFunctionsException) {
                        val code = e.code
                        val message = e.message
                        Log.e("EphemeralKey", "Ephemeral key provider returns error: $e $code $message")
                    }
                }
                val key = task.result?.data.toString()
                Log.d("EphemeralKey", "Ephemeral key provider returns $key")
                keyUpdateListener.onKeyUpdate(key)
            }
    }

}

import android.app.Application
import com.stripe.android.PaymentConfiguration

class MyApp : Application(){
    override fun onCreate() {
        super.onCreate()
        PaymentConfiguration.init(applicationContext, BuildConfig.PublishableKey)
    }
}```
>2020-09-07 09:23:29.753 15967-15967/com.asgd.indigenoussource D/PaymentSession4: 444isCommunicating true
>2020-09-07 09:23:29.754 15967-15967/com.asgd.indigenoussource D/PaymentSession1: 11PaymentSession has changed: PaymentSessionData(isShippingInfoRequired=false, isShippingMethodRequired=false, cartTotal=0, shippingTotal=0, shippingInformation=null, shippingMethod=null, paymentMethod=null, useGooglePay=false)
>2020-09-07 09:23:29.754 15967-15967/com.asgd.indigenoussource D/PaymentSession11: 1111 false <> null
>2020-09-07 09:23:31.636 15967-15967/com.asgd.indigenoussource D/PaymentSession4: 444isCommunicating false
  • after bearer its shows my publishable key – Native Mandroid Sep 07 '20 at 07:44
  • This could be a lead, there may be a hidden newline in the header: https://github.com/square/retrofit/issues/1153 – v3nkman Sep 07 '20 at 08:55
  • just tried a few of those but still no luck – Native Mandroid Sep 07 '20 at 09:34
  • Just to double check, `trim()` did not result in a valid String? I think the best approach now would be to try to detect invalid chars in the string if that doesn't work. There are some other approaches here, albeit in javascript but it should provide some inspiration: https://stackoverflow.com/questions/11305797/remove-zero-width-space-characters-from-a-javascript-string. It may be that a newline or something invalid has made it's way into the gradle configuration. – v3nkman Sep 07 '20 at 11:55
  • tried that and regex and also hardcoding it instead. seems to happen as soon as I submit payment methot with stripes prebuilt presentPaymentMethodSelection() – Native Mandroid Sep 07 '20 at 12:59
  • am following the firebase mobile payments video from stripe. Everything else seems to be working until then – Native Mandroid Sep 07 '20 at 13:01

1 Answers1

0

After getting some help from someone with stripe was able to get it working. My publishable key some how got changed within gradle and that key gets linked with device. I had to make new emulated device and reset everything