path-terminal-init 0.2.12 → 0.2.16

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,456 +1,456 @@
1
- ---
2
- description: >
3
- Rules for AI agents integrating the Path Terminal SDK into an iOS 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
- - "**/*.swift"
8
- - "**/Info.plist"
9
- - "**/Package.swift"
10
- - "**/*.xcodeproj/**"
11
- alwaysApply: false
12
- ---
13
-
14
- # Path Terminal SDK — Integration Rules
15
-
16
- You are integrating the Path Terminal SDK into an existing iOS 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`, `validate_integration`, `get_integration_checklist`,
21
- `explain_error`, `get_info_plist_requirements`
22
- - Always call MCP tools before writing code. Never guess at SDK patterns.
23
-
24
- > **Check whether the Swift Package is installed before doing anything else.**
25
- > Run:
26
- > ```
27
- > find . -name "project.pbxproj" ! -path "*/DerivedData/*" ! -path "*/Pods/*" ! -path "*/.build/*" ! -path "*/SourcePackages/*" -exec grep -l "path-terminal-sdk\|Path-terminal-sdk\|PathTerminalSDK" {} +
28
- > ```
29
- > (Searches the project at **any** depth — the app's `.xcodeproj` is often nested
30
- > a folder or two below the repo root — so the check can't miss it. Prints the
31
- > project file if the package is present, nothing if it is not.)
32
- > - **Prints a path** — package is already in the project. Do not mention SPM or
33
- > package installation anywhere in your output. Proceed directly to Step 1.
34
- > - **Prints nothing** — package is not yet added. Tell the developer to add it:
35
- > 1. Open Xcode → **File → Add Package Dependencies…**
36
- > 2. Enter URL: `https://github.com/keyman12/Path-terminal-sdk-IOS-Release`
37
- > (the compiled-SDK release repo — Xcode downloads prebuilt XCFrameworks)
38
- > 3. In the **Add to Target** sheet, tick **all four** products:
39
- > `PathTerminalSDK`, `PathEmulatorAdapter`, `PathCoreModels`,
40
- > `PathVerifonePSDKAdapter`
41
- > (PathDiagnostics can be left as None)
42
- > `PathVerifonePSDKAdapter` is required for the Verifone backend (Step 2c).
43
- > 4. Click **Add Package**
44
- > Then stop and ask the developer to confirm the packages have been added
45
- > before you continue.
46
-
47
- > **Language override.** Never write "Pico W" in any output or code comment.
48
- > Always use "Path POS Emulator" when referring to the physical device.
49
-
50
- > **Confirm the correct project directory first.** Run `pwd` before doing
51
- > anything. If the path contains `.Trash`, `tmp`, or `worktree`, stop immediately
52
- > — you are not in the real project. Ask the developer to relaunch Claude Code
53
- > from the correct project directory. Never make changes from a Trash or temp path.
54
-
55
- ---
56
-
57
- ## Your job — four steps, in this order
58
-
59
- ---
60
-
61
- ### Step 1 — Disconnect the existing terminal wiring
62
-
63
- Find the app entry point (`@main` App struct or AppDelegate) and **read the actual
64
- code** — do not rely on comments or file names alone.
65
-
66
- Check the exact argument in the terminal manager initialisation call. A comment
67
- saying *"pass a different adapter"* does not mean it has been done — read the code.
68
-
69
- **You are looking for this pattern:**
70
- ```swift
71
- // THIS is wired to Path — correct:
72
- TerminalManager(adapter: PathPaymentTerminalAdapter())
73
-
74
- // THIS is NOT wired to Path — fix it:
75
- TerminalManager()
76
- TerminalManager(adapter: SomeOtherAdapter())
77
- ```
78
-
79
- If the call does not pass `PathPaymentTerminalAdapter()`, change it now.
80
-
81
- > **The class must be named exactly `PathPaymentTerminalAdapter`.** This name is
82
- > referenced across CLAUDE.md and the existing codebase. Do not use
83
- > `PathPaymentAdapter`, `PathAdapter`, or any other variation.
84
-
85
- **Also disable any auto-connect on launch** — find calls to `.connect()` in
86
- `.onAppear`, `init`, or app lifecycle methods that fire automatically on start.
87
- Remove them. Path connects manually via Settings, not on launch.
88
-
89
- > Do this before anything else. Until this is changed, all payments go through
90
- > the existing adapter no matter what else you do.
91
-
92
- ---
93
-
94
- ### Step 2 — Add a backend switcher to Settings
95
-
96
- The purpose of this integration is: **prove the payment functions against the Path
97
- POS Emulator, then let the user switch — themselves, in the app — to a real Verifone
98
- terminal when they are ready.** So Settings must let the user pick *which* terminal to
99
- connect to. Do NOT hardcode one transport.
100
-
101
- > **Connection details are entered by the user in the app, not by you.** You wire the
102
- > switcher, the IP field, and the credential storage. You do NOT ask the user for an IP
103
- > or password during this run — you build the screen, ship sensible test defaults, and
104
- > describe how to use it in your end-of-run summary (see Step 4).
105
-
106
- Find the Settings screen (a view named Settings, Preferences, or similar). Add a
107
- **Path POS Adapter** section with a backend picker offering **three** backends:
108
-
109
- | Backend | Transport | Adapter | Needs |
110
- |---|---|---|---|
111
- | **Emulator (Wi-Fi)** — *default selection* | TCP/IP | `TcpPathTerminalAdapter(host:)` | emulator IP |
112
- | **Emulator (Bluetooth)** | BLE | `BLEPathTerminalAdapter()` | scan + pick device |
113
- | **Verifone** | TCP/IP (PSDK) | `VerifonePSDKAdapter(config:)` | terminal IP + stored login |
114
-
115
- Mirror the demo app's proven pattern exactly — **call `get_code_example` with operation
116
- `"backend-switch"` first** and reproduce its `makeAdapter(for:)` shape. All sale / refund /
117
- void / receipt code is backend-agnostic; only adapter construction differs.
118
-
119
- **Persist the user's selection and connection details** (so the choice survives a relaunch).
120
- Store these in `UserDefaults` behind a small settings type, with test defaults so a fresh
121
- clone connects without typing anything:
122
- ```swift
123
- enum TerminalBackend: String, CaseIterable, Identifiable {
124
- case emulatorWifi = "emulator_wifi" // default
125
- case emulatorBLE = "emulator_ble"
126
- case verifone = "verifone"
127
- var id: String { rawValue }
128
- }
129
- // Stored keys: backend, emulatorHost, verifoneHost,
130
- // loginUsername, loginPassword, loginShift, refundPassword
131
- // Test defaults: verifoneHost "192.168.1.88", username "user",
132
- // password "password123", shift "shift123"
133
- ```
134
-
135
- #### Step 2a — Emulator over Wi-Fi (the default) and Verifone — IP entry
136
-
137
- For the two IP-addressed backends, show a **host/IP text field** (label it
138
- "Emulator IP" or "Verifone terminal IP" depending on the selection) plus an
139
- **Apply & Connect** button. On connect, build the matching adapter from the stored
140
- host and rebuild `PathTerminal` (disconnect the previous adapter first — real
141
- terminals and the emulator's Wi-Fi mode allow only **one** client at a time).
142
-
143
- #### Step 2b — Emulator over Bluetooth — scan and connect
144
-
145
- > **Add the Bluetooth permission to Info.plist now — this is not optional.** The BLE
146
- > backend cannot scan without it, and iOS fails **silently**: no prompt, no error, an
147
- > empty device list (it looks like a dead terminal). Add it **before** writing the scan
148
- > code — call `get_info_plist_requirements` for the exact XML:
149
- > ```xml
150
- > <key>NSBluetoothAlwaysUsageDescription</key>
151
- > <string>This app uses Bluetooth to connect to the Path POS payment terminal.</string>
152
- > ```
153
- > Do this **even when you validate over Wi-Fi/IP first** — the missing key stays
154
- > invisible until the user tries Bluetooth, then every scan silently finds nothing.
155
-
156
- For the BLE backend, keep the scan/connect flow:
157
- 1. **Scan for Path Terminals** — triggers BLE discovery
158
- 2. Discovered devices list — each with a **Connect** button
159
- 3. Live connection state (Scanning… / Connected / Disconnected)
160
- 4. **Disconnect** when connected
161
-
162
- To support this, add scan/connect to the manager and adapter.
163
-
164
- **Add to `PaymentTerminalAdapter` protocol** (with no-op default implementations
165
- so any other existing adapter requires no changes):
166
- ```swift
167
- func scanForDevices() async throws -> [TerminalDeviceInfo]
168
- func connectToDevice(id: String) async throws
169
- ```
170
-
171
- **Add `TerminalDeviceInfo`** to the terminal models file **before** writing any
172
- code that references it — other files will fail to compile if this is added last:
173
- ```swift
174
- struct TerminalDeviceInfo: Identifiable, Equatable {
175
- let id: String
176
- let name: String
177
- }
178
- ```
179
-
180
- **Implement in `PathPaymentTerminalAdapter`** — call `get_code_example` with
181
- operation `"discover"` first. The SDK exposes a concrete `DiscoveredDevice` type
182
- with a `.name` property — use it directly. Do not use `[some Any]` or `[any Any]`
183
- to store devices, and do not parse names from string descriptions. Then implement:
184
- - `scanForDevices()` — calls `pathTerminal.discoverDevices()`, maps results to
185
- `[TerminalDeviceInfo]`, stores a closure per device (so the SDK's opaque
186
- device type never leaks into the rest of the app)
187
- - `connectToDevice(id:)` — looks up the stored closure by id and calls it
188
-
189
- > **TCP and Verifone need no scan** — `discoverDevices()` returns one synthetic
190
- > device for the configured host, so "Apply & Connect" can connect directly.
191
-
192
- **Expose on the terminal manager:**
193
- ```swift
194
- @Published private(set) var discoveredDevices: [TerminalDeviceInfo] = []
195
- @Published private(set) var isScanning: Bool = false
196
-
197
- func scanForDevices() async { ... }
198
- func connectToDevice(_ device: TerminalDeviceInfo) async { ... }
199
- ```
200
-
201
- #### Step 2c — Verifone backend and stored credentials
202
-
203
- The Verifone backend connects to a real terminal over the PSDK. Call
204
- `get_code_example` with operation `"verifone-init"` first. Construct it from a
205
- `VerifoneTerminalConfig` built from the **stored** login fields — never hardcode
206
- credentials inline:
207
- ```swift
208
- import PathVerifonePSDKAdapter // separate SPM product — ticked at install
209
-
210
- let adapter = VerifonePSDKAdapter(config: VerifoneTerminalConfig(
211
- host: settings.verifoneHost,
212
- username: settings.loginUsername,
213
- password: settings.loginPassword,
214
- shift: settings.loginShift,
215
- refundPassword: settings.refundPassword
216
- ))
217
- ```
218
- Keep the Verifone login fields **separate from the emulator settings** (their own
219
- stored keys, their own editable fields in Settings) — cleaner than reusing the
220
- emulator's. All terminals are treated as **generic Verifone** (same AGPA app, same
221
- PSDK connect/login), so no per-terminal configuration is needed beyond IP + login.
222
-
223
- #### Step 2d — Merchant logo on the customer display (idle branding)
224
-
225
- Wire the terminal's **customer-display logo** as part of the integration — it is a
226
- one-call feature that partners expect, and it is easy to miss. Call `get_code_example`
227
- with operation `"idle-branding"` first, then reproduce it.
228
-
229
- - Push the logo with `terminal.setIdleBranding(CustomerDisplayContent(imageData:caption:))`
230
- **right after a successful `connect()`** (and whenever the logo changes). It is a
231
- **no-op while disconnected**, and a **no-op on the emulator** (only Verifone paints a
232
- customer screen), so it is safe to call for any backend.
233
- - `imageData` is raw **PNG/JPEG bytes**. Use the app's existing logo asset if there is
234
- one; otherwise a simple placeholder. Caption = the store name.
235
- - **Hard size rule:** the payload must fit **~32 KB base64**. A full-size photo is
236
- **silently dropped by the PSDK and can wedge the next transaction**. Size the logo to
237
- ~**360px** wide (a small PNG). Do not pass a camera-resolution image.
238
- - On success the SDK logs `idle branding pushed to customer display` — surface that in
239
- your `onLog` handler so the developer can confirm it worked.
240
-
241
- Do **not** call `setIdleBranding` before connect in the payment path, and never block the
242
- sale/refund flow on it — it is fire-and-forget branding, not part of a transaction.
243
-
244
- #### After creating PathPaymentTerminalAdapter.swift — confirm the SDK is linked
245
-
246
- Before triggering the first build, run:
247
-
248
- ```
249
- find . -name "project.pbxproj" ! -path "*/DerivedData/*" ! -path "*/Pods/*" ! -path "*/.build/*" ! -path "*/SourcePackages/*" -exec grep -l "PBXFrameworksBuildPhase" {} +
250
- ```
251
-
252
- If this prints **nothing**, the Frameworks build phase is missing and the build will
253
- fail with `Undefined symbol` linker errors for PathTerminalSDK / PathEmulatorAdapter
254
- / PathCoreModels. Fix it now — do not wait for the build to fail.
255
-
256
- **How to add the missing Frameworks build phase:**
257
-
258
- 1. Find the four package product UUIDs from the target's `packageProductDependencies`:
259
- ```
260
- find . -name "project.pbxproj" ! -path "*/DerivedData/*" ! -path "*/Pods/*" ! -path "*/.build/*" ! -path "*/SourcePackages/*" -exec grep -A 8 "packageProductDependencies" {} +
261
- ```
262
- You will see something like:
263
- ```
264
- packageProductDependencies = (
265
- AAAA1111BBBB2222 /* PathCoreModels */,
266
- CCCC3333DDDD4444 /* PathEmulatorAdapter */,
267
- EEEE5555FFFF6666 /* PathTerminalSDK */,
268
- 7777888899990000 /* PathVerifonePSDKAdapter */,
269
- );
270
- ```
271
-
272
- 2. In the `/* Begin PBXBuildFile section */`, add one entry per product. Use
273
- **fresh UUIDs** for the build file entries (not the productRef UUIDs):
274
- ```
275
- A1A1A1A1B2B2B2B2 /* PathCoreModels in Frameworks */ = {isa = PBXBuildFile; productRef = AAAA1111BBBB2222 /* PathCoreModels */; };
276
- C3C3C3C3D4D4D4D4 /* PathEmulatorAdapter in Frameworks */ = {isa = PBXBuildFile; productRef = CCCC3333DDDD4444 /* PathEmulatorAdapter */; };
277
- E5E5E5E5F6F6F6F6 /* PathTerminalSDK in Frameworks */ = {isa = PBXBuildFile; productRef = EEEE5555FFFF6666 /* PathTerminalSDK */; };
278
- 1212343456567878 /* PathVerifonePSDKAdapter in Frameworks */ = {isa = PBXBuildFile; productRef = 7777888899990000 /* PathVerifonePSDKAdapter */; };
279
- ```
280
-
281
- 3. Add a `PBXFrameworksBuildPhase` section (before `PBXResourcesBuildPhase`):
282
- ```
283
- /* Begin PBXFrameworksBuildPhase section */
284
- F7F7F7F7A8A8A8A8 /* Frameworks */ = {
285
- isa = PBXFrameworksBuildPhase;
286
- buildActionMask = 2147483647;
287
- files = (
288
- A1A1A1A1B2B2B2B2 /* PathCoreModels in Frameworks */,
289
- C3C3C3C3D4D4D4D4 /* PathEmulatorAdapter in Frameworks */,
290
- E5E5E5E5F6F6F6F6 /* PathTerminalSDK in Frameworks */,
291
- 1212343456567878 /* PathVerifonePSDKAdapter in Frameworks */,
292
- );
293
- runOnlyForDeploymentPostprocessing = 0;
294
- };
295
- /* End PBXFrameworksBuildPhase section */
296
- ```
297
-
298
- 4. Add the Frameworks phase UUID to the target's `buildPhases` array:
299
- ```
300
- buildPhases = (
301
- <Sources UUID>,
302
- F7F7F7F7A8A8A8A8 /* Frameworks */, ← add this line
303
- <Resources UUID>,
304
- );
305
- ```
306
-
307
- Replace every UUID shown above with freshly generated unique hex strings.
308
- **Never reuse an existing UUID from the file.** The productRef values (step 1)
309
- are the only values copied from the existing file.
310
-
311
- ---
312
-
313
- ### Step 3 — Wire sale and refund through the Path SDK
314
-
315
- If `PathPaymentTerminalAdapter.swift` already exists and implements `submitSale()`
316
- and `submitRefund()`, **no changes are needed to the payment flow** — once Step 1
317
- is done, all sales and refunds automatically go through Path.
318
-
319
- Call `validate_integration` with the existing `PathPaymentTerminalAdapter.swift`
320
- to confirm it is correct. Fix any warnings before proceeding.
321
-
322
- If `PathPaymentTerminalAdapter.swift` does not exist, create it:
323
- - Call `get_code_example` with operation `"sale"` before writing sale code
324
- - Call `get_code_example` with operation `"refund"` before writing refund code
325
- - Call `validate_integration` on your code before finalising
326
-
327
- ---
328
-
329
- ### Step 3.5 — Build to catch API mismatches (check the environment first)
330
-
331
- Confirm your Swift compiles against the SDK's **real** API. **Probe the toolchain first**, and
332
- treat a missing one as an environment limitation to advise on, **never as a failure:**
333
-
334
- 1. `xcodebuild -version` — is Xcode (or its command-line tools) available?
335
- 2. **Present** → you may run a build (e.g. `xcodebuild -scheme <App> -destination 'generic/platform=iOS' build`)
336
- to surface Swift errors, then fix real API mismatches. (Full runtime verification is the
337
- developer's step below — it needs Xcode + a device/simulator.)
338
- 3. **Absent** → the integration code is complete; you just can't build *here*. Do **not** report a
339
- failure. Tell the developer plainly: *"The code is done — open the project in Xcode and build;
340
- paste back any errors and I'll fix them,"* and summarise every file you changed. (Xcode installs
341
- from the App Store, not a package manager — don't offer to install it.)
342
-
343
- Frame a missing build toolchain as **"here's how to verify"**, never as a failure of the work.
344
-
345
- ### Step 4 — Verify it works end to end
346
-
347
- 1. Confirm the app entry point injects `PathPaymentTerminalAdapter()`.
348
- If the old adapter is still the default, go back to Step 1.
349
-
350
- 2. Confirm Info.plist has the BLE permission `NSBluetoothAlwaysUsageDescription`
351
- (you added it in Step 2b). If it is missing, the Bluetooth backend finds **no
352
- devices** and shows no error — add it now (`get_info_plist_requirements` has the XML).
353
-
354
- 3. **Prove on the emulator first (the default path — Wi-Fi/IP):**
355
- - Put the Path POS Emulator in Wi-Fi mode (Config → Connection on the device);
356
- it shows its `IP:port` on the welcome screen.
357
- - Run the app → Settings → Path POS Adapter → backend **Emulator (Wi-Fi)** →
358
- type that IP → **Apply & Connect**. Connection state should reach `.connected`.
359
- - (Optionally also prove **Emulator (Bluetooth)**: select it, **Scan**, **Connect**.)
360
-
361
- 4. Return to the EPOS. Add items to the cart. Trigger a card payment.
362
- When prompted, present a card / tap the NFC tag on the Path POS Emulator.
363
-
364
- 5. Verify:
365
- - `result.state == .approved`
366
- - `result.transactionId` is non-nil
367
- - Receipt data is populated (call `get_code_example` with operation `"receipt"`)
368
-
369
- 6. **Then prove the switch to Verifone:** select backend **Verifone**, enter the
370
- terminal's IP and confirm the stored login, **Apply & Connect** — the SDK runs
371
- the PSDK init + login. The same sale/refund/void flow now runs against the real
372
- terminal, unchanged. (Only one client may connect to a Verifone terminal at a
373
- time — disconnect any other POS first.)
374
-
375
- 7. **Confirm the customer-display logo (Step 2d):** on the Verifone connect, the
376
- terminal screen should show the merchant logo, and the log should read
377
- `idle branding pushed to customer display`. If the screen is blank, the image is
378
- almost certainly over the ~32 KB base64 limit — shrink it (a ~360px PNG) and retry.
379
- (The emulator has no customer display, so nothing shows there — that is expected.)
380
-
381
- ---
382
-
383
- ## SDK rules — always follow these
384
-
385
- **ALWAYS:**
386
- - Call `get_code_example` before writing any SDK code
387
- - Use `TransactionRequest.sale(...)` and `TransactionRequest.refund(...)` factory methods
388
- - Use `RequestEnvelope.create(sdkVersion:adapterVersion:)` — never build envelopes manually
389
- - Handle all `TransactionState` cases: `.approved`, `.declined`, `.timedOut`, `.failed`
390
- - For **refunds**, the success state is `.refunded` — **not** `.approved`. Using
391
- `.approved` in a refund switch will never match and every refund will appear to fail.
392
- - Catch `PathError` and check `.recoverable` — if true, retry is safe; if false, do not retry
393
- - Call `validate_integration` on your code before presenting it to the developer
394
- - Import both `PathTerminalSDK` and `PathCoreModels`
395
- - When wiring the BLE backend, add `NSBluetoothAlwaysUsageDescription` to Info.plist —
396
- without it iOS blocks Bluetooth scanning **silently** (empty device list, no error)
397
-
398
- **NEVER:**
399
- - Use the raw `TransactionRequest` initialiser — always use the factory methods
400
- - Store or log card data (maskedPan is acceptable, nothing else)
401
- - Retry a financial operation without a new `idempotencyKey`
402
- - Leave the existing default adapter in place — Path must be the injected adapter
403
- - Leave auto-connect-on-launch running — it will connect the old adapter and bypass Path
404
- - Remove the existing adapter from the codebase — it must remain available alongside Path
405
- - Skip `PathError` handling
406
- - Use `[some Any]` or `[any Any]` to store discovered devices. This is always wrong:
407
- ```swift
408
- // WRONG — will not compile when assigned from discoverDevices():
409
- let devices: [some Any]
410
- let devices: [any Any]
411
-
412
- // CORRECT — use the concrete SDK type directly:
413
- let devices: [DiscoveredDevice]
414
- ```
415
- `DiscoveredDevice` is a concrete type from `PathCoreModels` with a `.name` property.
416
- - Reference a type (e.g. `TerminalDeviceInfo`) in one file before adding it to the
417
- models file — add shared types to the models file first, then reference them
418
- - Parse device names from string descriptions — use `device.name` directly
419
- - Write "Pico W" anywhere in code, comments, or UI strings — always "Path POS Emulator"
420
- - Guess at UUIDs when editing `project.pbxproj` — always read existing UUIDs
421
- from the file itself. All productRef UUIDs for the SDK packages already exist
422
- in the target's `packageProductDependencies` array — use those exact values.
423
-
424
- **AMOUNTS:** Minor currency units only.
425
- - `100` = £1.00 GBP · `1250` = £12.50 GBP · Never pass decimals.
426
-
427
- **TIMEOUTS:**
428
- - Never wrap `terminal.submitSale()` or `terminal.submitRefund()` in any app-level
429
- timeout shorter than 30 seconds — the Path POS Emulator needs time to wait for
430
- card/NFC presentation. The SDK handles its own 30-second response ceiling.
431
- - Never call `terminal.connect()` automatically inside the payment or refund flow.
432
- If the terminal is not connected when a payment is triggered, show an error
433
- directing the user to Settings → Payment Terminal to reconnect. Do not attempt
434
- to auto-reconnect mid-flow — a silent reconnect attempt masquerading as a
435
- payment timeout is confusing and hard to recover from.
436
-
437
- ---
438
-
439
- ## After making changes
440
-
441
- Summarise what you changed:
442
- - Each file modified and the specific change made
443
- - Confirm Step 1 (adapter injection) was completed
444
-
445
- Then give **tailored connect instructions** — the user enters the connection
446
- details themselves, so tell them exactly where you wired things in:
447
- - The Settings screen / section name you added the backend switcher to
448
- - How to connect to the emulator over **Wi-Fi/IP** (the default): which backend to
449
- pick, where to type the emulator's IP, which button connects
450
- - How to also connect over **Bluetooth** if they prefer (Scan → Connect)
451
- - How to **switch to Verifone** when they're ready: pick Verifone, enter the
452
- terminal IP, confirm the stored login, connect
453
- - That the **merchant logo** appears on the Verifone customer display on connect
454
- (Step 2d), and where to drop in their own logo asset (keep it a small ~360px PNG)
455
-
456
- 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 iOS 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
+ - "**/*.swift"
8
+ - "**/Info.plist"
9
+ - "**/Package.swift"
10
+ - "**/*.xcodeproj/**"
11
+ alwaysApply: false
12
+ ---
13
+
14
+ # Path Terminal SDK — Integration Rules
15
+
16
+ You are integrating the Path Terminal SDK into an existing iOS 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`, `validate_integration`, `get_integration_checklist`,
21
+ `explain_error`, `get_info_plist_requirements`
22
+ - Always call MCP tools before writing code. Never guess at SDK patterns.
23
+
24
+ > **Check whether the Swift Package is installed before doing anything else.**
25
+ > Run:
26
+ > ```
27
+ > find . -name "project.pbxproj" ! -path "*/DerivedData/*" ! -path "*/Pods/*" ! -path "*/.build/*" ! -path "*/SourcePackages/*" -exec grep -l "path-terminal-sdk\|Path-terminal-sdk\|PathTerminalSDK" {} +
28
+ > ```
29
+ > (Searches the project at **any** depth — the app's `.xcodeproj` is often nested
30
+ > a folder or two below the repo root — so the check can't miss it. Prints the
31
+ > project file if the package is present, nothing if it is not.)
32
+ > - **Prints a path** — package is already in the project. Do not mention SPM or
33
+ > package installation anywhere in your output. Proceed directly to Step 1.
34
+ > - **Prints nothing** — package is not yet added. Tell the developer to add it:
35
+ > 1. Open Xcode → **File → Add Package Dependencies…**
36
+ > 2. Enter URL: `https://github.com/keyman12/Path-terminal-sdk-IOS-Release`
37
+ > (the compiled-SDK release repo — Xcode downloads prebuilt XCFrameworks)
38
+ > 3. In the **Add to Target** sheet, tick **all four** products:
39
+ > `PathTerminalSDK`, `PathEmulatorAdapter`, `PathCoreModels`,
40
+ > `PathVerifonePSDKAdapter`
41
+ > (PathDiagnostics can be left as None)
42
+ > `PathVerifonePSDKAdapter` is required for the Verifone backend (Step 2c).
43
+ > 4. Click **Add Package**
44
+ > Then stop and ask the developer to confirm the packages have been added
45
+ > before you continue.
46
+
47
+ > **Language override.** Never write "Pico W" in any output or code comment.
48
+ > Always use "Path POS Emulator" when referring to the physical device.
49
+
50
+ > **Confirm the correct project directory first.** Run `pwd` before doing
51
+ > anything. If the path contains `.Trash`, `tmp`, or `worktree`, stop immediately
52
+ > — you are not in the real project. Ask the developer to relaunch Claude Code
53
+ > from the correct project directory. Never make changes from a Trash or temp path.
54
+
55
+ ---
56
+
57
+ ## Your job — four steps, in this order
58
+
59
+ ---
60
+
61
+ ### Step 1 — Disconnect the existing terminal wiring
62
+
63
+ Find the app entry point (`@main` App struct or AppDelegate) and **read the actual
64
+ code** — do not rely on comments or file names alone.
65
+
66
+ Check the exact argument in the terminal manager initialisation call. A comment
67
+ saying *"pass a different adapter"* does not mean it has been done — read the code.
68
+
69
+ **You are looking for this pattern:**
70
+ ```swift
71
+ // THIS is wired to Path — correct:
72
+ TerminalManager(adapter: PathPaymentTerminalAdapter())
73
+
74
+ // THIS is NOT wired to Path — fix it:
75
+ TerminalManager()
76
+ TerminalManager(adapter: SomeOtherAdapter())
77
+ ```
78
+
79
+ If the call does not pass `PathPaymentTerminalAdapter()`, change it now.
80
+
81
+ > **The class must be named exactly `PathPaymentTerminalAdapter`.** This name is
82
+ > referenced across CLAUDE.md and the existing codebase. Do not use
83
+ > `PathPaymentAdapter`, `PathAdapter`, or any other variation.
84
+
85
+ **Also disable any auto-connect on launch** — find calls to `.connect()` in
86
+ `.onAppear`, `init`, or app lifecycle methods that fire automatically on start.
87
+ Remove them. Path connects manually via Settings, not on launch.
88
+
89
+ > Do this before anything else. Until this is changed, all payments go through
90
+ > the existing adapter no matter what else you do.
91
+
92
+ ---
93
+
94
+ ### Step 2 — Add a backend switcher to Settings
95
+
96
+ The purpose of this integration is: **prove the payment functions against the Path
97
+ POS Emulator, then let the user switch — themselves, in the app — to a real Verifone
98
+ terminal when they are ready.** So Settings must let the user pick *which* terminal to
99
+ connect to. Do NOT hardcode one transport.
100
+
101
+ > **Connection details are entered by the user in the app, not by you.** You wire the
102
+ > switcher, the IP field, and the credential storage. You do NOT ask the user for an IP
103
+ > or password during this run — you build the screen, ship sensible test defaults, and
104
+ > describe how to use it in your end-of-run summary (see Step 4).
105
+
106
+ Find the Settings screen (a view named Settings, Preferences, or similar). Add a
107
+ **Path POS Adapter** section with a backend picker offering **three** backends:
108
+
109
+ | Backend | Transport | Adapter | Needs |
110
+ |---|---|---|---|
111
+ | **Emulator (Wi-Fi)** — *default selection* | TCP/IP | `TcpPathTerminalAdapter(host:)` | emulator IP |
112
+ | **Emulator (Bluetooth)** | BLE | `BLEPathTerminalAdapter()` | scan + pick device |
113
+ | **Verifone** | TCP/IP (PSDK) | `VerifonePSDKAdapter(config:)` | terminal IP + stored login |
114
+
115
+ Mirror the demo app's proven pattern exactly — **call `get_code_example` with operation
116
+ `"backend-switch"` first** and reproduce its `makeAdapter(for:)` shape. All sale / refund /
117
+ void / receipt code is backend-agnostic; only adapter construction differs.
118
+
119
+ **Persist the user's selection and connection details** (so the choice survives a relaunch).
120
+ Store these in `UserDefaults` behind a small settings type, with test defaults so a fresh
121
+ clone connects without typing anything:
122
+ ```swift
123
+ enum TerminalBackend: String, CaseIterable, Identifiable {
124
+ case emulatorWifi = "emulator_wifi" // default
125
+ case emulatorBLE = "emulator_ble"
126
+ case verifone = "verifone"
127
+ var id: String { rawValue }
128
+ }
129
+ // Stored keys: backend, emulatorHost, verifoneHost,
130
+ // loginUsername, loginPassword, loginShift, refundPassword
131
+ // Test defaults: verifoneHost "192.168.1.88", username "user",
132
+ // password "password123", shift "shift123"
133
+ ```
134
+
135
+ #### Step 2a — Emulator over Wi-Fi (the default) and Verifone — IP entry
136
+
137
+ For the two IP-addressed backends, show a **host/IP text field** (label it
138
+ "Emulator IP" or "Verifone terminal IP" depending on the selection) plus an
139
+ **Apply & Connect** button. On connect, build the matching adapter from the stored
140
+ host and rebuild `PathTerminal` (disconnect the previous adapter first — real
141
+ terminals and the emulator's Wi-Fi mode allow only **one** client at a time).
142
+
143
+ #### Step 2b — Emulator over Bluetooth — scan and connect
144
+
145
+ > **Add the Bluetooth permission to Info.plist now — this is not optional.** The BLE
146
+ > backend cannot scan without it, and iOS fails **silently**: no prompt, no error, an
147
+ > empty device list (it looks like a dead terminal). Add it **before** writing the scan
148
+ > code — call `get_info_plist_requirements` for the exact XML:
149
+ > ```xml
150
+ > <key>NSBluetoothAlwaysUsageDescription</key>
151
+ > <string>This app uses Bluetooth to connect to the Path POS payment terminal.</string>
152
+ > ```
153
+ > Do this **even when you validate over Wi-Fi/IP first** — the missing key stays
154
+ > invisible until the user tries Bluetooth, then every scan silently finds nothing.
155
+
156
+ For the BLE backend, keep the scan/connect flow:
157
+ 1. **Scan for Path Terminals** — triggers BLE discovery
158
+ 2. Discovered devices list — each with a **Connect** button
159
+ 3. Live connection state (Scanning… / Connected / Disconnected)
160
+ 4. **Disconnect** when connected
161
+
162
+ To support this, add scan/connect to the manager and adapter.
163
+
164
+ **Add to `PaymentTerminalAdapter` protocol** (with no-op default implementations
165
+ so any other existing adapter requires no changes):
166
+ ```swift
167
+ func scanForDevices() async throws -> [TerminalDeviceInfo]
168
+ func connectToDevice(id: String) async throws
169
+ ```
170
+
171
+ **Add `TerminalDeviceInfo`** to the terminal models file **before** writing any
172
+ code that references it — other files will fail to compile if this is added last:
173
+ ```swift
174
+ struct TerminalDeviceInfo: Identifiable, Equatable {
175
+ let id: String
176
+ let name: String
177
+ }
178
+ ```
179
+
180
+ **Implement in `PathPaymentTerminalAdapter`** — call `get_code_example` with
181
+ operation `"discover"` first. The SDK exposes a concrete `DiscoveredDevice` type
182
+ with a `.name` property — use it directly. Do not use `[some Any]` or `[any Any]`
183
+ to store devices, and do not parse names from string descriptions. Then implement:
184
+ - `scanForDevices()` — calls `pathTerminal.discoverDevices()`, maps results to
185
+ `[TerminalDeviceInfo]`, stores a closure per device (so the SDK's opaque
186
+ device type never leaks into the rest of the app)
187
+ - `connectToDevice(id:)` — looks up the stored closure by id and calls it
188
+
189
+ > **TCP and Verifone need no scan** — `discoverDevices()` returns one synthetic
190
+ > device for the configured host, so "Apply & Connect" can connect directly.
191
+
192
+ **Expose on the terminal manager:**
193
+ ```swift
194
+ @Published private(set) var discoveredDevices: [TerminalDeviceInfo] = []
195
+ @Published private(set) var isScanning: Bool = false
196
+
197
+ func scanForDevices() async { ... }
198
+ func connectToDevice(_ device: TerminalDeviceInfo) async { ... }
199
+ ```
200
+
201
+ #### Step 2c — Verifone backend and stored credentials
202
+
203
+ The Verifone backend connects to a real terminal over the PSDK. Call
204
+ `get_code_example` with operation `"verifone-init"` first. Construct it from a
205
+ `VerifoneTerminalConfig` built from the **stored** login fields — never hardcode
206
+ credentials inline:
207
+ ```swift
208
+ import PathVerifonePSDKAdapter // separate SPM product — ticked at install
209
+
210
+ let adapter = VerifonePSDKAdapter(config: VerifoneTerminalConfig(
211
+ host: settings.verifoneHost,
212
+ username: settings.loginUsername,
213
+ password: settings.loginPassword,
214
+ shift: settings.loginShift,
215
+ refundPassword: settings.refundPassword
216
+ ))
217
+ ```
218
+ Keep the Verifone login fields **separate from the emulator settings** (their own
219
+ stored keys, their own editable fields in Settings) — cleaner than reusing the
220
+ emulator's. All terminals are treated as **generic Verifone** (same AGPA app, same
221
+ PSDK connect/login), so no per-terminal configuration is needed beyond IP + login.
222
+
223
+ #### Step 2d — Merchant logo on the customer display (idle branding)
224
+
225
+ Wire the terminal's **customer-display logo** as part of the integration — it is a
226
+ one-call feature that partners expect, and it is easy to miss. Call `get_code_example`
227
+ with operation `"idle-branding"` first, then reproduce it.
228
+
229
+ - Push the logo with `terminal.setIdleBranding(CustomerDisplayContent(imageData:caption:))`
230
+ **right after a successful `connect()`** (and whenever the logo changes). It is a
231
+ **no-op while disconnected**, and a **no-op on the emulator** (only Verifone paints a
232
+ customer screen), so it is safe to call for any backend.
233
+ - `imageData` is raw **PNG/JPEG bytes**. Use the app's existing logo asset if there is
234
+ one; otherwise a simple placeholder. Caption = the store name.
235
+ - **Hard size rule:** the payload must fit **~32 KB base64**. A full-size photo is
236
+ **silently dropped by the PSDK and can wedge the next transaction**. Size the logo to
237
+ ~**360px** wide (a small PNG). Do not pass a camera-resolution image.
238
+ - On success the SDK logs `idle branding pushed to customer display` — surface that in
239
+ your `onLog` handler so the developer can confirm it worked.
240
+
241
+ Do **not** call `setIdleBranding` before connect in the payment path, and never block the
242
+ sale/refund flow on it — it is fire-and-forget branding, not part of a transaction.
243
+
244
+ #### After creating PathPaymentTerminalAdapter.swift — confirm the SDK is linked
245
+
246
+ Before triggering the first build, run:
247
+
248
+ ```
249
+ find . -name "project.pbxproj" ! -path "*/DerivedData/*" ! -path "*/Pods/*" ! -path "*/.build/*" ! -path "*/SourcePackages/*" -exec grep -l "PBXFrameworksBuildPhase" {} +
250
+ ```
251
+
252
+ If this prints **nothing**, the Frameworks build phase is missing and the build will
253
+ fail with `Undefined symbol` linker errors for PathTerminalSDK / PathEmulatorAdapter
254
+ / PathCoreModels. Fix it now — do not wait for the build to fail.
255
+
256
+ **How to add the missing Frameworks build phase:**
257
+
258
+ 1. Find the four package product UUIDs from the target's `packageProductDependencies`:
259
+ ```
260
+ find . -name "project.pbxproj" ! -path "*/DerivedData/*" ! -path "*/Pods/*" ! -path "*/.build/*" ! -path "*/SourcePackages/*" -exec grep -A 8 "packageProductDependencies" {} +
261
+ ```
262
+ You will see something like:
263
+ ```
264
+ packageProductDependencies = (
265
+ AAAA1111BBBB2222 /* PathCoreModels */,
266
+ CCCC3333DDDD4444 /* PathEmulatorAdapter */,
267
+ EEEE5555FFFF6666 /* PathTerminalSDK */,
268
+ 7777888899990000 /* PathVerifonePSDKAdapter */,
269
+ );
270
+ ```
271
+
272
+ 2. In the `/* Begin PBXBuildFile section */`, add one entry per product. Use
273
+ **fresh UUIDs** for the build file entries (not the productRef UUIDs):
274
+ ```
275
+ A1A1A1A1B2B2B2B2 /* PathCoreModels in Frameworks */ = {isa = PBXBuildFile; productRef = AAAA1111BBBB2222 /* PathCoreModels */; };
276
+ C3C3C3C3D4D4D4D4 /* PathEmulatorAdapter in Frameworks */ = {isa = PBXBuildFile; productRef = CCCC3333DDDD4444 /* PathEmulatorAdapter */; };
277
+ E5E5E5E5F6F6F6F6 /* PathTerminalSDK in Frameworks */ = {isa = PBXBuildFile; productRef = EEEE5555FFFF6666 /* PathTerminalSDK */; };
278
+ 1212343456567878 /* PathVerifonePSDKAdapter in Frameworks */ = {isa = PBXBuildFile; productRef = 7777888899990000 /* PathVerifonePSDKAdapter */; };
279
+ ```
280
+
281
+ 3. Add a `PBXFrameworksBuildPhase` section (before `PBXResourcesBuildPhase`):
282
+ ```
283
+ /* Begin PBXFrameworksBuildPhase section */
284
+ F7F7F7F7A8A8A8A8 /* Frameworks */ = {
285
+ isa = PBXFrameworksBuildPhase;
286
+ buildActionMask = 2147483647;
287
+ files = (
288
+ A1A1A1A1B2B2B2B2 /* PathCoreModels in Frameworks */,
289
+ C3C3C3C3D4D4D4D4 /* PathEmulatorAdapter in Frameworks */,
290
+ E5E5E5E5F6F6F6F6 /* PathTerminalSDK in Frameworks */,
291
+ 1212343456567878 /* PathVerifonePSDKAdapter in Frameworks */,
292
+ );
293
+ runOnlyForDeploymentPostprocessing = 0;
294
+ };
295
+ /* End PBXFrameworksBuildPhase section */
296
+ ```
297
+
298
+ 4. Add the Frameworks phase UUID to the target's `buildPhases` array:
299
+ ```
300
+ buildPhases = (
301
+ <Sources UUID>,
302
+ F7F7F7F7A8A8A8A8 /* Frameworks */, ← add this line
303
+ <Resources UUID>,
304
+ );
305
+ ```
306
+
307
+ Replace every UUID shown above with freshly generated unique hex strings.
308
+ **Never reuse an existing UUID from the file.** The productRef values (step 1)
309
+ are the only values copied from the existing file.
310
+
311
+ ---
312
+
313
+ ### Step 3 — Wire sale and refund through the Path SDK
314
+
315
+ If `PathPaymentTerminalAdapter.swift` already exists and implements `submitSale()`
316
+ and `submitRefund()`, **no changes are needed to the payment flow** — once Step 1
317
+ is done, all sales and refunds automatically go through Path.
318
+
319
+ Call `validate_integration` with the existing `PathPaymentTerminalAdapter.swift`
320
+ to confirm it is correct. Fix any warnings before proceeding.
321
+
322
+ If `PathPaymentTerminalAdapter.swift` does not exist, create it:
323
+ - Call `get_code_example` with operation `"sale"` before writing sale code
324
+ - Call `get_code_example` with operation `"refund"` before writing refund code
325
+ - Call `validate_integration` on your code before finalising
326
+
327
+ ---
328
+
329
+ ### Step 3.5 — Build to catch API mismatches (check the environment first)
330
+
331
+ Confirm your Swift compiles against the SDK's **real** API. **Probe the toolchain first**, and
332
+ treat a missing one as an environment limitation to advise on, **never as a failure:**
333
+
334
+ 1. `xcodebuild -version` — is Xcode (or its command-line tools) available?
335
+ 2. **Present** → you may run a build (e.g. `xcodebuild -scheme <App> -destination 'generic/platform=iOS' build`)
336
+ to surface Swift errors, then fix real API mismatches. (Full runtime verification is the
337
+ developer's step below — it needs Xcode + a device/simulator.)
338
+ 3. **Absent** → the integration code is complete; you just can't build *here*. Do **not** report a
339
+ failure. Tell the developer plainly: *"The code is done — open the project in Xcode and build;
340
+ paste back any errors and I'll fix them,"* and summarise every file you changed. (Xcode installs
341
+ from the App Store, not a package manager — don't offer to install it.)
342
+
343
+ Frame a missing build toolchain as **"here's how to verify"**, never as a failure of the work.
344
+
345
+ ### Step 4 — Verify it works end to end
346
+
347
+ 1. Confirm the app entry point injects `PathPaymentTerminalAdapter()`.
348
+ If the old adapter is still the default, go back to Step 1.
349
+
350
+ 2. Confirm Info.plist has the BLE permission `NSBluetoothAlwaysUsageDescription`
351
+ (you added it in Step 2b). If it is missing, the Bluetooth backend finds **no
352
+ devices** and shows no error — add it now (`get_info_plist_requirements` has the XML).
353
+
354
+ 3. **Prove on the emulator first (the default path — Wi-Fi/IP):**
355
+ - Put the Path POS Emulator in Wi-Fi mode (Config → Connection on the device);
356
+ it shows its `IP:port` on the welcome screen.
357
+ - Run the app → Settings → Path POS Adapter → backend **Emulator (Wi-Fi)** →
358
+ type that IP → **Apply & Connect**. Connection state should reach `.connected`.
359
+ - (Optionally also prove **Emulator (Bluetooth)**: select it, **Scan**, **Connect**.)
360
+
361
+ 4. Return to the EPOS. Add items to the cart. Trigger a card payment.
362
+ When prompted, present a card / tap the NFC tag on the Path POS Emulator.
363
+
364
+ 5. Verify:
365
+ - `result.state == .approved`
366
+ - `result.transactionId` is non-nil
367
+ - Receipt data is populated (call `get_code_example` with operation `"receipt"`)
368
+
369
+ 6. **Then prove the switch to Verifone:** select backend **Verifone**, enter the
370
+ terminal's IP and confirm the stored login, **Apply & Connect** — the SDK runs
371
+ the PSDK init + login. The same sale/refund/void flow now runs against the real
372
+ terminal, unchanged. (Only one client may connect to a Verifone terminal at a
373
+ time — disconnect any other POS first.)
374
+
375
+ 7. **Confirm the customer-display logo (Step 2d):** on the Verifone connect, the
376
+ terminal screen should show the merchant logo, and the log should read
377
+ `idle branding pushed to customer display`. If the screen is blank, the image is
378
+ almost certainly over the ~32 KB base64 limit — shrink it (a ~360px PNG) and retry.
379
+ (The emulator has no customer display, so nothing shows there — that is expected.)
380
+
381
+ ---
382
+
383
+ ## SDK rules — always follow these
384
+
385
+ **ALWAYS:**
386
+ - Call `get_code_example` before writing any SDK code
387
+ - Use `TransactionRequest.sale(...)` and `TransactionRequest.refund(...)` factory methods
388
+ - Use `RequestEnvelope.create(sdkVersion:adapterVersion:)` — never build envelopes manually
389
+ - Handle all `TransactionState` cases: `.approved`, `.declined`, `.timedOut`, `.failed`
390
+ - For **refunds**, the success state is `.refunded` — **not** `.approved`. Using
391
+ `.approved` in a refund switch will never match and every refund will appear to fail.
392
+ - Catch `PathError` and check `.recoverable` — if true, retry is safe; if false, do not retry
393
+ - Call `validate_integration` on your code before presenting it to the developer
394
+ - Import both `PathTerminalSDK` and `PathCoreModels`
395
+ - When wiring the BLE backend, add `NSBluetoothAlwaysUsageDescription` to Info.plist —
396
+ without it iOS blocks Bluetooth scanning **silently** (empty device list, no error)
397
+
398
+ **NEVER:**
399
+ - Use the raw `TransactionRequest` initialiser — always use the factory methods
400
+ - Store or log card data (maskedPan is acceptable, nothing else)
401
+ - Retry a financial operation without a new `idempotencyKey`
402
+ - Leave the existing default adapter in place — Path must be the injected adapter
403
+ - Leave auto-connect-on-launch running — it will connect the old adapter and bypass Path
404
+ - Remove the existing adapter from the codebase — it must remain available alongside Path
405
+ - Skip `PathError` handling
406
+ - Use `[some Any]` or `[any Any]` to store discovered devices. This is always wrong:
407
+ ```swift
408
+ // WRONG — will not compile when assigned from discoverDevices():
409
+ let devices: [some Any]
410
+ let devices: [any Any]
411
+
412
+ // CORRECT — use the concrete SDK type directly:
413
+ let devices: [DiscoveredDevice]
414
+ ```
415
+ `DiscoveredDevice` is a concrete type from `PathCoreModels` with a `.name` property.
416
+ - Reference a type (e.g. `TerminalDeviceInfo`) in one file before adding it to the
417
+ models file — add shared types to the models file first, then reference them
418
+ - Parse device names from string descriptions — use `device.name` directly
419
+ - Write "Pico W" anywhere in code, comments, or UI strings — always "Path POS Emulator"
420
+ - Guess at UUIDs when editing `project.pbxproj` — always read existing UUIDs
421
+ from the file itself. All productRef UUIDs for the SDK packages already exist
422
+ in the target's `packageProductDependencies` array — use those exact values.
423
+
424
+ **AMOUNTS:** Minor currency units only.
425
+ - `100` = £1.00 GBP · `1250` = £12.50 GBP · Never pass decimals.
426
+
427
+ **TIMEOUTS:**
428
+ - Never wrap `terminal.submitSale()` or `terminal.submitRefund()` in any app-level
429
+ timeout shorter than 30 seconds — the Path POS Emulator needs time to wait for
430
+ card/NFC presentation. The SDK handles its own 30-second response ceiling.
431
+ - Never call `terminal.connect()` automatically inside the payment or refund flow.
432
+ If the terminal is not connected when a payment is triggered, show an error
433
+ directing the user to Settings → Payment Terminal to reconnect. Do not attempt
434
+ to auto-reconnect mid-flow — a silent reconnect attempt masquerading as a
435
+ payment timeout is confusing and hard to recover from.
436
+
437
+ ---
438
+
439
+ ## After making changes
440
+
441
+ Summarise what you changed:
442
+ - Each file modified and the specific change made
443
+ - Confirm Step 1 (adapter injection) was completed
444
+
445
+ Then give **tailored connect instructions** — the user enters the connection
446
+ details themselves, so tell them exactly where you wired things in:
447
+ - The Settings screen / section name you added the backend switcher to
448
+ - How to connect to the emulator over **Wi-Fi/IP** (the default): which backend to
449
+ pick, where to type the emulator's IP, which button connects
450
+ - How to also connect over **Bluetooth** if they prefer (Scan → Connect)
451
+ - How to **switch to Verifone** when they're ready: pick Verifone, enter the
452
+ terminal IP, confirm the stored login, connect
453
+ - That the **merchant logo** appears on the Verifone customer display on connect
454
+ (Step 2d), and where to drop in their own logo asset (keep it a small ~360px PNG)
455
+
456
+ Keep it brief and concrete — the user should be able to follow it without reading code.