├── .cli.json
├── .github
└── pull_request_template.md
├── .gitignore
├── CODEOWNERS
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── stripe
│ │ └── aod
│ │ └── sampleapp
│ │ ├── Config.kt
│ │ ├── MyApp.kt
│ │ ├── activity
│ │ └── MainActivity.kt
│ │ ├── data
│ │ ├── CreatePaymentParams.kt
│ │ ├── EmailReceiptParams.kt
│ │ └── PaymentIntentCreationResponse.kt
│ │ ├── fragment
│ │ ├── EmailFragment.kt
│ │ ├── HomeFragment.kt
│ │ ├── InputFragment.kt
│ │ └── ReceiptFragment.kt
│ │ ├── listener
│ │ └── TerminalEventListener.kt
│ │ ├── model
│ │ ├── CheckoutViewModel.kt
│ │ ├── ConnectionToken.kt
│ │ ├── FailureMessage.kt
│ │ ├── InputViewModel.kt
│ │ └── MainViewModel.kt
│ │ ├── network
│ │ ├── ApiClient.kt
│ │ ├── BackendService.kt
│ │ └── TokenProvider.kt
│ │ └── utils
│ │ └── ActivityExt.kt
│ └── res
│ ├── anim
│ ├── slide_left_in.xml
│ ├── slide_left_out.xml
│ ├── slide_right_in.xml
│ └── slide_right_out.xml
│ ├── color
│ ├── selector_btn_secondary_text.xml
│ ├── selector_btn_text.xml
│ └── selector_textinputlayout_border.xml
│ ├── drawable-night
│ ├── back.xml
│ ├── backspace.xml
│ ├── ic_launcher_background.xml
│ ├── keyboard_active.xml
│ ├── keyboard_inactive.xml
│ ├── logo.xml
│ └── selector_keyboard.xml
│ ├── drawable-v24
│ └── ic_launcher_foreground.xml
│ ├── drawable
│ ├── back.xml
│ ├── backspace.xml
│ ├── btn_default.xml
│ ├── btn_disable.xml
│ ├── btn_pressed.xml
│ ├── btn_secondary_default.xml
│ ├── btn_secondary_disable.xml
│ ├── btn_secondary_pressed.xml
│ ├── email_input.xml
│ ├── ic_launcher_background.xml
│ ├── keyboard_active.xml
│ ├── keyboard_inactive.xml
│ ├── logo.xml
│ ├── selector_btn.xml
│ ├── selector_keyboard.xml
│ ├── selector_secondary_btn.xml
│ └── settings.xml
│ ├── layout
│ ├── activity_main.xml
│ ├── fragment_email.xml
│ ├── fragment_home.xml
│ ├── fragment_input.xml
│ ├── fragment_receipt.xml
│ └── widget_keyboard.xml
│ ├── mipmap-anydpi-v26
│ └── ic_launcher.xml
│ ├── mipmap-anydpi-v33
│ └── ic_launcher.xml
│ ├── mipmap-hdpi
│ └── ic_launcher.webp
│ ├── mipmap-mdpi
│ └── ic_launcher.webp
│ ├── mipmap-xhdpi
│ └── ic_launcher.webp
│ ├── mipmap-xxhdpi
│ └── ic_launcher.webp
│ ├── mipmap-xxxhdpi
│ └── ic_launcher.webp
│ ├── navigation
│ └── nav_graph.xml
│ ├── values-night
│ ├── colors.xml
│ └── themes.xml
│ ├── values
│ ├── colors.xml
│ ├── dimens.xml
│ ├── strings.xml
│ └── themes.xml
│ └── xml
│ ├── backup_rules.xml
│ └── data_extraction_rules.xml
├── build.gradle.kts
├── demo.gif
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts
/.cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "terminal-apps-on-devices",
3 | "configureDotEnv": true,
4 | "integrations": [
5 | {
6 | "name": "main"
7 | }
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # How has this been tested?
6 | - [ ] Automated
7 | - [ ] Manual
8 |
9 | ## Reproducible test instructions
10 |
11 |
12 |
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 | .cxx
10 | local.properties
11 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Code owners * clause must be set up to a stripes-only
2 | # set of people in order to allow BBPOS collaboration.
3 | # An approval from a stripe must be required for every PR into Stripe repos.
4 |
5 | * @stripe/terminal-aod
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Apps on Devices integration
2 |
3 | This repository contains a sample app that demonstrates best practices for writing an [Apps on Devices](https://stripe.com/docs/terminal/features/apps-on-devices/overview) integration.
4 |
5 | The app demonstrates the following
6 | - How to discover and connect the handoff reader
7 | - How to collect and confirm a payment
8 | - How to deep link to the device's admin settings
9 |
10 |
11 |
12 | ## Prerequisites
13 | Before proceeding with the integration, ensure you have the following
14 | - Stripe S700 DevKit smart reader
15 | - [Android Studio Flamingo](https://developer.android.com/studio/releases) or greater
16 |
17 | ## Setup
18 |
19 | ### Clone the repo
20 |
21 | Clone this repo and open it in [Android Studio](https://developer.android.com/studio).
22 |
23 | ### Deploy the Example Terminal Backend
24 | The Apps on Devices example app depends on the [Example Terminal Backend](https://github.com/stripe/example-terminal-backend).
25 |
26 | We recommend deploying the backend to Render.com.
27 |
28 | 1. Set up a free [Render account](https://dashboard.render.com/register) if you haven't created one previously.
29 | 2. Click the button below to deploy the backend. You'll be prompted to enter a name for the Render service group as well as your Stripe API test mode secret key.
30 | 3. Go to the [next steps](#next-steps) in this README for how to use this app
31 |
32 | [](https://render.com/deploy?repo=https://github.com/stripe/example-terminal-backend/)
33 |
34 | ### Point the example app to your backend
35 |
36 | Edit `local.properties` and add an entry for `BACKEND_URL`. For example if your instance is available at `https://my-backend-123.onrender.com`, you'll add the following.
37 |
38 | ```
39 | BACKEND_URL=https://my-backend-123.onrender.com
40 | ```
41 |
42 | ## Run
43 |
44 | Run the example app on a Stripe S700 DevKit smart reader.
45 |
46 | 
47 |
48 |
49 | ## Next steps
50 |
51 | - [Deploy the sample app](https://stripe.com/docs/terminal/features/apps-on-devices/deploy) to learn how to upload and deploy your app
52 | - Read [troubleshooting apps on devices](https://stripe.com/docs/terminal/features/apps-on-devices/troubleshooting) for resolutions to common issues
53 |
54 | ## Get support
55 | If you found a bug or want to suggest a new [feature/use case/sample], please [file an issue](../../issues).
56 |
57 | If you have questions, comments, or need help with code, we're here to help:
58 | - on [Discord](https://stripe.com/go/developer-chat)
59 | - on Twitter at [@StripeDev](https://twitter.com/StripeDev)
60 | - on Stack Overflow at the [stripe-payments](https://stackoverflow.com/tags/stripe-payments/info) tag
61 | - by [email](mailto:support+github@stripe.com)
62 |
63 | Sign up to [stay updated with developer news](https://go.stripe.global/dev-digest).
64 |
65 | ## Author(s)
66 | - [ericlin-bbpos](https://github.com/ericlin-bbpos)
67 | - [ianlin-bbpos](https://github.com/ianlin-bbpos)
68 | - [mshafrir-stripe](https://github.com/mshafrir-stripe)
69 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.application")
3 | id("org.jetbrains.kotlin.android")
4 | id("androidx.navigation.safeargs.kotlin")
5 | id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
6 | }
7 |
8 | android {
9 | namespace = "com.stripe.aod.sampleapp"
10 | compileSdk = 35
11 |
12 | defaultConfig {
13 | applicationId = "com.stripe.aod.sampleapp"
14 | minSdk = 28
15 | targetSdk = 35
16 | versionCode = 1
17 | versionName = "1.0"
18 |
19 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
20 | }
21 |
22 | buildTypes {
23 | release {
24 | isMinifyEnabled = false
25 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
26 | }
27 | }
28 |
29 | kotlin {
30 | jvmToolchain(JavaLanguageVersion.of(17).asInt())
31 | }
32 |
33 | kotlinOptions {
34 | allWarningsAsErrors = true
35 | }
36 |
37 | buildFeatures {
38 | viewBinding = true
39 | buildConfig = true
40 | }
41 | }
42 |
43 | dependencies {
44 | implementation(libs.androidx.appcompat)
45 | implementation(libs.android.material)
46 | implementation(libs.androidx.core.ktx)
47 | implementation(libs.androidx.swiperefreshlayout)
48 |
49 | // ViewModel and LiveData
50 | implementation(libs.androidx.lifecycle.livedata)
51 | implementation(libs.androidx.lifecycle.runtime)
52 | implementation(libs.androidx.lifecycle.viewmodel)
53 |
54 | implementation(libs.kotlinx.coroutines.android)
55 |
56 | implementation(libs.androidx.fragment.ktx)
57 |
58 | // OK HTTP
59 | implementation(libs.okhttp)
60 |
61 | // Retrofit
62 | implementation(libs.retrofit2.retrofit)
63 | implementation(libs.retrofit2.converter.gson)
64 |
65 | // Stripe Terminal library
66 | implementation(libs.stripe.terminal.core)
67 | implementation(libs.stripe.terminal.handoffclient)
68 |
69 | // navigation
70 | implementation(libs.androidx.navigation.fragment.ktx)
71 | implementation(libs.androidx.navigation.ui.ktx)
72 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.kts.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
19 |
20 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/app/src/main/java/com/stripe/aod/sampleapp/Config.kt:
--------------------------------------------------------------------------------
1 | package com.stripe.aod.sampleapp
2 |
3 | class Config {
4 | companion object {
5 | const val TAG = "AOD_SampleApp"
6 | }
7 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/stripe/aod/sampleapp/MyApp.kt:
--------------------------------------------------------------------------------
1 | package com.stripe.aod.sampleapp
2 |
3 | import android.app.Application
4 | import com.stripe.stripeterminal.TerminalApplicationDelegate
5 |
6 | class MyApp : Application() {
7 |
8 | override fun onCreate() {
9 | super.onCreate()
10 | TerminalApplicationDelegate.onCreate(this)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/stripe/aod/sampleapp/activity/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.stripe.aod.sampleapp.activity
2 |
3 | import android.Manifest
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.content.pm.PackageManager
7 | import android.location.LocationManager
8 | import android.os.Build
9 | import android.os.Bundle
10 | import android.provider.Settings
11 | import android.view.ContextThemeWrapper
12 | import androidx.activity.result.contract.ActivityResultContracts
13 | import androidx.activity.viewModels
14 | import androidx.annotation.RequiresApi
15 | import androidx.appcompat.app.AlertDialog
16 | import androidx.appcompat.app.AppCompatActivity
17 | import androidx.core.content.ContextCompat
18 | import com.stripe.aod.sampleapp.R
19 | import com.stripe.aod.sampleapp.listener.TerminalEventListener
20 | import com.stripe.aod.sampleapp.model.MainViewModel
21 | import com.stripe.stripeterminal.Terminal
22 | import com.stripe.stripeterminal.external.models.TerminalException
23 | import com.stripe.stripeterminal.log.LogLevel
24 |
25 | class MainActivity : AppCompatActivity() {
26 | private val viewModel by viewModels()
27 |
28 | private val requestPermissionLauncher = registerForActivityResult(
29 | ActivityResultContracts.RequestMultiplePermissions(),
30 | ::onPermissionResult
31 | )
32 |
33 | public override fun onCreate(savedInstanceState: Bundle?) {
34 | super.onCreate(savedInstanceState)
35 | setContentView(R.layout.activity_main)
36 |
37 | requestPermissionsIfNecessary()
38 | }
39 |
40 | private fun requestPermissionsIfNecessary() {
41 | if (Build.VERSION.SDK_INT >= 31) {
42 | requestPermissionsIfNecessarySdk31()
43 | } else {
44 | requestPermissionsIfNecessarySdkBelow31()
45 | }
46 | }
47 |
48 | private fun isGranted(permission: String): Boolean {
49 | return ContextCompat.checkSelfPermission(
50 | this,
51 | permission
52 | ) == PackageManager.PERMISSION_GRANTED
53 | }
54 |
55 | private fun requestPermissionsIfNecessarySdkBelow31() {
56 | // Check for location permissions
57 | if (!isGranted(Manifest.permission.ACCESS_FINE_LOCATION)) {
58 | // If we don't have them yet, request them before doing anything else
59 | requestPermissionLauncher.launch(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION))
60 | } else if (!Terminal.isInitialized() && verifyGpsEnabled()) {
61 | initialize()
62 | }
63 | }
64 |
65 | @RequiresApi(Build.VERSION_CODES.S)
66 | private fun requestPermissionsIfNecessarySdk31() {
67 | // Check for location and bluetooth permissions
68 | val deniedPermissions = listOf(
69 | Manifest.permission.ACCESS_FINE_LOCATION,
70 | Manifest.permission.BLUETOOTH_CONNECT,
71 | Manifest.permission.BLUETOOTH_SCAN
72 | )
73 | .filterNot(::isGranted)
74 | .toTypedArray()
75 |
76 | if (deniedPermissions.isNotEmpty()) {
77 | // If we don't have them yet, request them before doing anything else
78 | requestPermissionLauncher.launch(deniedPermissions)
79 | } else if (!Terminal.isInitialized() && verifyGpsEnabled()) {
80 | initialize()
81 | }
82 | }
83 |
84 | private fun onPermissionResult(result: Map) {
85 | val deniedPermissions: List = result
86 | .filter { !it.value }
87 | .map { it.key }
88 |
89 | // If we receive a response to our permission check, initialize
90 | if (deniedPermissions.isEmpty() && !Terminal.isInitialized() && verifyGpsEnabled()) {
91 | initialize()
92 | }
93 | }
94 |
95 | /**
96 | * Initialize the [Terminal]
97 | */
98 | private fun initialize() {
99 | // Initialize the Terminal as soon as possible
100 | try {
101 | Terminal.initTerminal(
102 | applicationContext,
103 | LogLevel.VERBOSE,
104 | viewModel.tokenProvider,
105 | TerminalEventListener,
106 | )
107 |
108 | viewModel.discoveryReaders()
109 | } catch (e: TerminalException) {
110 | throw RuntimeException(
111 | "Location services are required in order to initialize the Terminal.",
112 | e
113 | )
114 | }
115 | }
116 |
117 | private fun verifyGpsEnabled(): Boolean {
118 | val locationManager: LocationManager? =
119 | applicationContext.getSystemService(Context.LOCATION_SERVICE) as? LocationManager
120 |
121 | val gpsEnabled = runCatching {
122 | locationManager?.isProviderEnabled(LocationManager.GPS_PROVIDER) ?: false
123 | }.getOrDefault(false)
124 |
125 | if (!gpsEnabled) {
126 | // notify user
127 | AlertDialog.Builder(
128 | ContextThemeWrapper(
129 | this,
130 | com.google.android.material.R.style.Theme_MaterialComponents_DayNight_DarkActionBar
131 | )
132 | )
133 | .setMessage("Please enable location services")
134 | .setCancelable(false)
135 | .setPositiveButton("Open location settings") { _, _ ->
136 | this.startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
137 | }
138 | .create()
139 | .show()
140 | }
141 |
142 | return gpsEnabled
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/app/src/main/java/com/stripe/aod/sampleapp/data/CreatePaymentParams.kt:
--------------------------------------------------------------------------------
1 | package com.stripe.aod.sampleapp.data
2 |
3 | data class CreatePaymentParams(
4 | val amount: Int,
5 | val currency: String,
6 | val requestExtendedAuthorization: Boolean = true,
7 | val requestIncrementalAuthorizationSupport: Boolean = true,
8 | val description: String = "",
9 | )
10 |
11 | fun CreatePaymentParams.toMap(): Map {
12 | return mapOf(
13 | "amount" to amount.toString(),
14 | "currency" to currency,
15 | "description" to description,
16 | "payment_method_options[card_present[request_extended_authorization]]" to requestExtendedAuthorization.toString(),
17 | "payment_method_options[card_present[request_incremental_authorization_support]]" to requestIncrementalAuthorizationSupport.toString()
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/stripe/aod/sampleapp/data/EmailReceiptParams.kt:
--------------------------------------------------------------------------------
1 | package com.stripe.aod.sampleapp.data
2 |
3 | data class EmailReceiptParams(
4 | val paymentIntentId: String,
5 | val receiptEmail: String
6 | )
7 |
8 | fun EmailReceiptParams.toMap(): Map {
9 | return mapOf(
10 | "payment_intent_id" to paymentIntentId,
11 | "receipt_email" to receiptEmail
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/stripe/aod/sampleapp/data/PaymentIntentCreationResponse.kt:
--------------------------------------------------------------------------------
1 | package com.stripe.aod.sampleapp.data
2 |
3 | /**
4 | * PaymentIntentCreationResponse data model from example backend
5 | */
6 | data class PaymentIntentCreationResponse(val paymentIntentId: String, val secret: String)
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/stripe/aod/sampleapp/fragment/EmailFragment.kt:
--------------------------------------------------------------------------------
1 | package com.stripe.aod.sampleapp.fragment
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import androidx.core.widget.doAfterTextChanged
6 | import androidx.fragment.app.Fragment
7 | import androidx.fragment.app.viewModels
8 | import androidx.navigation.fragment.findNavController
9 | import androidx.navigation.fragment.navArgs
10 | import com.google.android.material.snackbar.Snackbar
11 | import com.stripe.aod.sampleapp.R
12 | import com.stripe.aod.sampleapp.data.EmailReceiptParams
13 | import com.stripe.aod.sampleapp.databinding.FragmentEmailBinding
14 | import com.stripe.aod.sampleapp.model.CheckoutViewModel
15 | import com.stripe.aod.sampleapp.utils.backToHome
16 | import com.stripe.aod.sampleapp.utils.hideKeyboard
17 | import com.stripe.aod.sampleapp.utils.setThrottleClickListener
18 |
19 | class EmailFragment : Fragment(R.layout.fragment_email) {
20 | private val emailRegex = "^[A-Za-z\\d+_.-]+@[A-Za-z\\d.-]+\$"
21 | private val viewModel by viewModels()
22 | private val args: EmailFragmentArgs by navArgs()
23 |
24 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
25 | super.onViewCreated(view, savedInstanceState)
26 |
27 | val viewBinding = FragmentEmailBinding.bind(view)
28 |
29 | viewBinding.back.setThrottleClickListener {
30 | findNavController().navigateUp()
31 | }
32 |
33 | viewBinding.inputEdit.doAfterTextChanged {
34 | val isValidEmail = it?.toString()?.matches(emailRegex.toRegex()) ?: false
35 | viewBinding.emailSend.isEnabled = isValidEmail
36 | }
37 |
38 | viewBinding.emailSend.setThrottleClickListener {
39 | viewBinding.emailSend.isEnabled = false
40 | viewBinding.inputEdit.hideKeyboard()
41 | viewModel.updateReceiptEmailPaymentIntent(
42 | EmailReceiptParams(
43 | paymentIntentId = args.paymentIntentID,
44 | receiptEmail = viewBinding.inputEdit.text.toString().trim()
45 | ),
46 | onSuccess = {
47 | backToHome()
48 | },
49 | onFailure = { message ->
50 | viewBinding.emailSend.isEnabled = true
51 |
52 | Snackbar.make(
53 | viewBinding.emailSend,
54 | message.value.ifEmpty {
55 | getString(R.string.error_fail_to_send_email_receipt)
56 | },
57 | Snackbar.LENGTH_SHORT
58 | ).show()
59 | }
60 | )
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/app/src/main/java/com/stripe/aod/sampleapp/fragment/HomeFragment.kt:
--------------------------------------------------------------------------------
1 | package com.stripe.aod.sampleapp.fragment
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import android.os.Bundle
6 | import android.view.View
7 | import androidx.fragment.app.Fragment
8 | import androidx.fragment.app.activityViewModels
9 | import androidx.navigation.fragment.findNavController
10 | import com.google.android.material.snackbar.Snackbar
11 | import com.stripe.aod.sampleapp.R
12 | import com.stripe.aod.sampleapp.databinding.FragmentHomeBinding
13 | import com.stripe.aod.sampleapp.model.MainViewModel
14 | import com.stripe.aod.sampleapp.utils.launchAndRepeatWithViewLifecycle
15 | import com.stripe.aod.sampleapp.utils.navOptions
16 | import com.stripe.aod.sampleapp.utils.setThrottleClickListener
17 | import com.stripe.stripeterminal.external.models.ConnectionStatus
18 | import com.stripe.stripeterminal.external.models.PaymentStatus
19 | import kotlinx.coroutines.flow.filter
20 |
21 | class HomeFragment : Fragment(R.layout.fragment_home) {
22 | private val viewModel: MainViewModel by activityViewModels()
23 |
24 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
25 | super.onViewCreated(view, savedInstanceState)
26 |
27 | val viewBinding = FragmentHomeBinding.bind(view)
28 |
29 | viewBinding.menuSettings.setThrottleClickListener {
30 | startActivity(Intent(Intent.ACTION_VIEW).setData(Uri.parse("stripe://settings/")))
31 | }
32 |
33 | launchAndRepeatWithViewLifecycle {
34 | viewModel.readerConnectStatus.collect {
35 | viewBinding.indicator.visibility = if (it != ConnectionStatus.CONNECTED) {
36 | View.VISIBLE
37 | } else {
38 | View.INVISIBLE
39 | }
40 | }
41 | }
42 |
43 | launchAndRepeatWithViewLifecycle {
44 | viewModel.readerPaymentStatus.collect {
45 | viewBinding.newPayment.isEnabled = (it == PaymentStatus.READY)
46 | }
47 | }
48 |
49 | launchAndRepeatWithViewLifecycle {
50 | viewModel.userMessage.filter {
51 | it.isNotEmpty()
52 | }.collect { message ->
53 | Snackbar.make(viewBinding.newPayment, message, Snackbar.LENGTH_SHORT).show()
54 | }
55 | }
56 |
57 | viewBinding.newPayment.setThrottleClickListener {
58 | findNavController().navigate(
59 | R.id.action_homeFragment_to_inputFragment,
60 | null,
61 | navOptions()
62 | )
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/app/src/main/java/com/stripe/aod/sampleapp/fragment/InputFragment.kt:
--------------------------------------------------------------------------------
1 | package com.stripe.aod.sampleapp.fragment
2 |
3 | import android.annotation.SuppressLint
4 | import android.os.Bundle
5 | import android.view.MotionEvent
6 | import android.view.View
7 | import android.view.View.OnTouchListener
8 | import android.widget.TextView
9 | import androidx.activity.OnBackPressedCallback
10 | import androidx.appcompat.content.res.AppCompatResources
11 | import androidx.fragment.app.Fragment
12 | import androidx.fragment.app.viewModels
13 | import androidx.navigation.fragment.findNavController
14 | import com.google.android.material.snackbar.Snackbar
15 | import com.stripe.aod.sampleapp.R
16 | import com.stripe.aod.sampleapp.data.CreatePaymentParams
17 | import com.stripe.aod.sampleapp.databinding.FragmentInputBinding
18 | import com.stripe.aod.sampleapp.model.CheckoutViewModel
19 | import com.stripe.aod.sampleapp.model.InputViewModel
20 | import com.stripe.aod.sampleapp.utils.formatCentsToString
21 | import com.stripe.aod.sampleapp.utils.launchAndRepeatWithViewLifecycle
22 | import com.stripe.aod.sampleapp.utils.navOptions
23 | import com.stripe.aod.sampleapp.utils.setThrottleClickListener
24 | import com.stripe.stripeterminal.external.models.PaymentIntentStatus
25 |
26 | class InputFragment : Fragment(R.layout.fragment_input), OnTouchListener {
27 | private lateinit var viewBinding: FragmentInputBinding
28 | private val inputViewModel by viewModels()
29 | private val checkoutViewModel by viewModels()
30 |
31 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
32 | super.onViewCreated(view, savedInstanceState)
33 | initView(view)
34 | // hand back press action
35 | requireActivity().onBackPressedDispatcher.addCallback(
36 | viewLifecycleOwner,
37 | object : OnBackPressedCallback(true) {
38 | override fun handleOnBackPressed() {
39 | findNavController().navigateUp()
40 | }
41 | }
42 | )
43 | }
44 |
45 | @SuppressLint("ClickableViewAccessibility")
46 | private fun initView(view: View) {
47 | viewBinding = FragmentInputBinding.bind(view)
48 | viewBinding.back.setThrottleClickListener { findNavController().navigateUp() }
49 | viewBinding.keypad.key0.setOnTouchListener(this)
50 | viewBinding.keypad.key1.setOnTouchListener(this)
51 | viewBinding.keypad.key2.setOnTouchListener(this)
52 | viewBinding.keypad.key3.setOnTouchListener(this)
53 | viewBinding.keypad.key4.setOnTouchListener(this)
54 | viewBinding.keypad.key5.setOnTouchListener(this)
55 | viewBinding.keypad.key6.setOnTouchListener(this)
56 | viewBinding.keypad.key7.setOnTouchListener(this)
57 | viewBinding.keypad.key8.setOnTouchListener(this)
58 | viewBinding.keypad.key9.setOnTouchListener(this)
59 | viewBinding.keypad.keyClear.setOnTouchListener(this)
60 | viewBinding.keypad.keyBackspace.setOnTouchListener(this)
61 | viewBinding.submit.setThrottleClickListener { requestNewPayment() }
62 |
63 | inputViewModel.displayAmount(action = InputViewModel.Action.Clear)
64 |
65 | launchAndRepeatWithViewLifecycle {
66 | inputViewModel.showModifierKeys.collect {
67 | val visibility = if (it) View.VISIBLE else View.INVISIBLE
68 |
69 | viewBinding.keypad.keyClear.visibility = visibility
70 | viewBinding.keypad.keyBackspace.visibility = visibility
71 | viewBinding.submit.isEnabled = it
72 | }
73 | }
74 |
75 | launchAndRepeatWithViewLifecycle {
76 | inputViewModel.amount.collect {
77 | viewBinding.amount.text = if (it.isEmpty()) {
78 | formatCentsToString(0)
79 | } else {
80 | formatCentsToString(
81 | it.toInt()
82 | )
83 | }
84 | }
85 | }
86 |
87 | launchAndRepeatWithViewLifecycle {
88 | checkoutViewModel.currentPaymentIntent.collect { paymentIntent ->
89 | paymentIntent?.takeIf {
90 | it.status == PaymentIntentStatus.REQUIRES_CAPTURE
91 | }?.let {
92 | findNavController().navigate(
93 | InputFragmentDirections.actionInputFragmentToReceiptFragment(
94 | paymentIntentID = it.id.orEmpty(),
95 | amount = it.amount.toInt()
96 | ),
97 | navOptions()
98 | )
99 | }
100 | }
101 | }
102 | }
103 |
104 | private fun requestNewPayment() {
105 | viewBinding.submit.isEnabled = false
106 |
107 | checkoutViewModel.createPaymentIntent(
108 | CreatePaymentParams(
109 | amount = inputViewModel.amount.value.toInt(),
110 | currency = "usd",
111 | description = "Apps on Devices sample app transaction",
112 | )
113 | ) { failureMessage ->
114 | Snackbar.make(
115 | viewBinding.root,
116 | failureMessage.value.ifEmpty {
117 | getString(R.string.error_fail_to_create_payment_intent)
118 | },
119 | Snackbar.LENGTH_SHORT
120 | ).show()
121 | viewBinding.submit.isEnabled = true
122 | }
123 | }
124 |
125 | @SuppressLint("ClickableViewAccessibility")
126 | override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {
127 | var inputChar: Char? = null
128 | val scaleView = when (val id = view.id) {
129 | R.id.key_0 -> {
130 | inputChar = '0'
131 | viewBinding.keypad.digit0
132 | }
133 | R.id.key_1 -> {
134 | inputChar = '1'
135 | viewBinding.keypad.digit1
136 | }
137 | R.id.key_2 -> {
138 | inputChar = '2'
139 | viewBinding.keypad.digit2
140 | }
141 | R.id.key_3 -> {
142 | inputChar = '3'
143 | viewBinding.keypad.digit3
144 | }
145 | R.id.key_4 -> {
146 | inputChar = '4'
147 | viewBinding.keypad.digit4
148 | }
149 | R.id.key_5 -> {
150 | inputChar = '5'
151 | viewBinding.keypad.digit5
152 | }
153 | R.id.key_6 -> {
154 | inputChar = '6'
155 | viewBinding.keypad.digit6
156 | }
157 | R.id.key_7 -> {
158 | inputChar = '7'
159 | viewBinding.keypad.digit7
160 | }
161 | R.id.key_8 -> {
162 | inputChar = '8'
163 | viewBinding.keypad.digit8
164 | }
165 | R.id.key_9 -> {
166 | inputChar = '9'
167 | viewBinding.keypad.digit9
168 | }
169 | R.id.key_clear -> {
170 | viewBinding.keypad.clear
171 | }
172 | R.id.key_backspace -> {
173 | viewBinding.keypad.backspace
174 | }
175 | else -> {
176 | error("Unexpected view with id: $id")
177 | }
178 | }
179 |
180 | when (motionEvent.action) {
181 | MotionEvent.ACTION_DOWN -> {
182 | if (view.id !in listOf(R.id.key_backspace, R.id.key_clear)) {
183 | view.background = AppCompatResources.getDrawable(
184 | requireContext(),
185 | R.drawable.keyboard_active
186 | )
187 | (scaleView as TextView).setTextColor(
188 | resources.getColor(R.color.text_digit_pressed, context?.theme)
189 | )
190 | }
191 | scaleView.animate()
192 | .scaleX(1.5f)
193 | .scaleY(1.5f)
194 | .setDuration(200)
195 | .start()
196 | }
197 | MotionEvent.ACTION_UP -> {
198 | if (view.id !in listOf(R.id.key_backspace, R.id.key_clear)) {
199 | view.background = AppCompatResources.getDrawable(
200 | requireContext(),
201 | R.drawable.keyboard_inactive
202 | )
203 | (scaleView as TextView).setTextColor(
204 | resources.getColor(R.color.text_digit_default, context?.theme)
205 | )
206 | }
207 | scaleView.animate()
208 | .scaleX(1.0f)
209 | .scaleY(1.0f)
210 | .setDuration(200)
211 | .start()
212 | handlerClickAction(scaleView, inputChar)
213 | }
214 | }
215 | return true
216 | }
217 |
218 | private fun handlerClickAction(view: View, inputChar: Char?) {
219 | when (view) {
220 | viewBinding.keypad.clear -> {
221 | inputViewModel.displayAmount(action = InputViewModel.Action.Clear)
222 | }
223 | viewBinding.keypad.backspace -> {
224 | inputViewModel.displayAmount(action = InputViewModel.Action.Delete)
225 | }
226 | else -> {
227 | inputViewModel.displayAmount(inputChar)
228 | }
229 | }
230 | }
231 | }
232 |
--------------------------------------------------------------------------------
/app/src/main/java/com/stripe/aod/sampleapp/fragment/ReceiptFragment.kt:
--------------------------------------------------------------------------------
1 | package com.stripe.aod.sampleapp.fragment
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import androidx.fragment.app.Fragment
6 | import androidx.navigation.fragment.findNavController
7 | import androidx.navigation.fragment.navArgs
8 | import com.stripe.aod.sampleapp.R
9 | import com.stripe.aod.sampleapp.databinding.FragmentReceiptBinding
10 | import com.stripe.aod.sampleapp.utils.backToHome
11 | import com.stripe.aod.sampleapp.utils.formatCentsToString
12 | import com.stripe.aod.sampleapp.utils.navOptions
13 | import com.stripe.aod.sampleapp.utils.setThrottleClickListener
14 |
15 | class ReceiptFragment : Fragment(R.layout.fragment_receipt) {
16 | private val args: ReceiptFragmentArgs by navArgs()
17 |
18 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
19 | super.onViewCreated(view, savedInstanceState)
20 | val viewBinding = FragmentReceiptBinding.bind(view)
21 |
22 | viewBinding.totalAmount.text = formatCentsToString(args.amount)
23 | viewBinding.receiptPrint.isEnabled = false
24 | viewBinding.receiptSms.isEnabled = false
25 | viewBinding.receiptEmail.setThrottleClickListener {
26 | findNavController().navigate(
27 | ReceiptFragmentDirections.actionReceiptFragmentToEmailFragment(
28 | args.paymentIntentID
29 | ),
30 | navOptions()
31 | )
32 | }
33 |
34 | viewBinding.receiptSkip.setOnClickListener {
35 | backToHome()
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/src/main/java/com/stripe/aod/sampleapp/listener/TerminalEventListener.kt:
--------------------------------------------------------------------------------
1 | package com.stripe.aod.sampleapp.listener
2 |
3 | import android.util.Log
4 | import com.stripe.aod.sampleapp.Config
5 | import com.stripe.stripeterminal.external.callable.TerminalListener
6 | import com.stripe.stripeterminal.external.models.ConnectionStatus
7 | import com.stripe.stripeterminal.external.models.PaymentStatus
8 | import kotlinx.coroutines.CoroutineScope
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.flow.Flow
11 | import kotlinx.coroutines.flow.MutableSharedFlow
12 | import kotlinx.coroutines.flow.asSharedFlow
13 | import kotlinx.coroutines.launch
14 |
15 | /**
16 | * The `TerminalEventListener` implements the [TerminalListener] interface and will
17 | * forward along any events to other parts of the app that register for updates.
18 | *
19 | */
20 | object TerminalEventListener : TerminalListener {
21 | private val _onConnectionStatusChange = MutableSharedFlow()
22 | private val _onPaymentStatusChange = MutableSharedFlow()
23 |
24 | val onConnectionStatusChange: Flow = _onConnectionStatusChange.asSharedFlow()
25 | val onPaymentStatusChange: Flow = _onPaymentStatusChange.asSharedFlow()
26 |
27 | private val scope = CoroutineScope(Dispatchers.IO)
28 |
29 | override fun onConnectionStatusChange(status: ConnectionStatus) {
30 | Log.i(Config.TAG, "onConnectionStatusChange: $status")
31 |
32 | scope.launch {
33 | _onConnectionStatusChange.emit(status)
34 | }
35 | }
36 |
37 | override fun onPaymentStatusChange(status: PaymentStatus) {
38 | Log.i(Config.TAG, "onPaymentStatusChange: $status")
39 |
40 | scope.launch {
41 | _onPaymentStatusChange.emit(status)
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/java/com/stripe/aod/sampleapp/model/CheckoutViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.stripe.aod.sampleapp.model
2 |
3 | import android.util.Log
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.stripe.aod.sampleapp.Config
7 | import com.stripe.aod.sampleapp.data.CreatePaymentParams
8 | import com.stripe.aod.sampleapp.data.EmailReceiptParams
9 | import com.stripe.aod.sampleapp.data.toMap
10 | import com.stripe.aod.sampleapp.network.ApiClient
11 | import com.stripe.stripeterminal.Terminal
12 | import com.stripe.stripeterminal.external.callable.PaymentIntentCallback
13 | import com.stripe.stripeterminal.external.models.CollectConfiguration
14 | import com.stripe.stripeterminal.external.models.PaymentIntent
15 | import com.stripe.stripeterminal.external.models.TerminalException
16 | import kotlinx.coroutines.flow.MutableStateFlow
17 | import kotlinx.coroutines.flow.asStateFlow
18 | import kotlinx.coroutines.flow.update
19 | import kotlinx.coroutines.launch
20 | import kotlin.coroutines.resume
21 | import kotlin.coroutines.suspendCoroutine
22 |
23 | class CheckoutViewModel : ViewModel() {
24 | private val _currentPaymentIntent = MutableStateFlow(null)
25 | val currentPaymentIntent = _currentPaymentIntent.asStateFlow()
26 |
27 | fun createPaymentIntent(
28 | createPaymentParams: CreatePaymentParams,
29 | onFailure: (FailureMessage) -> Unit
30 | ) {
31 | viewModelScope.launch {
32 | createAndProcessPaymentIntent(createPaymentParams.toMap())
33 | .fold(
34 | onSuccess = { paymentIntent ->
35 | _currentPaymentIntent.update { paymentIntent }
36 | },
37 | onFailure = {
38 | val failureMessage = if (it is TerminalException) {
39 | it.errorMessage
40 | } else {
41 | it.message ?: "Failed to collect payment"
42 | }.let(::FailureMessage)
43 | onFailure(failureMessage)
44 | }
45 | )
46 | }
47 | }
48 |
49 | private suspend fun createAndProcessPaymentIntent(
50 | createPaymentIntentParams: Map
51 | ): Result {
52 | return ApiClient.createPaymentIntent(createPaymentIntentParams)
53 | .mapCatching { response ->
54 | val secret = response.secret
55 | val paymentIntent = retrievePaymentIntent(secret)
56 | val paymentIntentAfterCollect = collectPaymentInfo(paymentIntent)
57 | processPayment(paymentIntentAfterCollect)
58 | }
59 | }
60 |
61 | private suspend fun retrievePaymentIntent(
62 | secret: String
63 | ): PaymentIntent = suspendCoroutine { continuation ->
64 | Terminal.getInstance().retrievePaymentIntent(
65 | secret,
66 | object : PaymentIntentCallback {
67 | override fun onSuccess(paymentIntent: PaymentIntent) {
68 | continuation.resume(paymentIntent)
69 | }
70 |
71 | override fun onFailure(e: TerminalException) {
72 | Log.e(Config.TAG, "retrievePaymentIntent failure", e)
73 | continuation.resumeWith(Result.failure(e))
74 | }
75 | }
76 | )
77 | }
78 |
79 | private suspend fun collectPaymentInfo(
80 | paymentIntent: PaymentIntent
81 | ): PaymentIntent = suspendCoroutine { continuation ->
82 | Terminal.getInstance().collectPaymentMethod(
83 | paymentIntent,
84 | object : PaymentIntentCallback {
85 | override fun onSuccess(paymentIntent: PaymentIntent) {
86 | continuation.resume(paymentIntent)
87 | }
88 |
89 | override fun onFailure(e: TerminalException) {
90 | Log.e(Config.TAG, "collectPaymentMethod failure", e)
91 | continuation.resumeWith(Result.failure(e))
92 | }
93 | },
94 | CollectConfiguration.Builder().skipTipping(false).build()
95 | )
96 | }
97 |
98 | private suspend fun processPayment(paymentIntent: PaymentIntent): PaymentIntent {
99 | return suspendCoroutine { continuation ->
100 | Terminal.getInstance().confirmPaymentIntent(
101 | paymentIntent,
102 | object : PaymentIntentCallback {
103 | override fun onSuccess(paymentIntent: PaymentIntent) {
104 | Log.d(Config.TAG, "processPaymentCallback onSuccess ")
105 | continuation.resume(paymentIntent)
106 | }
107 |
108 | override fun onFailure(e: TerminalException) {
109 | Log.e(Config.TAG, "processPayment failure", e)
110 | continuation.resumeWith(Result.failure(e))
111 | }
112 | }
113 | )
114 | }
115 | }
116 |
117 | fun updateReceiptEmailPaymentIntent(
118 | emailReceiptParams: EmailReceiptParams,
119 | onSuccess: () -> Unit,
120 | onFailure: (FailureMessage) -> Unit
121 | ) {
122 | viewModelScope.launch {
123 | updateAndProcessPaymentIntent(emailReceiptParams.toMap()).fold(
124 | onSuccess = {
125 | onSuccess()
126 | },
127 | onFailure = {
128 | onFailure(
129 | FailureMessage("Failed to update PaymentIntent")
130 | )
131 | }
132 | )
133 | }
134 | }
135 |
136 | private suspend fun updateAndProcessPaymentIntent(
137 | createPaymentIntentParams: Map
138 | ): Result =
139 | ApiClient.updatePaymentIntent(createPaymentIntentParams)
140 | .mapCatching { response ->
141 | val secret = response.secret
142 | val paymentIntent = retrievePaymentIntent(secret)
143 | capturePaymentIntent(paymentIntent).isSuccess
144 | }
145 |
146 | private suspend fun capturePaymentIntent(paymentIntent: PaymentIntent) =
147 | ApiClient.capturePaymentIntent(paymentIntent.id.orEmpty())
148 | }
149 |
--------------------------------------------------------------------------------
/app/src/main/java/com/stripe/aod/sampleapp/model/ConnectionToken.kt:
--------------------------------------------------------------------------------
1 | package com.stripe.aod.sampleapp.model
2 |
3 | /**
4 | * Data class used to handle the connection token response from our backend
5 | */
6 | data class ConnectionToken(val secret: String)
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/stripe/aod/sampleapp/model/FailureMessage.kt:
--------------------------------------------------------------------------------
1 | package com.stripe.aod.sampleapp.model
2 |
3 | @JvmInline
4 | value class FailureMessage(val value: String)
5 |
--------------------------------------------------------------------------------
/app/src/main/java/com/stripe/aod/sampleapp/model/InputViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.stripe.aod.sampleapp.model
2 |
3 | import androidx.lifecycle.ViewModel
4 | import java.util.regex.Pattern
5 | import kotlinx.coroutines.flow.MutableStateFlow
6 | import kotlinx.coroutines.flow.StateFlow
7 | import kotlinx.coroutines.flow.asStateFlow
8 | import kotlinx.coroutines.flow.update
9 |
10 | class InputViewModel : ViewModel() {
11 | enum class Action {
12 | Add, Delete, Clear
13 | }
14 |
15 | private val pattern = Pattern.compile("0*")
16 |
17 | private val _amount: MutableStateFlow = MutableStateFlow("")
18 | val amount: StateFlow = _amount.asStateFlow()
19 |
20 | private val _showModifierKeys: MutableStateFlow = MutableStateFlow(false)
21 | val showModifierKeys: StateFlow = _showModifierKeys.asStateFlow()
22 |
23 | private fun addAmountCharacter(w: Char?): String {
24 | _amount.update { currentAmount ->
25 | if (w == null || currentAmount.length >= 8 ||
26 | (w == '0' && pattern.matcher(currentAmount).matches())
27 | ) {
28 | currentAmount
29 | } else {
30 | currentAmount + w
31 | }
32 | }
33 | return _amount.value
34 | }
35 |
36 | private fun deleteAmountCharacter(): String {
37 | _amount.update { currentAmount ->
38 | if (currentAmount.isEmpty()) {
39 | currentAmount
40 | } else {
41 | currentAmount.substring(0, currentAmount.length - 1)
42 | }
43 | }
44 | return _amount.value
45 | }
46 |
47 | private fun clearAmount(): String {
48 | _amount.update { "" }
49 | return _amount.value
50 | }
51 |
52 | fun displayAmount(amt: Char? = null, action: Action = Action.Add) {
53 | val amountValue = when (action) {
54 | Action.Add -> {
55 | addAmountCharacter(amt)
56 | }
57 | Action.Delete -> {
58 | deleteAmountCharacter()
59 | }
60 | Action.Clear -> {
61 | clearAmount()
62 | }
63 | }
64 | _amount.update { amountValue }
65 | _showModifierKeys.update { _amount.value.isNotBlank() }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/app/src/main/java/com/stripe/aod/sampleapp/model/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.stripe.aod.sampleapp.model
2 |
3 | import android.util.Log
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.stripe.aod.sampleapp.Config
7 | import com.stripe.aod.sampleapp.listener.TerminalEventListener
8 | import com.stripe.aod.sampleapp.network.TokenProvider
9 | import com.stripe.stripeterminal.Terminal
10 | import com.stripe.stripeterminal.external.callable.Callback
11 | import com.stripe.stripeterminal.external.callable.Cancelable
12 | import com.stripe.stripeterminal.external.callable.DiscoveryListener
13 | import com.stripe.stripeterminal.external.callable.HandoffReaderListener
14 | import com.stripe.stripeterminal.external.callable.ReaderCallback
15 | import com.stripe.stripeterminal.external.models.ConnectionConfiguration
16 | import com.stripe.stripeterminal.external.models.ConnectionStatus
17 | import com.stripe.stripeterminal.external.models.DisconnectReason
18 | import com.stripe.stripeterminal.external.models.DiscoveryConfiguration
19 | import com.stripe.stripeterminal.external.models.PaymentStatus
20 | import com.stripe.stripeterminal.external.models.Reader
21 | import com.stripe.stripeterminal.external.models.ReaderEvent
22 | import com.stripe.stripeterminal.external.models.TerminalException
23 | import kotlinx.coroutines.flow.MutableStateFlow
24 | import kotlinx.coroutines.flow.StateFlow
25 | import kotlinx.coroutines.flow.asStateFlow
26 | import kotlinx.coroutines.flow.update
27 | import kotlinx.coroutines.launch
28 |
29 | class MainViewModel : ViewModel() {
30 | val tokenProvider = TokenProvider(viewModelScope)
31 |
32 | private val _readerConnectStatus: MutableStateFlow = MutableStateFlow(
33 | ConnectionStatus.NOT_CONNECTED
34 | )
35 | val readerConnectStatus: StateFlow = _readerConnectStatus.asStateFlow()
36 | private val _readerPaymentStatus: MutableStateFlow = MutableStateFlow(
37 | PaymentStatus.NOT_READY
38 | )
39 | val readerPaymentStatus: StateFlow = _readerPaymentStatus.asStateFlow()
40 |
41 | private var discoveryTask: Cancelable? = null
42 | private val config = DiscoveryConfiguration.HandoffDiscoveryConfiguration()
43 |
44 | private val _userMessage: MutableStateFlow = MutableStateFlow("")
45 | val userMessage: StateFlow = _userMessage.asStateFlow()
46 |
47 | private lateinit var targetReader: Reader
48 |
49 | private val discoveryListener: DiscoveryListener = object : DiscoveryListener {
50 | override fun onUpdateDiscoveredReaders(readers: List) {
51 | val reader = readers.firstOrNull { it.networkStatus == Reader.NetworkStatus.ONLINE }
52 | if (reader != null) {
53 | targetReader = reader
54 | connectReader()
55 | } else {
56 | _userMessage.update {
57 | "Register a reader using the device settings to start accepting payments"
58 | }
59 | }
60 | }
61 | }
62 |
63 | private val discoveryCallback: Callback = object : Callback {
64 | override fun onSuccess() {
65 | Log.d(Config.TAG, "discoveryCallback onSuccess")
66 | }
67 |
68 | override fun onFailure(e: TerminalException) {
69 | Log.e(Config.TAG, "discoveryCallback onFailure", e)
70 | }
71 | }
72 |
73 | init {
74 | viewModelScope.launch {
75 | launch {
76 | TerminalEventListener.onConnectionStatusChange.collect(::updateConnectStatus)
77 | }
78 |
79 | launch {
80 | TerminalEventListener.onPaymentStatusChange.collect(::updatePaymentStatus)
81 | }
82 | }
83 | }
84 |
85 | @Suppress("MissingPermission")
86 | fun discoveryReaders() {
87 | discoveryTask = Terminal.getInstance().discoverReaders(
88 | config,
89 | discoveryListener,
90 | discoveryCallback
91 | )
92 | }
93 |
94 | fun connectReader() {
95 | getCurrentReader()?.let { reader ->
96 | // same one , skip
97 | if (targetReader.id == reader.id) {
98 | return
99 | }
100 |
101 | // different reader , disconnect old first then connect new one again
102 | val currentReader: Reader = reader
103 | Terminal.getInstance().disconnectReader(object : Callback {
104 | override fun onSuccess() {
105 | Log.d(Config.TAG, "Current Reader [ ${currentReader.id} ] disconnect success ")
106 | }
107 |
108 | override fun onFailure(e: TerminalException) {
109 | Log.e(Config.TAG, "Current Reader [ ${currentReader.id} ] disconnect fail ")
110 | }
111 | })
112 | }
113 |
114 | Log.i(Config.TAG, "Connecting to new Reader [ ${targetReader.id} ] .... ")
115 | val readerCallback: ReaderCallback = object : ReaderCallback {
116 | override fun onSuccess(reader: Reader) {
117 | Log.i(Config.TAG, "Reader [ ${targetReader.id} ] Connected ")
118 | }
119 |
120 | override fun onFailure(e: TerminalException) {
121 | _userMessage.update { e.errorMessage }
122 | }
123 | }
124 |
125 | Terminal.getInstance().connectReader(
126 | targetReader,
127 | ConnectionConfiguration.HandoffConnectionConfiguration(
128 | object : HandoffReaderListener {
129 | override fun onDisconnect(reason: DisconnectReason) {
130 | Log.i(Config.TAG, "onDisconnect: $reason")
131 | }
132 |
133 | override fun onReportReaderEvent(event: ReaderEvent) {
134 | Log.i(Config.TAG, "onReportReaderEvent: $event")
135 | }
136 | }
137 | ),
138 | readerCallback
139 | )
140 | }
141 |
142 | private fun updateConnectStatus(status: ConnectionStatus) {
143 | _readerConnectStatus.update { status }
144 | }
145 |
146 | private fun updatePaymentStatus(status: PaymentStatus) {
147 | _readerPaymentStatus.update { status }
148 | }
149 |
150 | private fun getCurrentReader(): Reader? {
151 | return Terminal.getInstance().connectedReader
152 | }
153 |
154 | private fun stopDiscovery() {
155 | discoveryTask?.cancel(object : Callback {
156 | override fun onSuccess() {
157 | discoveryTask = null
158 | }
159 |
160 | override fun onFailure(e: TerminalException) {
161 | discoveryTask = null
162 | }
163 | })
164 |
165 | Terminal.getInstance().disconnectReader(object : Callback {
166 | override fun onFailure(e: TerminalException) {
167 | }
168 |
169 | override fun onSuccess() {
170 | }
171 | })
172 | }
173 |
174 | override fun onCleared() {
175 | super.onCleared()
176 | stopDiscovery()
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/app/src/main/java/com/stripe/aod/sampleapp/network/ApiClient.kt:
--------------------------------------------------------------------------------
1 | package com.stripe.aod.sampleapp.network
2 |
3 | import android.util.Log
4 | import com.stripe.aod.sampleapp.BuildConfig
5 | import com.stripe.aod.sampleapp.Config
6 | import com.stripe.aod.sampleapp.data.PaymentIntentCreationResponse
7 | import com.stripe.stripeterminal.external.models.ConnectionTokenException
8 | import okhttp3.OkHttpClient
9 | import retrofit2.Retrofit
10 | import retrofit2.converter.gson.GsonConverterFactory
11 | import retrofit2.http.Field
12 | import java.io.IOException
13 | import java.net.SocketTimeoutException
14 | import java.util.concurrent.TimeUnit
15 | import java.util.concurrent.TimeoutException
16 |
17 | object ApiClient {
18 |
19 | private val client = OkHttpClient.Builder()
20 | .connectTimeout(30, TimeUnit.SECONDS)
21 | .readTimeout(30, TimeUnit.SECONDS)
22 | .writeTimeout(30, TimeUnit.SECONDS)
23 | .build()
24 |
25 | private val retrofit: Retrofit = Retrofit.Builder()
26 | .baseUrl(BuildConfig.BACKEND_URL)
27 | .client(client)
28 | .addConverterFactory(GsonConverterFactory.create())
29 | .build()
30 |
31 | private val service: BackendService = retrofit.create(BackendService::class.java)
32 |
33 | @Throws(ConnectionTokenException::class)
34 | internal suspend fun createConnectionToken(
35 | canRetry: Boolean,
36 | ): String {
37 | return try {
38 | val result = service.getConnectionToken()
39 |
40 | result.secret.ifEmpty {
41 | throw ConnectionTokenException("Empty connection token.")
42 | }
43 | } catch (e: Exception) {
44 | when (e) {
45 | is SocketTimeoutException,
46 | is TimeoutException,
47 | is IOException -> {
48 | if (canRetry) {
49 | Log.e(Config.TAG, "Error while creating connection token, retrying.", e)
50 | createConnectionToken(canRetry = false)
51 | } else {
52 | throw ConnectionTokenException("Failed to create connection token.", e)
53 | }
54 | }
55 |
56 | else -> {
57 | throw ConnectionTokenException("Failed to create connection token.", e)
58 | }
59 | }
60 | }
61 | }
62 |
63 | suspend fun createPaymentIntent(createPaymentIntentParams: Map): Result =
64 | runCatching {
65 | val response = service.createPaymentIntent(createPaymentIntentParams.toMap())
66 | response ?: error("Failed to create PaymentIntent")
67 | }
68 |
69 | suspend fun updatePaymentIntent(updatePaymentIntentParams: Map): Result =
70 | runCatching {
71 | val response = service.updatePaymentIntent(updatePaymentIntentParams.toMap())
72 | response ?: error("Failed to update PaymentIntent")
73 | }
74 |
75 | suspend fun capturePaymentIntent(@Field("payment_intent_id") id: String): Result =
76 | runCatching {
77 | val response = service.capturePaymentIntent(id)
78 | response ?: error("Failed to capture PaymentIntent")
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/app/src/main/java/com/stripe/aod/sampleapp/network/BackendService.kt:
--------------------------------------------------------------------------------
1 | package com.stripe.aod.sampleapp.network
2 |
3 | import com.stripe.aod.sampleapp.data.PaymentIntentCreationResponse
4 | import com.stripe.aod.sampleapp.model.ConnectionToken
5 | import retrofit2.http.Field
6 | import retrofit2.http.FieldMap
7 | import retrofit2.http.FormUrlEncoded
8 | import retrofit2.http.POST
9 |
10 | /**
11 | * The `BackendService` interface handles the calls we need to make to our backend.
12 | */
13 | interface BackendService {
14 |
15 | /**
16 | * Get a connection token string from the backend
17 | */
18 | @POST("connection_token")
19 | suspend fun getConnectionToken(): ConnectionToken
20 |
21 | @FormUrlEncoded
22 | @POST("create_payment_intent")
23 | suspend fun createPaymentIntent(@FieldMap createPaymentIntentParams: Map): PaymentIntentCreationResponse?
24 |
25 | @FormUrlEncoded
26 | @POST("update_payment_intent")
27 | suspend fun updatePaymentIntent(@FieldMap updatePaymentIntentParams: Map): PaymentIntentCreationResponse?
28 |
29 | @FormUrlEncoded
30 | @POST("capture_payment_intent")
31 | suspend fun capturePaymentIntent(@Field("payment_intent_id") id: String): PaymentIntentCreationResponse?
32 | }
33 |
--------------------------------------------------------------------------------
/app/src/main/java/com/stripe/aod/sampleapp/network/TokenProvider.kt:
--------------------------------------------------------------------------------
1 | package com.stripe.aod.sampleapp.network
2 |
3 | import com.stripe.stripeterminal.external.callable.ConnectionTokenCallback
4 | import com.stripe.stripeterminal.external.callable.ConnectionTokenProvider
5 | import com.stripe.stripeterminal.external.models.ConnectionTokenException
6 | import kotlinx.coroutines.CoroutineScope
7 | import kotlinx.coroutines.launch
8 |
9 | /**
10 | * A simple implementation of the [ConnectionTokenProvider] interface. We just request a
11 | * new token from our backend simulator and forward any exceptions along to the SDK.
12 | */
13 | class TokenProvider(private val coroutineScope: CoroutineScope) : ConnectionTokenProvider {
14 | override fun fetchConnectionToken(callback: ConnectionTokenCallback) {
15 | coroutineScope.launch {
16 | try {
17 | val token = ApiClient.createConnectionToken(canRetry = true)
18 | callback.onSuccess(token)
19 | } catch (e: ConnectionTokenException) {
20 | callback.onFailure(e)
21 | }
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/stripe/aod/sampleapp/utils/ActivityExt.kt:
--------------------------------------------------------------------------------
1 | package com.stripe.aod.sampleapp.utils
2 |
3 | import android.content.Context
4 | import android.os.SystemClock
5 | import android.view.View
6 | import android.view.inputmethod.InputMethodManager
7 | import android.widget.EditText
8 | import androidx.fragment.app.Fragment
9 | import androidx.lifecycle.Lifecycle
10 | import androidx.lifecycle.lifecycleScope
11 | import androidx.lifecycle.repeatOnLifecycle
12 | import androidx.navigation.NavOptions
13 | import androidx.navigation.fragment.findNavController
14 | import com.stripe.aod.sampleapp.R
15 | import java.text.NumberFormat
16 | import java.util.Locale
17 | import kotlinx.coroutines.CoroutineScope
18 | import kotlinx.coroutines.Job
19 | import kotlinx.coroutines.launch
20 |
21 | fun navOptions(): NavOptions {
22 | return NavOptions.Builder()
23 | .setEnterAnim(R.anim.slide_right_in)
24 | .setExitAnim(R.anim.slide_left_out)
25 | .setPopEnterAnim(R.anim.slide_left_in)
26 | .setPopExitAnim(R.anim.slide_right_out)
27 | .build()
28 | }
29 |
30 | fun formatCentsToString(amount: Int): String {
31 | return NumberFormat.getCurrencyInstance(Locale.US).format(amount / 100.0)
32 | }
33 |
34 | inline fun Fragment.launchAndRepeatWithViewLifecycle(
35 | minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
36 | crossinline block: suspend CoroutineScope.() -> Unit
37 | ): Job {
38 | return viewLifecycleOwner.lifecycleScope.launch {
39 | viewLifecycleOwner.lifecycle.repeatOnLifecycle(minActiveState) {
40 | block()
41 | }
42 | }
43 | }
44 |
45 | fun Fragment.backToHome() {
46 | findNavController().navigate(
47 | R.id.homeFragment,
48 | null,
49 | NavOptions.Builder()
50 | .setPopUpTo(R.id.inputFragment, true)
51 | .build()
52 | )
53 | }
54 |
55 | fun EditText.hideKeyboard() {
56 | val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
57 | imm.hideSoftInputFromWindow(windowToken, 0)
58 | }
59 |
60 | inline fun View.setThrottleClickListener(
61 | intervalDuration: Long = 1000,
62 | crossinline block: (view: View) -> Unit
63 | ) {
64 | this.setOnClickListener(object : View.OnClickListener {
65 | private var lastClickTime: Long = 0
66 | override fun onClick(v: View) {
67 | if (SystemClock.elapsedRealtime() - lastClickTime >= intervalDuration) {
68 | block(v)
69 | lastClickTime = SystemClock.elapsedRealtime()
70 | }
71 | }
72 | })
73 | }
74 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_left_in.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_left_out.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_right_in.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_right_out.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/color/selector_btn_secondary_text.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/color/selector_btn_text.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/color/selector_textinputlayout_border.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-night/back.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-night/backspace.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-night/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-night/keyboard_active.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
13 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-night/keyboard_inactive.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
11 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-night/logo.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-night/selector_keyboard.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/back.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/backspace.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/btn_default.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
13 |
17 |
18 |
19 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/btn_disable.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
13 |
17 |
18 |
19 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/btn_pressed.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
13 |
17 |
18 |
19 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/btn_secondary_default.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
13 |
17 |
18 |
19 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/btn_secondary_disable.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
13 |
17 |
18 |
19 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/btn_secondary_pressed.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
13 |
17 |
18 |
19 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/email_input.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
14 |
18 |
19 |
20 |
23 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/keyboard_active.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
13 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/keyboard_inactive.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
11 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/logo.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/selector_btn.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/selector_keyboard.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/selector_secondary_btn.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_email.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
11 |
12 |
21 |
22 |
32 |
41 |
42 |
43 |
60 |
61 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_home.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
11 |
21 |
22 |
32 |
33 |
46 |
47 |
56 |
57 |
68 |
69 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_input.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
11 |
12 |
21 |
22 |
35 |
36 |
48 |
49 |
58 |
59 |
73 |
74 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_receipt.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
12 |
13 |
29 |
30 |
46 |
47 |
63 |
64 |
80 |
81 |
97 |
98 |
114 |
115 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/widget_keyboard.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
19 |
20 |
28 |
29 |
30 |
31 |
41 |
42 |
50 |
51 |
52 |
64 |
65 |
73 |
74 |
75 |
87 |
88 |
96 |
97 |
98 |
108 |
109 |
117 |
118 |
119 |
131 |
132 |
140 |
141 |
142 |
154 |
155 |
163 |
164 |
165 |
175 |
176 |
184 |
185 |
186 |
198 |
199 |
207 |
208 |
209 |
221 |
222 |
230 |
231 |
232 |
242 |
243 |
251 |
252 |
253 |
265 |
266 |
272 |
273 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stripe-samples/terminal-apps-on-devices/3e840a46b2f34a97f3a172dcc6552b5c5ee6359e/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stripe-samples/terminal-apps-on-devices/3e840a46b2f34a97f3a172dcc6552b5c5ee6359e/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stripe-samples/terminal-apps-on-devices/3e840a46b2f34a97f3a172dcc6552b5c5ee6359e/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stripe-samples/terminal-apps-on-devices/3e840a46b2f34a97f3a172dcc6552b5c5ee6359e/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stripe-samples/terminal-apps-on-devices/3e840a46b2f34a97f3a172dcc6552b5c5ee6359e/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/navigation/nav_graph.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
14 |
15 |
19 |
22 |
26 |
29 |
30 |
34 |
37 |
40 |
44 |
47 |
48 |
52 |
55 |
58 |
59 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
11 |
12 |
13 | #121212
14 |
15 |
16 | #FFFFFF
17 | #C0C8D2
18 | #FFFFFF
19 | #FFFFFF
20 | #FFFFFF
21 | #F6F8FA
22 | #80FFFFFF
23 | #F6F8FA
24 | #F6F8FA
25 | #80F6F8FA
26 | #F6F8FA
27 |
28 |
29 | #0570DE
30 | #03438E
31 | #03438E
32 | #1F1F1F
33 | #801F1F1F
34 | #1F1F1F
35 | #0570DE
36 |
37 | #E0E6EB
38 | #E0E6EB
39 |
40 |
41 | #635BFF
42 | #121212
43 | #ffffff
44 | #ffffff
45 |
46 |
47 | #1F1F1F
48 | #F6F8FA
49 | #635BFF
50 |
51 |
52 | #36373A
53 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
21 |
22 |
27 |
28 |
33 |
34 |
41 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
11 |
12 |
13 | #FFFFFF
14 |
15 |
16 | #1F1F1F
17 | #6A7383
18 | #36373A
19 | #FFFFFF
20 | #FFFFFF
21 | #FFFFFF
22 | #B3FFFFFF
23 | #1F1F1F
24 | #1F1F1F
25 | #36373A
26 | #6A7383
27 | #C0C8D2
28 | #1F1F1F
29 |
30 |
31 | #635BFF
32 | #453ECC
33 | #B3635BFF
34 | #EAEDEF
35 | #DCDEE1
36 | #B3EAEDEF
37 | #0570DE
38 | #778292
39 | #FFFFFF
40 |
41 |
42 | #635BFF
43 | #FFFFFF
44 | #36373A
45 | #36373A
46 |
47 |
48 | #10C210
49 | #777474
50 |
51 |
52 | #FFFFFF
53 | #1F1F1F
54 | #635BFF
55 |
56 |
57 | #C0C8D2
58 |
59 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Apps on Devices Sample App
4 |
5 | Terminal sample app
6 | New payment
7 |
8 | Please select a reader first!
9 |
10 | Reader connected!
11 | Reader offline!
12 | Unknown Reader
13 | Connected
14 |
15 | 1
16 | 2
17 | 3
18 | 4
19 | 5
20 | 6
21 | 7
22 | 8
23 | 9
24 | C
25 | 0
26 |
27 | Enter amount
28 | $0.00
29 | Request payment
30 | Checkout
31 | Amount entered
32 | Settings
33 | Stripe
34 | Back
35 |
36 | Failed to send receipt email
37 | Failed to create PaymentIntent
38 | Failed to capture PaymentIntent
39 |
40 | How would you like to receive a receipt?
41 | Email
42 | SMS
43 | Print
44 | No receipt
45 | Send
46 | "Invalid email"
47 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
21 |
22 |
27 |
28 |
33 |
34 |
43 |
44 |
51 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | id("com.android.application") version "8.7.3" apply false
4 | id("androidx.navigation.safeargs.kotlin") version "2.8.5" apply false
5 | id("org.jetbrains.kotlin.android") version "2.1.0" apply false
6 | }
7 |
8 | buildscript {
9 | dependencies {
10 | classpath(libs.secrets.gradle.plugin)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stripe-samples/terminal-apps-on-devices/3e840a46b2f34a97f3a172dcc6552b5c5ee6359e/demo.gif
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Enables namespacing of each library's R class so that its R class includes only the
19 | # resources declared in the library itself and none from the library's dependencies,
20 | # thereby reducing the size of the R class for that library
21 | android.nonTransitiveRClass=true
22 |
23 | # Disable AIDL compilation
24 | # See https://developer.android.com/reference/tools/gradle-api/7.0/com/android/build/api/dsl/BuildFeatures#aidl
25 | android.defaults.buildfeatures.aidl=false
26 |
27 | # Disable renderscript
28 | # See https://developer.android.com/reference/tools/gradle-api/7.0/com/android/build/api/dsl/BuildFeatures#renderscript
29 | android.defaults.buildfeatures.renderscript=false
30 |
31 | # Disable shaders
32 | # See https://developer.android.com/reference/tools/gradle-api/7.0/com/android/build/api/dsl/BuildFeatures#shaders
33 | android.defaults.buildfeatures.shaders=false
34 |
35 | # Disable resource processing for library modules
36 | # See https://developer.android.com/reference/tools/gradle-api/7.0/com/android/build/api/dsl/LibraryBuildFeatures#androidresources
37 | android.library.defaults.buildfeatures.androidresources=false
38 |
39 | android.nonFinalResIds=true
40 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | lifecycle-version = "2.8.7"
3 | navigation-version = "2.8.8"
4 | retrofit-version = "2.11.0"
5 | stripeterminal-version = "4.3.1"
6 |
7 | [libraries]
8 | androidx-appcompat = "androidx.appcompat:appcompat:1.7.0"
9 | androidx-core-ktx = "androidx.core:core-ktx:1.15.0"
10 | androidx-fragment-ktx = "androidx.fragment:fragment-ktx:1.8.6"
11 | androidx-swiperefreshlayout = "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
12 |
13 | androidx-lifecycle-livedata = { group = "androidx.lifecycle", name = "lifecycle-livedata", version.ref = "lifecycle-version" }
14 | androidx-lifecycle-viewmodel = { group = "androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "lifecycle-version" }
15 | androidx-lifecycle-runtime = { group = "androidx.lifecycle", name = "lifecycle-runtime", version.ref = "lifecycle-version" }
16 |
17 | androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigation-version" }
18 | androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigation-version" }
19 |
20 | android-material = "com.google.android.material:material:1.12.0"
21 |
22 | kotlinx-coroutines-android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1"
23 |
24 | okhttp = "com.squareup.okhttp3:okhttp:4.12.0"
25 |
26 | retrofit2-retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit-version" }
27 | retrofit2-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit-version" }
28 |
29 | secrets-gradle-plugin = { module = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin", version = "2.0.1" }
30 | stripe-terminal-core = { group = "com.stripe", name = "stripeterminal-core", version.ref = "stripeterminal-version" }
31 | stripe-terminal-handoffclient = { group = "com.stripe", name = "stripeterminal-handoffclient", version.ref = "stripeterminal-version" }
32 |
33 | [plugins]
34 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stripe-samples/terminal-apps-on-devices/3e840a46b2f34a97f3a172dcc6552b5c5ee6359e/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 | # SPDX-License-Identifier: Apache-2.0
19 | #
20 |
21 | ##############################################################################
22 | #
23 | # Gradle start up script for POSIX generated by Gradle.
24 | #
25 | # Important for running:
26 | #
27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
28 | # noncompliant, but you have some other compliant shell such as ksh or
29 | # bash, then to run this script, type that shell name before the whole
30 | # command line, like:
31 | #
32 | # ksh Gradle
33 | #
34 | # Busybox and similar reduced shells will NOT work, because this script
35 | # requires all of these POSIX shell features:
36 | # * functions;
37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
39 | # * compound commands having a testable exit status, especially «case»;
40 | # * various built-in commands including «command», «set», and «ulimit».
41 | #
42 | # Important for patching:
43 | #
44 | # (2) This script targets any POSIX shell, so it avoids extensions provided
45 | # by Bash, Ksh, etc; in particular arrays are avoided.
46 | #
47 | # The "traditional" practice of packing multiple parameters into a
48 | # space-separated string is a well documented source of bugs and security
49 | # problems, so this is (mostly) avoided, by progressively accumulating
50 | # options in "$@", and eventually passing that to Java.
51 | #
52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
54 | # see the in-line comments for details.
55 | #
56 | # There are tweaks for specific operating systems such as AIX, CygWin,
57 | # Darwin, MinGW, and NonStop.
58 | #
59 | # (3) This script is generated from the Groovy template
60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
61 | # within the Gradle project.
62 | #
63 | # You can find Gradle at https://github.com/gradle/gradle/.
64 | #
65 | ##############################################################################
66 |
67 | # Attempt to set APP_HOME
68 |
69 | # Resolve links: $0 may be a link
70 | app_path=$0
71 |
72 | # Need this for daisy-chained symlinks.
73 | while
74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
75 | [ -h "$app_path" ]
76 | do
77 | ls=$( ls -ld "$app_path" )
78 | link=${ls#*' -> '}
79 | case $link in #(
80 | /*) app_path=$link ;; #(
81 | *) app_path=$APP_HOME$link ;;
82 | esac
83 | done
84 |
85 | # This is normally unused
86 | # shellcheck disable=SC2034
87 | APP_BASE_NAME=${0##*/}
88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
90 | ' "$PWD" ) || exit
91 |
92 | # Use the maximum available, or set MAX_FD != -1 to use that value.
93 | MAX_FD=maximum
94 |
95 | warn () {
96 | echo "$*"
97 | } >&2
98 |
99 | die () {
100 | echo
101 | echo "$*"
102 | echo
103 | exit 1
104 | } >&2
105 |
106 | # OS specific support (must be 'true' or 'false').
107 | cygwin=false
108 | msys=false
109 | darwin=false
110 | nonstop=false
111 | case "$( uname )" in #(
112 | CYGWIN* ) cygwin=true ;; #(
113 | Darwin* ) darwin=true ;; #(
114 | MSYS* | MINGW* ) msys=true ;; #(
115 | NONSTOP* ) nonstop=true ;;
116 | esac
117 |
118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
119 |
120 |
121 | # Determine the Java command to use to start the JVM.
122 | if [ -n "$JAVA_HOME" ] ; then
123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
124 | # IBM's JDK on AIX uses strange locations for the executables
125 | JAVACMD=$JAVA_HOME/jre/sh/java
126 | else
127 | JAVACMD=$JAVA_HOME/bin/java
128 | fi
129 | if [ ! -x "$JAVACMD" ] ; then
130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
131 |
132 | Please set the JAVA_HOME variable in your environment to match the
133 | location of your Java installation."
134 | fi
135 | else
136 | JAVACMD=java
137 | if ! command -v java >/dev/null 2>&1
138 | then
139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
140 |
141 | Please set the JAVA_HOME variable in your environment to match the
142 | location of your Java installation."
143 | fi
144 | fi
145 |
146 | # Increase the maximum file descriptors if we can.
147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
148 | case $MAX_FD in #(
149 | max*)
150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
151 | # shellcheck disable=SC2039,SC3045
152 | MAX_FD=$( ulimit -H -n ) ||
153 | warn "Could not query maximum file descriptor limit"
154 | esac
155 | case $MAX_FD in #(
156 | '' | soft) :;; #(
157 | *)
158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
159 | # shellcheck disable=SC2039,SC3045
160 | ulimit -n "$MAX_FD" ||
161 | warn "Could not set maximum file descriptor limit to $MAX_FD"
162 | esac
163 | fi
164 |
165 | # Collect all arguments for the java command, stacking in reverse order:
166 | # * args from the command line
167 | # * the main class name
168 | # * -classpath
169 | # * -D...appname settings
170 | # * --module-path (only if needed)
171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
172 |
173 | # For Cygwin or MSYS, switch paths to Windows format before running java
174 | if "$cygwin" || "$msys" ; then
175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
177 |
178 | JAVACMD=$( cygpath --unix "$JAVACMD" )
179 |
180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
181 | for arg do
182 | if
183 | case $arg in #(
184 | -*) false ;; # don't mess with options #(
185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
186 | [ -e "$t" ] ;; #(
187 | *) false ;;
188 | esac
189 | then
190 | arg=$( cygpath --path --ignore --mixed "$arg" )
191 | fi
192 | # Roll the args list around exactly as many times as the number of
193 | # args, so each arg winds up back in the position where it started, but
194 | # possibly modified.
195 | #
196 | # NB: a `for` loop captures its iteration list before it begins, so
197 | # changing the positional parameters here affects neither the number of
198 | # iterations, nor the values presented in `arg`.
199 | shift # remove old arg
200 | set -- "$@" "$arg" # push replacement arg
201 | done
202 | fi
203 |
204 |
205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
207 |
208 | # Collect all arguments for the java command:
209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
210 | # and any embedded shellness will be escaped.
211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
212 | # treated as '${Hostname}' itself on the command line.
213 |
214 | set -- \
215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
216 | -classpath "$CLASSPATH" \
217 | org.gradle.wrapper.GradleWrapperMain \
218 | "$@"
219 |
220 | # Stop when "xargs" is not available.
221 | if ! command -v xargs >/dev/null 2>&1
222 | then
223 | die "xargs is not available"
224 | fi
225 |
226 | # Use "xargs" to parse quoted args.
227 | #
228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
229 | #
230 | # In Bash we could simply go:
231 | #
232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
233 | # set -- "${ARGS[@]}" "$@"
234 | #
235 | # but POSIX shell has neither arrays nor command substitution, so instead we
236 | # post-process each arg (as a line of input to sed) to backslash-escape any
237 | # character that might be a shell metacharacter, then use eval to reverse
238 | # that process (while maintaining the separation between arguments), and wrap
239 | # the whole thing up as a single "set" statement.
240 | #
241 | # This will of course break if any of these variables contains a newline or
242 | # an unmatched quote.
243 | #
244 |
245 | eval "set -- $(
246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
247 | xargs -n1 |
248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
249 | tr '\n' ' '
250 | )" '"$@"'
251 |
252 | exec "$JAVACMD" "$@"
253 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 |
9 | dependencyResolutionManagement {
10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
11 | repositories {
12 | google()
13 | mavenCentral()
14 | }
15 | }
16 |
17 | rootProject.name = "AOD_SampleApp"
18 | include(":app")
19 |
--------------------------------------------------------------------------------