path-terminal-init 0.2.9 → 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,410 +1,410 @@
1
- ---
2
- description: >
3
- Rules for AI agents integrating the Path Terminal SDK into an Android EPOS
4
- application. Apply when the user asks to integrate payments, connect to
5
- a payment terminal, wire a sale or refund, or set up the Path emulator.
6
- globs:
7
- - "**/*.kt"
8
- - "**/AndroidManifest.xml"
9
- - "**/settings.gradle.kts"
10
- - "**/build.gradle.kts"
11
- alwaysApply: false
12
- ---
13
-
14
- # Path Terminal SDK — Android Integration Rules
15
-
16
- You are integrating the Path Terminal SDK into an existing Android EPOS application.
17
- This app is a **developer EPOS system** that replicates a production environment.
18
-
19
- You have access to the Path MCP server at mcp.path2ai.tech:
20
- - Tools: `get_code_example` (use platform='android'), `validate_integration` (platform='android'),
21
- `get_integration_checklist` (platform='android'), `explain_error`, `get_manifest_requirements`
22
- - Always call MCP tools before writing code. Never guess at SDK patterns.
23
-
24
- > **Check whether the SDK is already wired before doing anything else.**
25
- > Run:
26
- > ```
27
- > grep -r "path-terminal-sdk\|PathTerminal\|BLEPathTerminalAdapter" settings.gradle.kts app/build.gradle.kts 2>/dev/null | wc -l
28
- > ```
29
- > - **Count > 0** — SDK already present. Proceed directly to Step 1.
30
- > - **Count = 0** — SDK not yet added. Add the Gradle composite build first (Step 0).
31
-
32
- > **Language override.** Never write "Pico W" in any output or code comment.
33
- > Always use "Path POS Emulator" when referring to the physical device.
34
-
35
- > **Confirm the correct project directory first.** Run `pwd` before doing
36
- > anything. If the path contains `.Trash`, `tmp`, or `worktree`, stop immediately.
37
- > Ask the developer to relaunch Claude Code from the correct project directory.
38
-
39
- ---
40
-
41
- ## Step 0 — Gradle setup (if SDK not yet installed)
42
-
43
- The Path Android SDK is served from the **Path Maven repo** — one URL gives you every
44
- module, **including the Verifone backend (`path-psdk-adapter`) and its `.aar`**, resolved
45
- transitively. **No sibling SDK checkout, no composite build, no JitPack.** (The
46
- `npx path-terminal-init` step usually adds these for you — verify they are present.)
47
-
48
- Call `get_code_example` with operation='install' and platform='android' for the exact lines.
49
-
50
- 1. **settings.gradle.kts** — add the Path Maven repo inside
51
- `dependencyResolutionManagement { repositories { … } }` (keep `google()` + `mavenCentral()`
52
- — they resolve the AndroidX / Kotlin transitives):
53
- ```kotlin
54
- maven { url = uri("https://mcp.path2ai.tech/maven") }
55
- ```
56
-
57
- 2. **app/build.gradle.kts** — add to `dependencies {}`:
58
- ```kotlin
59
- implementation("tech.path2ai.sdk:path-core-models:1.5.0")
60
- implementation("tech.path2ai.sdk:path-terminal-sdk:1.5.0")
61
- implementation("tech.path2ai.sdk:path-emulator-adapter:1.5.0")
62
- implementation("tech.path2ai.sdk:path-psdk-adapter:1.5.0") // Verifone backend (Step 2c)
63
- ```
64
- The Verifone backend's `.aar` uses data binding, so enable it in the same file:
65
- ```kotlin
66
- android { buildFeatures { dataBinding = true } }
67
- ```
68
-
69
- 3. **gradle.properties** — ensure `android.useAndroidX=true` (the SDK pulls AndroidX).
70
-
71
- 4. **AndroidManifest.xml** — call `get_manifest_requirements` for exact XML.
72
-
73
- ---
74
-
75
- ## Your job — four steps, in this order
76
-
77
- ---
78
-
79
- ### Step 1 — Disconnect the existing terminal wiring
80
-
81
- Find the app entry point (Application class, MainActivity, or DI root) and **read the actual code**.
82
-
83
- Check what adapter is currently injected into the terminal manager. A comment saying
84
- *"replace with Path adapter"* does not mean it has been done — read the code.
85
-
86
- **You are looking for this pattern:**
87
- ```kotlin
88
- // THIS is wired to Path — correct:
89
- terminalManager = TerminalManager(PathTerminalAdapter(this))
90
-
91
- // THIS is NOT wired to Path — fix it:
92
- terminalManager = TerminalManager(OCPayTerminalAdapter())
93
- terminalManager = TerminalManager() // default adapter
94
- ```
95
-
96
- If the call does not use `PathTerminalAdapter` (or equivalent Path SDK adapter), change it.
97
-
98
- > **The adapter class must implement the existing `PaymentTerminalAdapter` interface.**
99
- > Do not rename or remove the interface — it is the integration contract.
100
-
101
- **Also disable any auto-connect on launch** — find calls to `.connect()` in
102
- `onCreate`, `init`, or `onResume` that fire automatically. Remove them.
103
- Path connects manually via Settings, not on launch.
104
-
105
- ---
106
-
107
- ### Step 2 — Add a backend switcher to Settings
108
-
109
- The purpose of this integration is: **prove the payment functions against the Path
110
- POS Emulator, then let the user switch — themselves, in the app — to a real Verifone
111
- terminal when ready.** Settings must let the user pick *which* terminal to connect to.
112
- Do NOT hardcode one transport.
113
-
114
- > **Connection details are entered by the user in the app, not by you.** You wire the
115
- > switcher, the IP field, and the credential storage, ship sensible test defaults, and
116
- > describe how to use it in your end-of-run summary (Step 4). You do NOT ask the user
117
- > for an IP or password during this run.
118
-
119
- Find the Settings screen (a Composable or Activity named Settings, Preferences, or
120
- similar). Add a **Path POS Adapter** section with a backend picker offering **three**:
121
-
122
- | Backend | Transport | Adapter | Needs |
123
- |---|---|---|---|
124
- | **Emulator (Wi-Fi)** — *default selection* | TCP/IP | `TcpPathTerminalAdapter(host = …)` | emulator IP |
125
- | **Emulator (Bluetooth)** | BLE | `BLEPathTerminalAdapter(context = …)` | scan + pick device |
126
- | **Verifone** | TCP/IP (PSDK) | `VerifonePSDKAdapter(context, config)` | terminal IP + stored login |
127
-
128
- Mirror the demo app's proven pattern — **call `get_code_example` with operation
129
- ='backend-switch' and platform='android' first** and reproduce its `buildAdapter(backend)`
130
- shape. All sale / refund / void / receipt code is backend-agnostic; only adapter
131
- construction differs.
132
-
133
- **Persist the user's selection and connection details** in `SharedPreferences` behind a
134
- small settings type, with test defaults so a fresh clone connects without typing anything:
135
- ```kotlin
136
- enum class TerminalBackend { EMULATOR_WIFI, EMULATOR_BLE, VERIFONE } // default EMULATOR_WIFI
137
- // Stored keys: backend, emulator_host, verifone_host,
138
- // login_username, login_password, login_shift, refund_password
139
- // Test defaults: verifone_host "192.168.1.88", username "user",
140
- // password "password123", shift "shift123"
141
- ```
142
-
143
- #### Step 2a — Emulator over Wi-Fi (the default) and Verifone — IP entry
144
-
145
- For the two IP-addressed backends, show a **host/IP text field** (label it "Emulator IP"
146
- or "Verifone terminal IP" depending on the selection) plus an **Apply & Connect** button.
147
- On connect, build the matching adapter from the stored host and rebuild `PathTerminal`
148
- (disconnect the previous adapter first — real terminals and the emulator's Wi-Fi mode
149
- allow only **one** client at a time). TCP and Verifone need no scan — `discoverDevices()`
150
- returns one synthetic device for the configured host, so "Apply & Connect" connects directly.
151
-
152
- #### Step 2b — Emulator over Bluetooth — scan and connect
153
-
154
- > **⚠ Runtime permissions are MANDATORY — this is the #1 reason Bluetooth silently
155
- > fails.** Declaring `BLUETOOTH_SCAN` / `BLUETOOTH_CONNECT` in the manifest is **not
156
- > enough** on Android 12+ (API 31). You must **request them at runtime** before the
157
- > first scan, or `discoverDevices()` finds nothing / throws and the user sees an empty
158
- > list with no error. Call `get_code_example` with operation='backend-switch' and
159
- > platform='android' and reproduce its `rememberBlePermission()` pattern:
160
- > - Use `ActivityResultContracts.RequestMultiplePermissions()` to request
161
- > `BLUETOOTH_SCAN` **and** `BLUETOOTH_CONNECT` (guarded by `Build.VERSION.SDK_INT >= S`).
162
- > - Show a **"Grant Bluetooth permission"** button when not yet granted, and **gate the
163
- > Scan button** on the granted state — don't let the user scan without permission.
164
- > - Keep `android:usesPermissionFlags="neverForLocation"` on `BLUETOOTH_SCAN` (call
165
- > `get_manifest_requirements`) so `ACCESS_FINE_LOCATION` is not also required.
166
- > - Surface **"Bluetooth is turned off"** — `discoverDevices()` throws
167
- > `PathError(CONNECTIVITY)` when the radio is disabled. The emulator must be in
168
- > Bluetooth mode and advertise a name containing "Path".
169
-
170
- For the BLE backend keep the scan/connect flow:
171
- 1. **Grant Bluetooth permission** (runtime, per the callout above) — required before scanning
172
- 2. **Scan for Path Terminals** — triggers BLE discovery (only enabled once permission is granted)
173
- 3. Discovered devices list — each with a **Connect** button
174
- 4. Live connection state (Scanning / Connected / Disconnected)
175
- 5. **Disconnect** when connected
176
-
177
- **Add to `PaymentTerminalAdapter` interface** (with no-op default implementations):
178
- ```kotlin
179
- suspend fun scanForDevices(): List<TerminalDeviceInfo>
180
- suspend fun connectToDevice(id: String)
181
- ```
182
-
183
- **Implement `PathTerminalAdapter`** — call `get_code_example` with operation='discover'
184
- and platform='android' first. The SDK exposes `DiscoveredDevice` with `.id` and `.name`
185
- properties — use them directly.
186
-
187
- **Expose on the ViewModel:**
188
- ```kotlin
189
- private val _discoveredDevices = MutableStateFlow<List<TerminalDeviceInfo>>(emptyList())
190
- val discoveredDevices: StateFlow<List<TerminalDeviceInfo>> = _discoveredDevices.asStateFlow()
191
-
192
- private val _isScanning = MutableStateFlow(false)
193
- val isScanning: StateFlow<Boolean> = _isScanning.asStateFlow()
194
-
195
- fun scanForDevices() {
196
- viewModelScope.launch {
197
- _isScanning.value = true
198
- _discoveredDevices.value = adapter.scanForDevices()
199
- _isScanning.value = false
200
- }
201
- }
202
- ```
203
-
204
- #### Step 2c — Verifone backend and stored credentials
205
-
206
- The Verifone backend connects to a real terminal over the PSDK. Call `get_code_example`
207
- with operation='verifone-init' and platform='android' first. Construct it from a
208
- `VerifoneTerminalConfig` built from the **stored** login fields — never hardcode
209
- credentials inline:
210
- ```kotlin
211
- import tech.path2ai.sdk.psdk.VerifonePSDKAdapter
212
- import tech.path2ai.sdk.psdk.VerifoneTerminalConfig
213
-
214
- val adapter = VerifonePSDKAdapter(
215
- context = context,
216
- config = VerifoneTerminalConfig(
217
- host = settings.verifoneHost,
218
- username = settings.loginUsername,
219
- password = settings.loginPassword,
220
- shift = settings.loginShift,
221
- refundPassword = settings.refundPassword
222
- )
223
- )
224
- ```
225
- Keep the Verifone login fields **separate from the emulator settings** (their own stored
226
- keys, their own editable fields) — cleaner than reusing the emulator's. All terminals are
227
- treated as **generic Verifone** (same AGPA app, same PSDK connect/login), so no
228
- per-terminal configuration is needed beyond IP + login.
229
-
230
- ---
231
-
232
- ### Step 3 — Wire sale and refund through the Path SDK
233
-
234
- Call `get_code_example` with operation='sale' and platform='android'.
235
- Call `get_code_example` with operation='refund' and platform='android'.
236
-
237
- **Implement `PathTerminalAdapter.submitSale()`:**
238
- ```kotlin
239
- override suspend fun submitSale(request: TerminalSaleRequest): TerminalSaleResponse {
240
- val envelope = RequestEnvelope.create(sdkVersion = "0.1.0", adapterVersion = "0.1.0")
241
- val sdkRequest = TransactionRequest.sale(
242
- amountMinor = request.amountPence,
243
- currency = request.currencyCode,
244
- envelope = envelope
245
- )
246
- val result = terminal.sale(sdkRequest)
247
- return when (result.state) {
248
- TransactionState.APPROVED -> TerminalSaleResponse(
249
- authorised = true,
250
- authorisationCode = result.cardLastFour, // map fields as needed
251
- terminalReference = result.transactionId
252
- )
253
- else -> TerminalSaleResponse(
254
- authorised = false,
255
- failureReason = result.error?.message ?: result.state.name
256
- )
257
- }
258
- }
259
- ```
260
-
261
- **For refunds**, the success state is `TransactionState.REFUNDED` — **not** `APPROVED`.
262
- Using `APPROVED` in a refund check will never match.
263
-
264
- Call `validate_integration` with platform='android' on your code before finalising.
265
-
266
- ---
267
-
268
- ### Step 3b — Merchant logo on the customer display (idle branding)
269
-
270
- The demo can push a merchant logo to the terminal's **customer-facing display** (idle/
271
- attract mode). This is a real feature you MUST wire — it works on the **emulator (Wi-Fi
272
- and Bluetooth)** and on Verifone. Call `get_code_example` with operation='idle-branding'
273
- and platform='android' first, then reproduce the demo's proven pattern.
274
-
275
- **The SDK API** (on `PathTerminal`, a `suspend` function):
276
- ```kotlin
277
- suspend fun setIdleBranding(content: CustomerDisplayContent?)
278
- // content: raw PNG/JPEG bytes wrapped in a class — NOT a Bitmap/base64/resource id
279
- CustomerDisplayContent(imageBytes = logoBytes, caption = caption?.ifBlank { null })
280
- // pass null to CLEAR the logo (when the "show logo" toggle is off)
281
- ```
282
-
283
- **Add a "Customer Display" section to Settings** with:
284
- - A **"Show merchant logo"** toggle (persisted).
285
- - A **photo picker** — `ActivityResultContracts.GetContent()` launched with `"image/*"` —
286
- storing the chosen image **pre-scaled to ≤ 480px PNG** in private storage (so a fresh
287
- launch still has a logo). Ship a default logo asset so it works out of the box.
288
- - An optional **caption** field.
289
- - An **"Apply to terminal now"** button, **enabled only when `connectionState` is Connected**.
290
-
291
- > **CRITICAL — re-apply after every connect.** A fresh connection starts with a **blank**
292
- > display. The SDK re-pushes after each sale/refund, but the *first* show is yours: call
293
- > your `applyBranding()` (which calls `terminal.setIdleBranding(...)`) **right after every
294
- > successful `terminal.connect()`**, not just from the button. An integration that only
295
- > sets branding once, or never calls `setIdleBranding` at all, shows nothing.
296
-
297
- > **Best-effort + backend-gated.** `setIdleBranding` returns `Unit` and swallows backend
298
- > errors — **"no exception" does not prove the logo rendered**; confirm on the device.
299
- > Keep the Verifone payload small (a logo, not a full-res photo) — pre-scaling to ≤ 480px
300
- > PNG keeps the base64 under the ~32 000-char cap that would otherwise wedge the display.
301
-
302
- Call `validate_integration` with platform='android' before finalising.
303
-
304
- ---
305
-
306
- ### Step 3.5 — Compile the changes (check the environment first)
307
-
308
- Confirm your code compiles against the SDK's **real** API — this catches wrong method
309
- names / signatures the examples can't guarantee. But **probe the build toolchain first**,
310
- and treat a missing one as an environment limitation to advise on, **never as a failure**:
311
-
312
- 1. Probe (non-fatal — just checking):
313
- - `command -v java && java -version` — a JDK 17+ on PATH?
314
- - `echo "$ANDROID_HOME"`, `ls ~/Library/Android/sdk 2>/dev/null`, or an Android Studio
315
- install — is the Android SDK available?
316
-
317
- 2. **Toolchain present** → run `./gradlew assembleDebug` (or `compileDebugKotlin` for speed).
318
- Fix any real API mismatches, then continue to Step 4.
319
-
320
- 3. **Toolchain absent** → the integration code is complete; you simply can't compile *here*.
321
- Do **not** report this as an error or a failed install. Instead:
322
- - Check `command -v brew`. If Homebrew is available, **offer** (don't assume) to install the
323
- toolchain — `brew install openjdk@21` plus the Android command-line tools — explaining
324
- what it does and that it's optional.
325
- - If Homebrew is absent, or the developer would rather not, tell them plainly:
326
- *"The integration code is done — I can't compile in this environment. Open the project in
327
- Android Studio and build; paste back any errors and I'll fix them."* Then summarise every
328
- file you changed so they can review and build with confidence.
329
-
330
- Frame a missing build toolchain as **"here's how to verify"**, never as a failure of the work.
331
-
332
- ### Step 4 — Verify it works end to end
333
-
334
- 1. Confirm the Application/MainActivity injects `PathTerminalAdapter` (not the old adapter).
335
- 2. Confirm AndroidManifest.xml has BLUETOOTH_SCAN and BLUETOOTH_CONNECT permissions
336
- (with `neverForLocation` on SCAN) **and** that the app requests them at **runtime**
337
- before scanning — manifest-only is not enough on Android 12+ (Step 2b).
338
- 3. **Prove on the emulator first (the default path — Wi-Fi/IP):**
339
- - Put the Path POS Emulator in Wi-Fi mode (Config → Connection on the device);
340
- it shows its `IP:port` on the welcome screen.
341
- - Run the app → Settings → Payment Terminal → backend **Emulator (Wi-Fi)** → type
342
- that IP → **Apply & Connect**. connectionState should reach Connected.
343
- - (Optionally also prove **Emulator (Bluetooth)**: **Grant Bluetooth permission** when
344
- prompted, then select it, **Scan**, **Connect**. If Scan finds nothing, the runtime
345
- permission was almost certainly not granted/requested — recheck Step 2b.)
346
- 4. Add items to cart → Card payment → present a card / tap NFC tag on emulator when prompted.
347
- 5. Verify:
348
- - result.state == TransactionState.APPROVED (or result.isApproved == true)
349
- - result.transactionId is non-null
350
- - Receipt data populated via terminal.getReceiptData(transactionId)
351
- 5b. **Prove the merchant logo (Step 3b):** in Settings → Customer Display, pick a logo and
352
- tap **Apply to terminal now** while connected — confirm it appears on the emulator's
353
- customer display. Then disconnect/reconnect and confirm it re-appears (proves you
354
- re-apply after connect).
355
- 6. **Then prove the switch to Verifone:** select backend **Verifone**, enter the terminal's
356
- IP and confirm the stored login, **Apply & Connect** — the SDK runs the PSDK init + login
357
- and the same sale/refund/void flow runs against the real terminal, unchanged. (Only one
358
- client may connect to a Verifone terminal at a time — disconnect any other POS first.)
359
-
360
- ---
361
-
362
- ## SDK rules — always follow these
363
-
364
- **ALWAYS:**
365
- - Call `get_code_example` with platform='android' before writing any SDK code
366
- - Use `TransactionRequest.sale(...)` and `TransactionRequest.refund(...)` factory methods
367
- - Use `RequestEnvelope.create(sdkVersion, adapterVersion)` — never build manually
368
- - Handle TransactionState: APPROVED, DECLINED, TIMED_OUT, FAILED, CANCELLED
369
- - For **refunds**, success state is `REFUNDED` — not `APPROVED`
370
- - Catch `PathError` and check `.recoverable`
371
- - Request BLE runtime permissions (`BLUETOOTH_SCAN` + `BLUETOOTH_CONNECT`) before scanning —
372
- the manifest declaration alone does NOT work on Android 12+ (see Step 2b)
373
- - Re-apply idle branding (`terminal.setIdleBranding(CustomerDisplayContent(imageBytes, caption))`)
374
- after every successful `connect()`, not just from the button (see Step 3b)
375
- - Call `validate_integration` with platform='android' before presenting code
376
- - Call from coroutines — `terminal.sale()` and `terminal.refund()` are `suspend` functions
377
-
378
- **NEVER:**
379
- - Use the raw `TransactionRequest(...)` constructor — always use factory methods
380
- - Store or log card data (cardLastFour is acceptable, nothing else)
381
- - Retry a financial operation without a new `RequestEnvelope` (new idempotencyKey)
382
- - Leave the existing default adapter in place — Path must be injected
383
- - Leave auto-connect-on-launch running
384
- - Skip `PathError` handling
385
- - Call blocking I/O from the main thread — all SDK calls are suspend functions
386
- - Wrap `terminal.sale()` in a timeout shorter than 30 seconds
387
-
388
- **AMOUNTS:** Minor currency units (pence/cents) only as integers.
389
- - `100` = £1.00 GBP · `1250` = £12.50 GBP · Never pass decimals.
390
-
391
- **COROUTINES:** All SDK methods are `suspend`. Always call from `viewModelScope.launch { }` or equivalent.
392
-
393
- ---
394
-
395
- ## After making changes
396
-
397
- Summarise:
398
- - Each file modified and the specific change made
399
- - Confirm Step 1 (adapter injection) was completed
400
-
401
- Then give **tailored connect instructions** — the user enters the connection details
402
- themselves, so tell them exactly where you wired things in:
403
- - The Settings screen / section name you added the backend switcher to
404
- - How to connect to the emulator over **Wi-Fi/IP** (the default): which backend to pick,
405
- where to type the emulator's IP, which button connects
406
- - How to also connect over **Bluetooth** if they prefer (Scan → Connect)
407
- - How to **switch to Verifone** when ready: pick Verifone, enter the terminal IP, confirm
408
- the stored login, connect
409
-
410
- Keep it brief and concrete — the user should be able to follow it without reading code.
1
+ ---
2
+ description: >
3
+ Rules for AI agents integrating the Path Terminal SDK into an Android EPOS
4
+ application. Apply when the user asks to integrate payments, connect to
5
+ a payment terminal, wire a sale or refund, or set up the Path emulator.
6
+ globs:
7
+ - "**/*.kt"
8
+ - "**/AndroidManifest.xml"
9
+ - "**/settings.gradle.kts"
10
+ - "**/build.gradle.kts"
11
+ alwaysApply: false
12
+ ---
13
+
14
+ # Path Terminal SDK — Android Integration Rules
15
+
16
+ You are integrating the Path Terminal SDK into an existing Android EPOS application.
17
+ This app is a **developer EPOS system** that replicates a production environment.
18
+
19
+ You have access to the Path MCP server at mcp.path2ai.tech:
20
+ - Tools: `get_code_example` (use platform='android'), `validate_integration` (platform='android'),
21
+ `get_integration_checklist` (platform='android'), `explain_error`, `get_manifest_requirements`
22
+ - Always call MCP tools before writing code. Never guess at SDK patterns.
23
+
24
+ > **Check whether the SDK is already wired before doing anything else.**
25
+ > Run:
26
+ > ```
27
+ > grep -r "path-terminal-sdk\|PathTerminal\|BLEPathTerminalAdapter" settings.gradle.kts app/build.gradle.kts 2>/dev/null | wc -l
28
+ > ```
29
+ > - **Count > 0** — SDK already present. Proceed directly to Step 1.
30
+ > - **Count = 0** — SDK not yet added. Add the Gradle composite build first (Step 0).
31
+
32
+ > **Language override.** Never write "Pico W" in any output or code comment.
33
+ > Always use "Path POS Emulator" when referring to the physical device.
34
+
35
+ > **Confirm the correct project directory first.** Run `pwd` before doing
36
+ > anything. If the path contains `.Trash`, `tmp`, or `worktree`, stop immediately.
37
+ > Ask the developer to relaunch Claude Code from the correct project directory.
38
+
39
+ ---
40
+
41
+ ## Step 0 — Gradle setup (if SDK not yet installed)
42
+
43
+ The Path Android SDK is served from the **Path Maven repo** — one URL gives you every
44
+ module, **including the Verifone backend (`path-psdk-adapter`) and its `.aar`**, resolved
45
+ transitively. **No sibling SDK checkout, no composite build, no JitPack.** (The
46
+ `npx path-terminal-init` step usually adds these for you — verify they are present.)
47
+
48
+ Call `get_code_example` with operation='install' and platform='android' for the exact lines.
49
+
50
+ 1. **settings.gradle.kts** — add the Path Maven repo inside
51
+ `dependencyResolutionManagement { repositories { … } }` (keep `google()` + `mavenCentral()`
52
+ — they resolve the AndroidX / Kotlin transitives):
53
+ ```kotlin
54
+ maven { url = uri("https://mcp.path2ai.tech/maven") }
55
+ ```
56
+
57
+ 2. **app/build.gradle.kts** — add to `dependencies {}`:
58
+ ```kotlin
59
+ implementation("tech.path2ai.sdk:path-core-models:1.6.0")
60
+ implementation("tech.path2ai.sdk:path-terminal-sdk:1.6.0")
61
+ implementation("tech.path2ai.sdk:path-emulator-adapter:1.6.0")
62
+ implementation("tech.path2ai.sdk:path-psdk-adapter:1.6.0") // Verifone backend (Step 2c)
63
+ ```
64
+ The Verifone backend's `.aar` uses data binding, so enable it in the same file:
65
+ ```kotlin
66
+ android { buildFeatures { dataBinding = true } }
67
+ ```
68
+
69
+ 3. **gradle.properties** — ensure `android.useAndroidX=true` (the SDK pulls AndroidX).
70
+
71
+ 4. **AndroidManifest.xml** — call `get_manifest_requirements` for exact XML.
72
+
73
+ ---
74
+
75
+ ## Your job — four steps, in this order
76
+
77
+ ---
78
+
79
+ ### Step 1 — Disconnect the existing terminal wiring
80
+
81
+ Find the app entry point (Application class, MainActivity, or DI root) and **read the actual code**.
82
+
83
+ Check what adapter is currently injected into the terminal manager. A comment saying
84
+ *"replace with Path adapter"* does not mean it has been done — read the code.
85
+
86
+ **You are looking for this pattern:**
87
+ ```kotlin
88
+ // THIS is wired to Path — correct:
89
+ terminalManager = TerminalManager(PathTerminalAdapter(this))
90
+
91
+ // THIS is NOT wired to Path — fix it:
92
+ terminalManager = TerminalManager(OCPayTerminalAdapter())
93
+ terminalManager = TerminalManager() // default adapter
94
+ ```
95
+
96
+ If the call does not use `PathTerminalAdapter` (or equivalent Path SDK adapter), change it.
97
+
98
+ > **The adapter class must implement the existing `PaymentTerminalAdapter` interface.**
99
+ > Do not rename or remove the interface — it is the integration contract.
100
+
101
+ **Also disable any auto-connect on launch** — find calls to `.connect()` in
102
+ `onCreate`, `init`, or `onResume` that fire automatically. Remove them.
103
+ Path connects manually via Settings, not on launch.
104
+
105
+ ---
106
+
107
+ ### Step 2 — Add a backend switcher to Settings
108
+
109
+ The purpose of this integration is: **prove the payment functions against the Path
110
+ POS Emulator, then let the user switch — themselves, in the app — to a real Verifone
111
+ terminal when ready.** Settings must let the user pick *which* terminal to connect to.
112
+ Do NOT hardcode one transport.
113
+
114
+ > **Connection details are entered by the user in the app, not by you.** You wire the
115
+ > switcher, the IP field, and the credential storage, ship sensible test defaults, and
116
+ > describe how to use it in your end-of-run summary (Step 4). You do NOT ask the user
117
+ > for an IP or password during this run.
118
+
119
+ Find the Settings screen (a Composable or Activity named Settings, Preferences, or
120
+ similar). Add a **Path POS Adapter** section with a backend picker offering **three**:
121
+
122
+ | Backend | Transport | Adapter | Needs |
123
+ |---|---|---|---|
124
+ | **Emulator (Wi-Fi)** — *default selection* | TCP/IP | `TcpPathTerminalAdapter(host = …)` | emulator IP |
125
+ | **Emulator (Bluetooth)** | BLE | `BLEPathTerminalAdapter(context = …)` | scan + pick device |
126
+ | **Verifone** | TCP/IP (PSDK) | `VerifonePSDKAdapter(context, config)` | terminal IP + stored login |
127
+
128
+ Mirror the demo app's proven pattern — **call `get_code_example` with operation
129
+ ='backend-switch' and platform='android' first** and reproduce its `buildAdapter(backend)`
130
+ shape. All sale / refund / void / receipt code is backend-agnostic; only adapter
131
+ construction differs.
132
+
133
+ **Persist the user's selection and connection details** in `SharedPreferences` behind a
134
+ small settings type, with test defaults so a fresh clone connects without typing anything:
135
+ ```kotlin
136
+ enum class TerminalBackend { EMULATOR_WIFI, EMULATOR_BLE, VERIFONE } // default EMULATOR_WIFI
137
+ // Stored keys: backend, emulator_host, verifone_host,
138
+ // login_username, login_password, login_shift, refund_password
139
+ // Test defaults: verifone_host "192.168.1.88", username "user",
140
+ // password "password123", shift "shift123"
141
+ ```
142
+
143
+ #### Step 2a — Emulator over Wi-Fi (the default) and Verifone — IP entry
144
+
145
+ For the two IP-addressed backends, show a **host/IP text field** (label it "Emulator IP"
146
+ or "Verifone terminal IP" depending on the selection) plus an **Apply & Connect** button.
147
+ On connect, build the matching adapter from the stored host and rebuild `PathTerminal`
148
+ (disconnect the previous adapter first — real terminals and the emulator's Wi-Fi mode
149
+ allow only **one** client at a time). TCP and Verifone need no scan — `discoverDevices()`
150
+ returns one synthetic device for the configured host, so "Apply & Connect" connects directly.
151
+
152
+ #### Step 2b — Emulator over Bluetooth — scan and connect
153
+
154
+ > **⚠ Runtime permissions are MANDATORY — this is the #1 reason Bluetooth silently
155
+ > fails.** Declaring `BLUETOOTH_SCAN` / `BLUETOOTH_CONNECT` in the manifest is **not
156
+ > enough** on Android 12+ (API 31). You must **request them at runtime** before the
157
+ > first scan, or `discoverDevices()` finds nothing / throws and the user sees an empty
158
+ > list with no error. Call `get_code_example` with operation='backend-switch' and
159
+ > platform='android' and reproduce its `rememberBlePermission()` pattern:
160
+ > - Use `ActivityResultContracts.RequestMultiplePermissions()` to request
161
+ > `BLUETOOTH_SCAN` **and** `BLUETOOTH_CONNECT` (guarded by `Build.VERSION.SDK_INT >= S`).
162
+ > - Show a **"Grant Bluetooth permission"** button when not yet granted, and **gate the
163
+ > Scan button** on the granted state — don't let the user scan without permission.
164
+ > - Keep `android:usesPermissionFlags="neverForLocation"` on `BLUETOOTH_SCAN` (call
165
+ > `get_manifest_requirements`) so `ACCESS_FINE_LOCATION` is not also required.
166
+ > - Surface **"Bluetooth is turned off"** — `discoverDevices()` throws
167
+ > `PathError(CONNECTIVITY)` when the radio is disabled. The emulator must be in
168
+ > Bluetooth mode and advertise a name containing "Path".
169
+
170
+ For the BLE backend keep the scan/connect flow:
171
+ 1. **Grant Bluetooth permission** (runtime, per the callout above) — required before scanning
172
+ 2. **Scan for Path Terminals** — triggers BLE discovery (only enabled once permission is granted)
173
+ 3. Discovered devices list — each with a **Connect** button
174
+ 4. Live connection state (Scanning / Connected / Disconnected)
175
+ 5. **Disconnect** when connected
176
+
177
+ **Add to `PaymentTerminalAdapter` interface** (with no-op default implementations):
178
+ ```kotlin
179
+ suspend fun scanForDevices(): List<TerminalDeviceInfo>
180
+ suspend fun connectToDevice(id: String)
181
+ ```
182
+
183
+ **Implement `PathTerminalAdapter`** — call `get_code_example` with operation='discover'
184
+ and platform='android' first. The SDK exposes `DiscoveredDevice` with `.id` and `.name`
185
+ properties — use them directly.
186
+
187
+ **Expose on the ViewModel:**
188
+ ```kotlin
189
+ private val _discoveredDevices = MutableStateFlow<List<TerminalDeviceInfo>>(emptyList())
190
+ val discoveredDevices: StateFlow<List<TerminalDeviceInfo>> = _discoveredDevices.asStateFlow()
191
+
192
+ private val _isScanning = MutableStateFlow(false)
193
+ val isScanning: StateFlow<Boolean> = _isScanning.asStateFlow()
194
+
195
+ fun scanForDevices() {
196
+ viewModelScope.launch {
197
+ _isScanning.value = true
198
+ _discoveredDevices.value = adapter.scanForDevices()
199
+ _isScanning.value = false
200
+ }
201
+ }
202
+ ```
203
+
204
+ #### Step 2c — Verifone backend and stored credentials
205
+
206
+ The Verifone backend connects to a real terminal over the PSDK. Call `get_code_example`
207
+ with operation='verifone-init' and platform='android' first. Construct it from a
208
+ `VerifoneTerminalConfig` built from the **stored** login fields — never hardcode
209
+ credentials inline:
210
+ ```kotlin
211
+ import tech.path2ai.sdk.psdk.VerifonePSDKAdapter
212
+ import tech.path2ai.sdk.psdk.VerifoneTerminalConfig
213
+
214
+ val adapter = VerifonePSDKAdapter(
215
+ context = context,
216
+ config = VerifoneTerminalConfig(
217
+ host = settings.verifoneHost,
218
+ username = settings.loginUsername,
219
+ password = settings.loginPassword,
220
+ shift = settings.loginShift,
221
+ refundPassword = settings.refundPassword
222
+ )
223
+ )
224
+ ```
225
+ Keep the Verifone login fields **separate from the emulator settings** (their own stored
226
+ keys, their own editable fields) — cleaner than reusing the emulator's. All terminals are
227
+ treated as **generic Verifone** (same AGPA app, same PSDK connect/login), so no
228
+ per-terminal configuration is needed beyond IP + login.
229
+
230
+ ---
231
+
232
+ ### Step 3 — Wire sale and refund through the Path SDK
233
+
234
+ Call `get_code_example` with operation='sale' and platform='android'.
235
+ Call `get_code_example` with operation='refund' and platform='android'.
236
+
237
+ **Implement `PathTerminalAdapter.submitSale()`:**
238
+ ```kotlin
239
+ override suspend fun submitSale(request: TerminalSaleRequest): TerminalSaleResponse {
240
+ val envelope = RequestEnvelope.create(sdkVersion = "0.1.0", adapterVersion = "0.1.0")
241
+ val sdkRequest = TransactionRequest.sale(
242
+ amountMinor = request.amountPence,
243
+ currency = request.currencyCode,
244
+ envelope = envelope
245
+ )
246
+ val result = terminal.sale(sdkRequest)
247
+ return when (result.state) {
248
+ TransactionState.APPROVED -> TerminalSaleResponse(
249
+ authorised = true,
250
+ authorisationCode = result.cardLastFour, // map fields as needed
251
+ terminalReference = result.transactionId
252
+ )
253
+ else -> TerminalSaleResponse(
254
+ authorised = false,
255
+ failureReason = result.error?.message ?: result.state.name
256
+ )
257
+ }
258
+ }
259
+ ```
260
+
261
+ **For refunds**, the success state is `TransactionState.REFUNDED` — **not** `APPROVED`.
262
+ Using `APPROVED` in a refund check will never match.
263
+
264
+ Call `validate_integration` with platform='android' on your code before finalising.
265
+
266
+ ---
267
+
268
+ ### Step 3b — Merchant logo on the customer display (idle branding)
269
+
270
+ The demo can push a merchant logo to the terminal's **customer-facing display** (idle/
271
+ attract mode). This is a real feature you MUST wire — it works on the **emulator (Wi-Fi
272
+ and Bluetooth)** and on Verifone. Call `get_code_example` with operation='idle-branding'
273
+ and platform='android' first, then reproduce the demo's proven pattern.
274
+
275
+ **The SDK API** (on `PathTerminal`, a `suspend` function):
276
+ ```kotlin
277
+ suspend fun setIdleBranding(content: CustomerDisplayContent?)
278
+ // content: raw PNG/JPEG bytes wrapped in a class — NOT a Bitmap/base64/resource id
279
+ CustomerDisplayContent(imageBytes = logoBytes, caption = caption?.ifBlank { null })
280
+ // pass null to CLEAR the logo (when the "show logo" toggle is off)
281
+ ```
282
+
283
+ **Add a "Customer Display" section to Settings** with:
284
+ - A **"Show merchant logo"** toggle (persisted).
285
+ - A **photo picker** — `ActivityResultContracts.GetContent()` launched with `"image/*"` —
286
+ storing the chosen image **pre-scaled to ≤ 480px PNG** in private storage (so a fresh
287
+ launch still has a logo). Ship a default logo asset so it works out of the box.
288
+ - An optional **caption** field.
289
+ - An **"Apply to terminal now"** button, **enabled only when `connectionState` is Connected**.
290
+
291
+ > **CRITICAL — re-apply after every connect.** A fresh connection starts with a **blank**
292
+ > display. The SDK re-pushes after each sale/refund, but the *first* show is yours: call
293
+ > your `applyBranding()` (which calls `terminal.setIdleBranding(...)`) **right after every
294
+ > successful `terminal.connect()`**, not just from the button. An integration that only
295
+ > sets branding once, or never calls `setIdleBranding` at all, shows nothing.
296
+
297
+ > **Best-effort + backend-gated.** `setIdleBranding` returns `Unit` and swallows backend
298
+ > errors — **"no exception" does not prove the logo rendered**; confirm on the device.
299
+ > Keep the Verifone payload small (a logo, not a full-res photo) — pre-scaling to ≤ 480px
300
+ > PNG keeps the base64 under the ~32 000-char cap that would otherwise wedge the display.
301
+
302
+ Call `validate_integration` with platform='android' before finalising.
303
+
304
+ ---
305
+
306
+ ### Step 3.5 — Compile the changes (check the environment first)
307
+
308
+ Confirm your code compiles against the SDK's **real** API — this catches wrong method
309
+ names / signatures the examples can't guarantee. But **probe the build toolchain first**,
310
+ and treat a missing one as an environment limitation to advise on, **never as a failure**:
311
+
312
+ 1. Probe (non-fatal — just checking):
313
+ - `command -v java && java -version` — a JDK 17+ on PATH?
314
+ - `echo "$ANDROID_HOME"`, `ls ~/Library/Android/sdk 2>/dev/null`, or an Android Studio
315
+ install — is the Android SDK available?
316
+
317
+ 2. **Toolchain present** → run `./gradlew assembleDebug` (or `compileDebugKotlin` for speed).
318
+ Fix any real API mismatches, then continue to Step 4.
319
+
320
+ 3. **Toolchain absent** → the integration code is complete; you simply can't compile *here*.
321
+ Do **not** report this as an error or a failed install. Instead:
322
+ - Check `command -v brew`. If Homebrew is available, **offer** (don't assume) to install the
323
+ toolchain — `brew install openjdk@21` plus the Android command-line tools — explaining
324
+ what it does and that it's optional.
325
+ - If Homebrew is absent, or the developer would rather not, tell them plainly:
326
+ *"The integration code is done — I can't compile in this environment. Open the project in
327
+ Android Studio and build; paste back any errors and I'll fix them."* Then summarise every
328
+ file you changed so they can review and build with confidence.
329
+
330
+ Frame a missing build toolchain as **"here's how to verify"**, never as a failure of the work.
331
+
332
+ ### Step 4 — Verify it works end to end
333
+
334
+ 1. Confirm the Application/MainActivity injects `PathTerminalAdapter` (not the old adapter).
335
+ 2. Confirm AndroidManifest.xml has BLUETOOTH_SCAN and BLUETOOTH_CONNECT permissions
336
+ (with `neverForLocation` on SCAN) **and** that the app requests them at **runtime**
337
+ before scanning — manifest-only is not enough on Android 12+ (Step 2b).
338
+ 3. **Prove on the emulator first (the default path — Wi-Fi/IP):**
339
+ - Put the Path POS Emulator in Wi-Fi mode (Config → Connection on the device);
340
+ it shows its `IP:port` on the welcome screen.
341
+ - Run the app → Settings → Payment Terminal → backend **Emulator (Wi-Fi)** → type
342
+ that IP → **Apply & Connect**. connectionState should reach Connected.
343
+ - (Optionally also prove **Emulator (Bluetooth)**: **Grant Bluetooth permission** when
344
+ prompted, then select it, **Scan**, **Connect**. If Scan finds nothing, the runtime
345
+ permission was almost certainly not granted/requested — recheck Step 2b.)
346
+ 4. Add items to cart → Card payment → present a card / tap NFC tag on emulator when prompted.
347
+ 5. Verify:
348
+ - result.state == TransactionState.APPROVED (or result.isApproved == true)
349
+ - result.transactionId is non-null
350
+ - Receipt data populated via terminal.getReceiptData(transactionId)
351
+ 5b. **Prove the merchant logo (Step 3b):** in Settings → Customer Display, pick a logo and
352
+ tap **Apply to terminal now** while connected — confirm it appears on the emulator's
353
+ customer display. Then disconnect/reconnect and confirm it re-appears (proves you
354
+ re-apply after connect).
355
+ 6. **Then prove the switch to Verifone:** select backend **Verifone**, enter the terminal's
356
+ IP and confirm the stored login, **Apply & Connect** — the SDK runs the PSDK init + login
357
+ and the same sale/refund/void flow runs against the real terminal, unchanged. (Only one
358
+ client may connect to a Verifone terminal at a time — disconnect any other POS first.)
359
+
360
+ ---
361
+
362
+ ## SDK rules — always follow these
363
+
364
+ **ALWAYS:**
365
+ - Call `get_code_example` with platform='android' before writing any SDK code
366
+ - Use `TransactionRequest.sale(...)` and `TransactionRequest.refund(...)` factory methods
367
+ - Use `RequestEnvelope.create(sdkVersion, adapterVersion)` — never build manually
368
+ - Handle TransactionState: APPROVED, DECLINED, TIMED_OUT, FAILED, CANCELLED
369
+ - For **refunds**, success state is `REFUNDED` — not `APPROVED`
370
+ - Catch `PathError` and check `.recoverable`
371
+ - Request BLE runtime permissions (`BLUETOOTH_SCAN` + `BLUETOOTH_CONNECT`) before scanning —
372
+ the manifest declaration alone does NOT work on Android 12+ (see Step 2b)
373
+ - Re-apply idle branding (`terminal.setIdleBranding(CustomerDisplayContent(imageBytes, caption))`)
374
+ after every successful `connect()`, not just from the button (see Step 3b)
375
+ - Call `validate_integration` with platform='android' before presenting code
376
+ - Call from coroutines — `terminal.sale()` and `terminal.refund()` are `suspend` functions
377
+
378
+ **NEVER:**
379
+ - Use the raw `TransactionRequest(...)` constructor — always use factory methods
380
+ - Store or log card data (cardLastFour is acceptable, nothing else)
381
+ - Retry a financial operation without a new `RequestEnvelope` (new idempotencyKey)
382
+ - Leave the existing default adapter in place — Path must be injected
383
+ - Leave auto-connect-on-launch running
384
+ - Skip `PathError` handling
385
+ - Call blocking I/O from the main thread — all SDK calls are suspend functions
386
+ - Wrap `terminal.sale()` in a timeout shorter than 30 seconds
387
+
388
+ **AMOUNTS:** Minor currency units (pence/cents) only as integers.
389
+ - `100` = £1.00 GBP · `1250` = £12.50 GBP · Never pass decimals.
390
+
391
+ **COROUTINES:** All SDK methods are `suspend`. Always call from `viewModelScope.launch { }` or equivalent.
392
+
393
+ ---
394
+
395
+ ## After making changes
396
+
397
+ Summarise:
398
+ - Each file modified and the specific change made
399
+ - Confirm Step 1 (adapter injection) was completed
400
+
401
+ Then give **tailored connect instructions** — the user enters the connection details
402
+ themselves, so tell them exactly where you wired things in:
403
+ - The Settings screen / section name you added the backend switcher to
404
+ - How to connect to the emulator over **Wi-Fi/IP** (the default): which backend to pick,
405
+ where to type the emulator's IP, which button connects
406
+ - How to also connect over **Bluetooth** if they prefer (Scan → Connect)
407
+ - How to **switch to Verifone** when ready: pick Verifone, enter the terminal IP, confirm
408
+ the stored login, connect
409
+
410
+ Keep it brief and concrete — the user should be able to follow it without reading code.