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. |
| 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 6 (Android only for now) |
| Language | Kotlin |
| Stripe SDK | Stripe Terminal Android SDK 4.7.x (core + taptopay + bluetooth modules) |
| Min SDK | 26 |
| Coroutines | kotlinx-coroutines-android 1.8.1 |
| HTTP | OkHttp 4.12.0 (connection token fetch) |
| mkdir | mkdir othe-crm-shell && cd othe-crm-shell |
| init | npm init -y |
| install | npm install @capacitor/core @capacitor/cli @capacitor/android |
| 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:4.7.0' |
| taptopay | implementation 'com.stripe:stripeterminal-taptopay:4.7.0' |
| bluetooth | implementation 'com.stripe:stripeterminal-bluetooth:4.7.0' |
| okhttp | implementation 'com.squareup.okhttp3:okhttp:4.12.0' |
| coroutines | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1' |
| repos | mavenCentral() (default) |
| 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" /> |
| 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 | SessionTokenProvider.fetchToken → POST stripeTerminal {action:get_connection_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 | createPaymentIntent(amount,currency,AUTOMATIC) → collectPaymentMethod → processPayment → return {paymentIntentId,status,amount,currency,card{brand,last4}} |
| discovery emit | onUpdateDiscoveredReaders → evaluateJavascript(window.__stripeTerminalOnReaders?.(arr)) → calls window.StripeTerminal.onReadersDiscovered |
| MainActivity | registerPlugin(StripeTerminalPlugin::class.java) before super.onCreate |
| Imports note | TapToPayConnectionConfiguration, BluetoothConnectionConfiguration, PaymentIntentParameters, CaptureMethod, TerminalException, JSArray — adjust to exact 4.7.x names |
| 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; capture + invoice updates happen in Base44 backend. |
| get_connection_token | POST /functions/stripeTerminal {action:get_connection_token} → {secret} |
| 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} |
| 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. |
| No web changes | All CRM/web changes ship from Base44. |