path-terminal-init 0.2.9 → 0.2.12

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