├── .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 | [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](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 | ![Screenshot 2024-07-23 at 9 38 59 AM](https://github.com/user-attachments/assets/0a58ef35-69d4-4b8c-9876-df74e393d04f) 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 | --------------------------------------------------------------------------------