CSV → Google Sheets: File → Import → Upload. PDF → in the print dialog pick “Save as PDF” as the destination.
Published app: https://lead-dispatch-flow.base44.app · Backend function: stripeTerminal
| Goal | Build a thin native Android Capacitor shell that wraps the LIVE Base44 CRM URL and adds card-present payments (Tap to Pay on Android + M2 Bluetooth reader) via the Stripe Terminal Android SDK, exposed to the WebView through a JS bridge. |
| v1.2 changes | Updated for Stripe Terminal Android SDK 5.8.1 + Capacitor 7. v1.0 targeted Stripe 4.7.x / Capacitor 6 with renamed APIs (SessionTokenProvider → ConnectionTokenProvider; two-step collect+confirm → processPaymentIntent) and a main-thread network call that crashes Android. Fixed here. |
| Base44 side status | ALREADY implemented — backend `stripeTerminal` + web `NativeTapToPay.jsx`. This brief builds ONLY the native shell + plugin. |
| Scope | Do NOT add login screens, CRM UI, or business logic. All of that ships from Base44. |
| Shell | Capacitor 7 (Android only for now) |
| Language | Kotlin |
| Stripe SDK | Stripe Terminal Android SDK 5.8.1 (core + taptopay + bluetooth modules) |
| Min SDK | 26 (target Android 15 / API 35) |
| Coroutines | kotlinx-coroutines-android 1.8.1 |
| HTTP | OkHttp 4.12.0 (connection-token fetch, on a background dispatcher) |
| mkdir | mkdir othe-crm-shell && cd othe-crm-shell |
| init | npm init -y |
| install | npm install @capacitor/core@^7 @capacitor/cli@^7 @capacitor/android@^7 |
| cap init | npx cap init "OTHE CRM" "com.othe.crm" --web-dir=www |
| www | mkdir www && echo '<meta http-equiv="refresh" content="0; url=https://lead-dispatch-flow.base44.app">' > www/index.html |
| add android | npx cap add android |
| config.ts | appId: com.othe.crm; appName: OTHE CRM; webDir: www; server.url: https://lead-dispatch-flow.base44.app; server.cleartext: false; android.allowMixedContent: false; SplashScreen launchShowDuration: 1200 |
| core | implementation 'com.stripe:stripeterminal-core:5.8.1' |
| taptopay | implementation 'com.stripe:stripeterminal-taptopay:5.8.1' |
| bluetooth | implementation 'com.stripe:stripeterminal-bluetooth:5.8.1' |
| okhttp | implementation 'com.squareup.okhttp3:okhttp:4.12.0' |
| coroutines | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1' |
| variables.gradle | minSdkVersion=26; compileSdkVersion=35; targetSdkVersion=35; kotlinVersion=1.9.25 |
| repos | mavenCentral() (default) |
| secrets.properties | android/app/secrets.properties (gitignored): BASE44_SERVICE_TOKEN=PASTE_THE_BASE44_SERVICE_TOKEN_VALUE_HERE |
| BuildConfig | defaultConfig: read secrets.properties -> buildConfigField "String", "BASE44_SERVICE_TOKEN", "<value>" (never hardcode in source) |
| INTERNET | <uses-permission android:name="android.permission.INTERNET" /> |
| NFC | <uses-permission android:name="android.permission.NFC" /> + <uses-feature android:name="android.hardware.nfc" android:required="false" /> |
| BT legacy | <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" /> + BLUETOOTH_ADMIN |
| BT modern | <uses-permission android:name="android.permission.BLUETOOTH_SCAN" /> + BLUETOOTH_CONNECT |
| Location | <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> |
| TokenProvider | ConnectionTokenProvider (NOT SessionTokenProvider). Implement fetchConnectionToken(callback). callback.onSuccess(secret) | callback.onFailure(ConnectionTokenException(...)) |
| Threading | fetchConnectionToken runs on the SDK init thread — may be the UI thread. Network there throws NetworkOnMainThreadException and Android kills the app. ALWAYS wrap the HTTP call in withContext(Dispatchers.IO){…}. |
| Init | Terminal.init(context, tokenProvider, listener, logger) once — from Application.onCreate via TerminalApplicationDelegate.onCreate(this), or lazily on first plugin use. |
| Unified payment | processPaymentIntent(intent, config, callback) (v5) replaces two-step collectPaymentMethod + confirmPaymentIntent. Two-step still compiles but processPaymentIntent is preferred. |
| PaymentIntentParameters | PaymentIntentParameters.Builder(amount, currency).setCaptureMethod(CaptureMethod.AUTOMATIC).build() |
| Connection check | Terminal.getInstance().connectionStatus == ConnectionStatus.CONNECTED |
| Imports | external.models: ConnectionTokenProvider, ConnectionTokenException, PaymentIntentParameters, CaptureMethod, TerminalException, TapToPayDiscoveryConfiguration, BluetoothDiscoveryConfiguration, TapToPayConnectionConfiguration, BluetoothConnectionConfiguration. callable: Callback, Cancelable, PaymentIntentCallback, ReaderCallback, TerminalListener, ConnectionTokenCallback. |
| Interface (frozen) | window.StripeTerminal = { isAvailable(), connect({mode, locationId?, isSimulated?}), discoverReaders(), connectReader(serial), collectPayment({amount, currency}), cancelCollect(), getConnectionStatus(), disconnect(), onReadersDiscovered(readers) } |
| Errors | Reject as { code: string, message: string }. Amounts in cents. |
| Package | com.othe.crm.terminal |
| Annotation | @CapacitorPlugin(name = "StripeTerminal") |
| Methods | isAvailable, connect, discoverReaders, connectReader, collectPayment, cancelCollect, getConnectionStatus, disconnect |
| TokenProvider | object : ConnectionTokenProvider { fetchConnectionToken(callback) -> ioScope.launch { withContext(IO){ fetchConnectionToken() } } -> callback.onSuccess(secret) | onFailure(ConnectionTokenException) } |
| fetchConnectionToken | withContext(Dispatchers.IO): OkHttp POST stripeTerminal {action:get_connection_token} header x-base44-service-token: BuildConfig.BASE44_SERVICE_TOKEN -> {secret} |
| tap_to_pay connect | TapToPayDiscoveryConfiguration(isSimulated) -> discoverReaders -> auto-connect built-in reader with TapToPayConnectionConfiguration |
| m2 connect | BluetoothDiscoveryConfiguration(isSimulated) -> discoverReaders -> user selects -> connectReader(serial, BluetoothConnectionConfiguration(locationId)) |
| collect flow (v5) | createPaymentIntent(params) -> processPaymentIntent(intent, null, callback, cancelableCallback) -> return {paymentIntentId,status,amount,currency,card{brand,last4}} |
| discovery emit | onUpdateDiscoveredReaders -> evaluateJavascript(window.__stripeTerminalOnReaders?.(arr)) -> calls window.StripeTerminal.onReadersDiscovered |
| load() glue | Plugin load() installs window.__stripeTerminalOnReaders that forwards to window.StripeTerminal.onReadersDiscovered |
| MainActivity | registerPlugin(StripeTerminalPlugin::class.java) before super.onCreate |
| Import note | If Stripe renamed any symbol between 5.8.1 and a later 5.x patch, adjust the import — the call shapes above are correct for 5.8.1. |
| Runtime | Request Bluetooth + location on first M2 use; ensure NFC for Tap to Pay. Handle onRequestPermissionsResult. Re-ask on denial. |
| sync | npx cap sync android |
| AAB | Android Studio -> Generate Signed Bundle (AAB) -> upload to Google Play Console |
| Update cadence | ONE-TIME upload. CRM updates ship automatically via Base44 publish (WebView loads live URL). Re-upload AAB only on shell/bridge changes. |
| 1 | Load the LIVE Base44 URL — never bundle a static web copy (keeps CRM auto-updating). |
| 2 | Freeze the bridge interface — add methods, never rename/reshape existing ones. |
| 3 | Connection tokens stay server-side — shell only calls get_connection_token (auth via x-base44-service-token header = BASE44_SERVICE_TOKEN); capture + invoice updates happen in Base44 backend. |
| 4 | Never block the main thread — all OkHttp / network calls run on Dispatchers.IO; doing them on the SDK callback thread triggers NetworkOnMainThreadException. |
| get_connection_token | POST /functions/stripeTerminal {action:get_connection_token} header x-base44-service-token: <BASE44_SERVICE_TOKEN> -> {secret} (no user session; only action the shell calls directly) |
| capture_payment | POST /functions/stripeTerminal {action:capture_payment, payment_intent_id, invoice_id?, test_mode?} -> {status, payment_intent_id, amount, card{brand,last4}, invoice_updated} (called by web app, user session) |
| caller | Web app calls capture_payment AFTER collectPayment resolves succeeded. |
| 1 Simulated | isSimulated: true, test card 4242 4242 4242 4242. |
| 2 Tap to Pay live | After Stripe Terminal activated on the account. |
| 3 M2 live | Real reader, $1 test charge, refund from Stripe Dashboard. |
| 4 Fallback | Confirm web app degrades gracefully when bridge absent (browser). |
| AAB | Signed AAB ready for Google Play. |
| Source | Capacitor shell source committed to its own repo (secrets.properties gitignored). |
| No web changes | All CRM/web changes ship from Base44. |