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.
- package/README.md +93 -80
- package/dist/cli.js +15 -10
- package/dist/init.js +14 -9
- package/package.json +1 -1
- package/rules/android/path-integration-android.mdc +410 -410
- package/rules/ios/path-integration.mdc +456 -455
- package/rules/windows-10/path-integration-windows10.mdc +300 -300
- package/rules/windows-11/path-integration-windows11.mdc +294 -294
|
@@ -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.
|
|
60
|
-
implementation("tech.path2ai.sdk:path-terminal-sdk:1.
|
|
61
|
-
implementation("tech.path2ai.sdk:path-emulator-adapter:1.
|
|
62
|
-
implementation("tech.path2ai.sdk:path-psdk-adapter:1.
|
|
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.
|