The 123Flip app is integrated with Stripe, a payment processor or gateway that allows your users or customers to safely and efficiently transfer funds from their credit cards or bank accounts in a variety of currencies.
This integration can provide extra revenue earning for developer. In the case of 123Flip app, users who wants to add personalized message and link needs to pay a small token known as sponsor fee.
Integrate Stripe in Android [Server side]
To integrate Stripe in android app, we will first begin with the server side and then go to android studio. On server side, we need to write the some server codes (which is actually quite short).
I use VS editor and test on Node.JS server on workstation before deploying somewhere in the cloud. I deploy the server codes to Heroku, which is a platform or service (PaaS) that enables developers to build, run, and operate applications entirely in the cloud. I choose Heroku because it offers a free plan.
Download and install the following free tools and sign up a free account with Heroku.
* Node.JS [link] * VS code (for editing) [link] * Creates Stripe account [link] * Heroku CLI [link] + Git [link]
Installation steps for Node.JS, VS code, and Stripe account
Step 1:
(i) In Build.gradle, add stripe, okhttp & gson library
(ii) In AndroidManifest.xml, add internet permission
(i) In Build.gradle, add : //Stripe dependency implementation 'com.stripe:stripe-android:18.0.0' // for network call implementation 'com.squareup.okhttp3:okhttp:4.4.0' implementation 'com.google.code.gson:gson:2.8.6'
(ii) In AndroidManifest.xml, add : <uses-permission android:name="android.permission.INTERNET"/>
Step 2:
Add server code in server.js (VS code)
const express = require("express"); const app = express(); // This is your test secret API key. const stripe = require("stripe")("sk_test_51JiugAJfHDlzUJLDt39KpbTq33H0983xeErpLas2IjWz5Y6ihVW8msoEiARxPOuO9yVEzLE9CL0qXqR8Cm3LSsyz007YQE4Gj2");app.use(express.static("public")); app.use(express.json()); const calculateOrderAmount = items => { // Replace this constant with a calculation of the order's amount // Calculate the order total on the server to prevent // people from directly manipulating the amount on the client //return items[0].amount; //return items[0]; return 199; }; app.post("/create-payment-intent", async (req, res) => { const { items } = req.body; // Create a PaymentIntent with the order amount and currency const paymentIntent = await stripe.paymentIntents.create({ amount: calculateOrderAmount(items), currency: "usd" }); res.send({ clientSecret: paymentIntent.client_secret }); }); app.get("/greet", async (req, res) => { res.send('It is working!'); }); //app.listen(4242, () => console.log('Node server listening on port 4242!')); const PORT = process.env.PORT || 5001; app.listen(PORT, () => console.log('Node server listening on port ${PORT}'));
Step 3:
(i) Test and run server.js in Node.js
(ii) Deploy server.js to Heroku
Step 4:
Design your payment UI in activity_checkout.xml with Stripe CardInputWidget
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/clCheckout" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/white"> <LinearLayout android:id="@+id/llFee" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:orientation="vertical" app:layout_constraintBottom_toTopOf="@id/CardMultilineWidget" app:layout_constraintTop_toBottomOf="@id/llGameLogo" tools:layout_editor_absoluteX="0dp"> <androidx.cardview.widget.CardView android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_gravity="clip_horizontal|center_horizontal" app:cardElevation="0dp"> <TextView android:id="@+id/tvFee" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@color/white" android:text="US$1.99" android:textAppearance="@style/TextAppearance.AppCompat.Medium" android:textColor="@color/black" /> </androidx.cardview.widget.CardView> </LinearLayout> <LinearLayout android:id="@+id/llGameLogo" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="8dp" android:orientation="horizontal" app:layout_constraintTop_toBottomOf="@+id/llCreditCard"> <androidx.cardview.widget.CardView android:id="@+id/cvGameDesc" android:layout_width="wrap_content" android:layout_height="wrap_content"> <de.hdodenhof.circleimageview.CircleImageView android:id="@+id/ivGamePicture" android:layout_width="92dp" android:layout_height="86dp" tools:srcCompat="@tools:sample/avatars" /> </androidx.cardview.widget.CardView> <androidx.cardview.widget.CardView android:id="@+id/cvGameLogo" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center"> <TextView android:id="@+id/payment_amount" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginTop="15dp" android:layout_marginEnd="24dp" android:fontFamily="sans-serif" android:gravity="center_horizontal" android:text="Game name" android:textAppearance="@style/TextAppearance.AppCompat.Medium" android:textColor="@android:color/holo_blue_bright" /> <TextView android:id="@+id/info_sponsorMsg" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal|center_vertical" android:layout_marginTop="24dp" android:fontFamily="sans-serif" android:text="@string/info_sponsorMsg" android:textColor="@android:color/darker_gray" android:textSize="12sp" /> </androidx.cardview.widget.CardView> </LinearLayout> <LinearLayout android:id="@+id/llTopBar" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#2196F3" android:orientation="horizontal" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> <androidx.cardview.widget.CardView android:id="@+id/cvLogo" android:layout_width="56dp" android:layout_height="wrap_content" android:layout_margin="10dp" android:layout_weight="0.1" app:cardBackgroundColor="@android:color/transparent" app:cardElevation="0dp"> <de.hdodenhof.circleimageview.CircleImageView android:id="@+id/profile_image" android:layout_width="60dp" android:layout_height="53dp" android:layout_marginEnd="40dp" android:background="#2196F3" android:foregroundGravity="center" app:layout_constraintEnd_toEndOf="parent" tools:layout_editor_absoluteY="0dp" tools:srcCompat="@tools:sample/avatars" /> </androidx.cardview.widget.CardView> <androidx.cardview.widget.CardView android:id="@+id/cvLogoAbout" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_margin="8dp" android:layout_weight="2" app:cardBackgroundColor="@android:color/holo_green_light" app:cardCornerRadius="15dp"> <TextView android:id="@+id/tvStripeLink" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|center" android:layout_marginLeft="50dp" android:autoLink="web" android:text="www.stripe.com" android:textColor="@color/black" android:textColorLink="@color/black" /> <TextView android:id="@+id/tvSponsor_Message" android:layout_width="197dp" android:layout_height="38dp" android:layout_gravity="right" android:layout_margin="5dp" android:text="@string/checkout_aboutstripe" android:textAlignment="center" android:textAppearance="@style/TextAppearance.AppCompat.Small" android:textColor="@color/black" android:textColorLink="@color/black" android:textStyle="bold|italic" /> <ImageView android:id="@+id/ivLogo" android:layout_width="70dp" android:layout_height="36dp" app:srcCompat="@drawable/ic_stripe_blurple" /> </androidx.cardview.widget.CardView> </LinearLayout> <LinearLayout android:id="@+id/llCreditCard" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="2dp" android:gravity="center" android:orientation="horizontal" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/llTopBar"> <androidx.cardview.widget.CardView android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="1" /> <ImageView android:id="@+id/iv_visa" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:padding="1dp" android:scaleType="fitStart" android:src="@drawable/ic_visa" /> <ImageView android:id="@+id/iv_mastercard" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:padding="1dp" android:scaleType="fitStart" android:src="@drawable/ic_mastercard" /> <ImageView android:id="@+id/iv_amex" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:padding="1dp" android:scaleType="fitStart" android:src="@drawable/ic_amex" /> <ImageView android:id="@+id/iv_discovery" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:padding="1sp" android:scaleType="fitStart" android:src="@drawable/ic_discover" /> <ImageView android:id="@+id/iv_jcb" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:padding="1dp" android:scaleType="fitStart" android:src="@drawable/ic_jcb" /> <ImageView android:id="@+id/iv_union" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:padding="1dp" android:scaleType="fitStart" android:src="@drawable/ic_unionpay" /> </LinearLayout> <com.stripe.android.view.CardMultilineWidget android:id="@+id/CardMultilineWidget" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:background="@color/teal_200" android:gravity="center_horizontal|top" android:theme="@style/CardTheme" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/llFee"> </com.stripe.android.view.CardMultilineWidget> <Button android:id="@+id/payButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:text="@string/btn_checkout_pay" app:layout_constraintEnd_toEndOf="@+id/CardMultilineWidget" app:layout_constraintStart_toStartOf="@+id/CardMultilineWidget" app:layout_constraintTop_toBottomOf="@+id/CardMultilineWidget" /> </androidx.constraintlayout.widget.ConstraintLayout>
Step 5:
In CheckoutActivity.kt (when payButton is clicked):
(i) startCheckout() is called to get paymentIntentClientSecret key
(ii) Transaction result is returned in onPaymentResult()
(iii) Payment is completed
class CheckoutActivity : AppCompatActivity() { private val CHECKOUTACITIVTY_REQUEST_CODE = 321 // 10.0.2.2 is the Android emulator's alias to localhost //private val backendUrl = "http://10.0.2.2:4242/" private val backendUrl = "https://diok.herokuapp.com/" private val httpClient = OkHttpClient() private lateinit var paymentIntentClientSecret: String private lateinit var paymentLauncher: PaymentLauncher lateinit var body : RequestBodyvar checkboxTcTicked = false private lateinit var mAuth: FirebaseAuth var personName: String = "" var personGivenName: String = "" var personFamilyName: String = "" var personEmail: String = "" var personId: String = "" private var personPhoto: Uri? = null private var sponsorImageUri: Uri? = null private lateinit var gameName : String companion object { private const val TAG = "CheckoutActivity" } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_checkout) supportActionBar?.setDisplayShowHomeEnabled(true) supportActionBar?.setLogo(R.drawable.ic_lock) supportActionBar?.setDisplayUseLogoEnabled(true) val sharedPreferences = getSharedPreferences("sharedPrefs", Context.MODE_PRIVATE) val gameNamePref = sharedPreferences.getString("PICKED_KEY", null) val preferSignIn: String? = sharedPreferences.getString("SIGN_IN", null) val preferImage: String? = sharedPreferences.getString("IMAGE_KEY", null) sponsorImageUri = preferImage?.toUri()!! gameName = gameNamePref.toString() supportActionBar?.title = getString(R.string.actbar_checkout_annualfee) val storageRef = FirebaseStorage.getInstance().reference.child("games/$gameNamePref/images") if (preferSignIn == "google") { val acct = GoogleSignIn.getLastSignedInAccount(this) if (acct != null) { personName = acct.displayName.toString() personGivenName = acct.givenName.toString() personFamilyName = acct.familyName.toString() personEmail = acct.email.toString() personId = acct.id.toString() personPhoto = acct.photoUrl!! saveSponsorEmailToPreference(personEmail) Log.i("TAG", "CheckoutActivity >> google personName: $personName $personEmail " + "$personGivenName photo=$personPhoto") } } if (!isSignedIn()){ val signInIntent = Intent(this,SignInActivity::class.java) startActivity(signInIntent) finish() } val gameName = gameNamePref // Configure the SDK with your Stripe publishable key so it can make requests to Stripe val paymentConfiguration = PaymentConfiguration.getInstance(applicationContext) paymentLauncher = PaymentLauncher.Companion.create( this, paymentConfiguration.publishableKey, paymentConfiguration.stripeAccountId, ::onPaymentResult ) if (isSignedIn()){ Log.i(TAG, "Checkout Activity >> isSignedIn()") startCheckout() Log.i(TAG, "Checkout Activity >> finish startCheckout()") } } override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.menu_home, menu) return true } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.mi_home -> { val intent = Intent( this, MainActivity::class.java) startActivityForResult(intent, 68) return true } } return super.onOptionsItemSelected(item) } private fun isSignedIn(): Boolean { return GoogleSignIn.getLastSignedInAccount(this) != null } private fun saveSponsorEmailToPreference(email: String) { val sharedPreferences = getSharedPreferences("sharedPrefs", Context.MODE_PRIVATE) val editor = sharedPreferences.edit() editor.apply { putString("SPONSOR_EMAIL", email) }.apply() Toast.makeText( this, "Picked name to SharedPreference Saved", Toast.LENGTH_SHORT).cancel() } private fun displayAlert( activity: Activity, title: String, message: String, restartDemo: Boolean = false ) { runOnUiThread { val builder = AlertDialog.Builder(activity) .setTitle(title) .setMessage(message) builder.setPositiveButton("Ok", null) builder.create().show() } } private fun startCheckout() { val weakActivity = WeakReference<Activity>(this) // Create a PaymentIntent by calling your server's endpoint. val mediaType = "application/json; charset=utf-8".toMediaType() Log.i("TAG", "before requestJson") val requestJson = """ { "currency":"usd", "items": [ {"gamename":"$gameName"} ] } """ Log.i("TAG", "requestjson = $requestJson") /*val amount: Double = 123.0 val payMap : HashMap<String, Any> = HashMap<String, Any> () val itemMap : HashMap<String, Any> = HashMap<String, Any> () val itemList: List<Map<String, Any>> = ArrayList() payMap["currency"] = "usd" itemMap["id"] = "game_subscription" itemMap["amount"] = amount itemList.toMutableList().add(itemMap) payMap["items"] = itemList val json = Gson().toJson(payMap)*/ body = requestJson.toRequestBody(mediaType) val request = Request.Builder() .url(backendUrl + "create-payment-intent") .post(body) .build() httpClient.newCall(request) .enqueue(object: Callback { override fun onFailure(call: Call, e: IOException) { weakActivity.get()?.let { activity -> displayAlert(activity, "Failed to load page", "Error: $e") } } override fun onResponse(call: Call, response: Response) { if (!response.isSuccessful) { weakActivity.get()?.let { activity -> displayAlert( activity, "Failed to load page", "Error: $response" ) } } else { val responseData = response.body?.string() val responseJson = responseData?.let { JSONObject(it) } ?: JSONObject() // For added security, our sample app gets the publishable key // from the server. paymentIntentClientSecret = responseJson.getString("clientSecret") } } }) Glide.with(this).load(personPhoto).into(profile_image) val tvPersonInfo: TextView = findViewById<TextView>(R.id.info_sponsorMsg) val tvAmount: TextView = findViewById<TextView>(R.id.payment_amount) Toast.makeText(baseContext, "person info=$personName", Toast.LENGTH_SHORT).cancel() tvPersonInfo.text = getString(R.string.info_sponsorMsg) tvAmount.text = "${gameName.trim()}" Glide.with(this).load(sponsorImageUri).into(ivGamePicture) // Hook up the pay button to the card widget and stripe instance val payButton: Button = findViewById(R.id.payButton) //if (isCheckboxTicked()) { payButton.setOnClickListener { val cardInputWidget = findViewById<CardMultilineWidget>(R.id.CardMultilineWidget) //cardInputWidget.postalCodeEnabled = false cardInputWidget.paymentMethodCreateParams?.let { params -> val confirmParams = ConfirmPaymentIntentParams .createWithPaymentMethodCreateParams(params, paymentIntentClientSecret) paymentLauncher.confirm(confirmParams) } } //} } private fun SponsorDialog() { val sponsorView = LayoutInflater.from( this).inflate(R.layout.dialog_sponsor_message, null) showAlertDialog(getString(R.string.dlog_checkout_title), sponsorView, View.OnClickListener { }) } private fun showAlertDialog( title:String, view: View?, positiveClickListener: View.OnClickListener) { AlertDialog.Builder( this) .setTitle(title) .setCancelable(false) .setView(view) .setNegativeButton( getString(R.string.dlog_cancel), null) .setPositiveButton("OK") {_, _ -> positiveClickListener.onClick( null) //***********save sponsor to firebase val etSponsorMessage = view?.findViewById<EditText>(R.id.etSponsorMessage) val etSponsorLink = view?.findViewById<EditText>(R.id.etSponsorLink) updateFirebaseSponsor(gameName, personEmail, etSponsorMessage?.text.toString(), etSponsorLink?.text.toString()) val intent = Intent(this, GamelistActivity::class.java) intent.putExtra(EXTRA_CMD, "my sponsor") startActivityForResult(intent, CHECKOUTACITIVTY_REQUEST_CODE) }.show() } private fun updateFirebaseSponsor( gn: String, email: String, msg: String, link: String ) { val db = FirebaseFirestore.getInstance() val noteRef = db.collection("games") .document(gn) noteRef.update( "sponsor", email, "sponsormessage", msg, "sponsorlink", link, "sponsordate", Timestamp.now() ).addOnCompleteListener { task -> if (task.isSuccessful) { Toast.makeText( this, "Updated sponsor $personEmail", Toast.LENGTH_SHORT).cancel() } else { Toast.makeText( this, "Failed update sponsor. Check Log", Toast.LENGTH_SHORT).show() } } } private fun onPaymentResult(paymentResult: PaymentResult) { var title = "" var message = "" var restartDemo = false when (paymentResult) { is PaymentResult.Completed -> { SponsorDialog() title = "Setup Completed" restartDemo = true Log.i("TAG", "body = $body") } is PaymentResult.Canceled -> { title = "Setup Canceled" displayAlert(this, title, message, restartDemo) } is PaymentResult.Failed -> { title = "Setup Failed" message = paymentResult.throwable.message!! displayAlert(this, title, message, restartDemo) } } } }