path-terminal-init 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -0
- package/dist/cli.js +725 -0
- package/dist/init.js +6805 -0
- package/package.json +50 -0
- package/rules/android/path-integration-android.mdc +325 -0
- package/rules/ios/path-integration.mdc +393 -0
- package/rules/windows-10/path-integration-windows10.mdc +238 -0
- package/rules/windows-11/path-integration-windows11.mdc +230 -0
|
@@ -0,0 +1,393 @@
|
|
|
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
|
+
> grep -c "path-terminal-sdk\|PathTerminalSDK" $(find . -maxdepth 2 -name "project.pbxproj" ! -path "*/DerivedData/*" | head -1)
|
|
28
|
+
> ```
|
|
29
|
+
> - **Count > 0** — package is already in the project. Do not mention SPM or
|
|
30
|
+
> package installation anywhere in your output. Proceed directly to Step 1.
|
|
31
|
+
> - **Count = 0** — package is not yet added. Tell the developer to add it:
|
|
32
|
+
> 1. Open Xcode → **File → Add Package Dependencies…**
|
|
33
|
+
> 2. Enter URL: `https://github.com/keyman12/path-terminal-sdk`
|
|
34
|
+
> 3. In the **Add to Target** sheet, tick **all four** products:
|
|
35
|
+
> `PathTerminalSDK`, `PathEmulatorAdapter`, `PathCoreModels`,
|
|
36
|
+
> `PathVerifonePSDKAdapter`
|
|
37
|
+
> (PathDiagnostics can be left as None)
|
|
38
|
+
> `PathVerifonePSDKAdapter` is required for the Verifone backend (Step 2c).
|
|
39
|
+
> 4. Click **Add Package**
|
|
40
|
+
> Then stop and ask the developer to confirm the packages have been added
|
|
41
|
+
> before you continue.
|
|
42
|
+
|
|
43
|
+
> **Language override.** Never write "Pico W" in any output or code comment.
|
|
44
|
+
> Always use "Path POS Emulator" when referring to the physical device.
|
|
45
|
+
|
|
46
|
+
> **Confirm the correct project directory first.** Run `pwd` before doing
|
|
47
|
+
> anything. If the path contains `.Trash`, `tmp`, or `worktree`, stop immediately
|
|
48
|
+
> — you are not in the real project. Ask the developer to relaunch Claude Code
|
|
49
|
+
> from the correct project directory. Never make changes from a Trash or temp path.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Your job — four steps, in this order
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
### Step 1 — Disconnect the existing terminal wiring
|
|
58
|
+
|
|
59
|
+
Find the app entry point (`@main` App struct or AppDelegate) and **read the actual
|
|
60
|
+
code** — do not rely on comments or file names alone.
|
|
61
|
+
|
|
62
|
+
Check the exact argument in the terminal manager initialisation call. A comment
|
|
63
|
+
saying *"pass a different adapter"* does not mean it has been done — read the code.
|
|
64
|
+
|
|
65
|
+
**You are looking for this pattern:**
|
|
66
|
+
```swift
|
|
67
|
+
// THIS is wired to Path — correct:
|
|
68
|
+
TerminalManager(adapter: PathPaymentTerminalAdapter())
|
|
69
|
+
|
|
70
|
+
// THIS is NOT wired to Path — fix it:
|
|
71
|
+
TerminalManager()
|
|
72
|
+
TerminalManager(adapter: SomeOtherAdapter())
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
If the call does not pass `PathPaymentTerminalAdapter()`, change it now.
|
|
76
|
+
|
|
77
|
+
> **The class must be named exactly `PathPaymentTerminalAdapter`.** This name is
|
|
78
|
+
> referenced across CLAUDE.md and the existing codebase. Do not use
|
|
79
|
+
> `PathPaymentAdapter`, `PathAdapter`, or any other variation.
|
|
80
|
+
|
|
81
|
+
**Also disable any auto-connect on launch** — find calls to `.connect()` in
|
|
82
|
+
`.onAppear`, `init`, or app lifecycle methods that fire automatically on start.
|
|
83
|
+
Remove them. Path connects manually via Settings, not on launch.
|
|
84
|
+
|
|
85
|
+
> Do this before anything else. Until this is changed, all payments go through
|
|
86
|
+
> the existing adapter no matter what else you do.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
### Step 2 — Add a backend switcher to Settings
|
|
91
|
+
|
|
92
|
+
The purpose of this integration is: **prove the payment functions against the Path
|
|
93
|
+
POS Emulator, then let the user switch — themselves, in the app — to a real Verifone
|
|
94
|
+
terminal when they are ready.** So Settings must let the user pick *which* terminal to
|
|
95
|
+
connect to. Do NOT hardcode one transport.
|
|
96
|
+
|
|
97
|
+
> **Connection details are entered by the user in the app, not by you.** You wire the
|
|
98
|
+
> switcher, the IP field, and the credential storage. You do NOT ask the user for an IP
|
|
99
|
+
> or password during this run — you build the screen, ship sensible test defaults, and
|
|
100
|
+
> describe how to use it in your end-of-run summary (see Step 4).
|
|
101
|
+
|
|
102
|
+
Find the Settings screen (a view named Settings, Preferences, or similar). Add a
|
|
103
|
+
**Path POS Adapter** section with a backend picker offering **three** backends:
|
|
104
|
+
|
|
105
|
+
| Backend | Transport | Adapter | Needs |
|
|
106
|
+
|---|---|---|---|
|
|
107
|
+
| **Emulator (Wi-Fi)** — *default selection* | TCP/IP | `TcpPathTerminalAdapter(host:)` | emulator IP |
|
|
108
|
+
| **Emulator (Bluetooth)** | BLE | `BLEPathTerminalAdapter()` | scan + pick device |
|
|
109
|
+
| **Verifone** | TCP/IP (PSDK) | `VerifonePSDKAdapter(config:)` | terminal IP + stored login |
|
|
110
|
+
|
|
111
|
+
Mirror the demo app's proven pattern exactly — **call `get_code_example` with operation
|
|
112
|
+
`"backend-switch"` first** and reproduce its `makeAdapter(for:)` shape. All sale / refund /
|
|
113
|
+
void / receipt code is backend-agnostic; only adapter construction differs.
|
|
114
|
+
|
|
115
|
+
**Persist the user's selection and connection details** (so the choice survives a relaunch).
|
|
116
|
+
Store these in `UserDefaults` behind a small settings type, with test defaults so a fresh
|
|
117
|
+
clone connects without typing anything:
|
|
118
|
+
```swift
|
|
119
|
+
enum TerminalBackend: String, CaseIterable, Identifiable {
|
|
120
|
+
case emulatorWifi = "emulator_wifi" // default
|
|
121
|
+
case emulatorBLE = "emulator_ble"
|
|
122
|
+
case verifone = "verifone"
|
|
123
|
+
var id: String { rawValue }
|
|
124
|
+
}
|
|
125
|
+
// Stored keys: backend, emulatorHost, verifoneHost,
|
|
126
|
+
// loginUsername, loginPassword, loginShift, refundPassword
|
|
127
|
+
// Test defaults: verifoneHost "192.168.1.88", username "user",
|
|
128
|
+
// password "password123", shift "shift123"
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
#### Step 2a — Emulator over Wi-Fi (the default) and Verifone — IP entry
|
|
132
|
+
|
|
133
|
+
For the two IP-addressed backends, show a **host/IP text field** (label it
|
|
134
|
+
"Emulator IP" or "Verifone terminal IP" depending on the selection) plus an
|
|
135
|
+
**Apply & Connect** button. On connect, build the matching adapter from the stored
|
|
136
|
+
host and rebuild `PathTerminal` (disconnect the previous adapter first — real
|
|
137
|
+
terminals and the emulator's Wi-Fi mode allow only **one** client at a time).
|
|
138
|
+
|
|
139
|
+
#### Step 2b — Emulator over Bluetooth — scan and connect
|
|
140
|
+
|
|
141
|
+
For the BLE backend, keep the scan/connect flow:
|
|
142
|
+
1. **Scan for Path Terminals** — triggers BLE discovery
|
|
143
|
+
2. Discovered devices list — each with a **Connect** button
|
|
144
|
+
3. Live connection state (Scanning… / Connected / Disconnected)
|
|
145
|
+
4. **Disconnect** when connected
|
|
146
|
+
|
|
147
|
+
To support this, add scan/connect to the manager and adapter.
|
|
148
|
+
|
|
149
|
+
**Add to `PaymentTerminalAdapter` protocol** (with no-op default implementations
|
|
150
|
+
so any other existing adapter requires no changes):
|
|
151
|
+
```swift
|
|
152
|
+
func scanForDevices() async throws -> [TerminalDeviceInfo]
|
|
153
|
+
func connectToDevice(id: String) async throws
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Add `TerminalDeviceInfo`** to the terminal models file **before** writing any
|
|
157
|
+
code that references it — other files will fail to compile if this is added last:
|
|
158
|
+
```swift
|
|
159
|
+
struct TerminalDeviceInfo: Identifiable, Equatable {
|
|
160
|
+
let id: String
|
|
161
|
+
let name: String
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Implement in `PathPaymentTerminalAdapter`** — call `get_code_example` with
|
|
166
|
+
operation `"discover"` first. The SDK exposes a concrete `DiscoveredDevice` type
|
|
167
|
+
with a `.name` property — use it directly. Do not use `[some Any]` or `[any Any]`
|
|
168
|
+
to store devices, and do not parse names from string descriptions. Then implement:
|
|
169
|
+
- `scanForDevices()` — calls `pathTerminal.discoverDevices()`, maps results to
|
|
170
|
+
`[TerminalDeviceInfo]`, stores a closure per device (so the SDK's opaque
|
|
171
|
+
device type never leaks into the rest of the app)
|
|
172
|
+
- `connectToDevice(id:)` — looks up the stored closure by id and calls it
|
|
173
|
+
|
|
174
|
+
> **TCP and Verifone need no scan** — `discoverDevices()` returns one synthetic
|
|
175
|
+
> device for the configured host, so "Apply & Connect" can connect directly.
|
|
176
|
+
|
|
177
|
+
**Expose on the terminal manager:**
|
|
178
|
+
```swift
|
|
179
|
+
@Published private(set) var discoveredDevices: [TerminalDeviceInfo] = []
|
|
180
|
+
@Published private(set) var isScanning: Bool = false
|
|
181
|
+
|
|
182
|
+
func scanForDevices() async { ... }
|
|
183
|
+
func connectToDevice(_ device: TerminalDeviceInfo) async { ... }
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
#### Step 2c — Verifone backend and stored credentials
|
|
187
|
+
|
|
188
|
+
The Verifone backend connects to a real terminal over the PSDK. Call
|
|
189
|
+
`get_code_example` with operation `"verifone-init"` first. Construct it from a
|
|
190
|
+
`VerifoneTerminalConfig` built from the **stored** login fields — never hardcode
|
|
191
|
+
credentials inline:
|
|
192
|
+
```swift
|
|
193
|
+
import PathVerifonePSDKAdapter // separate SPM product — ticked at install
|
|
194
|
+
|
|
195
|
+
let adapter = VerifonePSDKAdapter(config: VerifoneTerminalConfig(
|
|
196
|
+
host: settings.verifoneHost,
|
|
197
|
+
username: settings.loginUsername,
|
|
198
|
+
password: settings.loginPassword,
|
|
199
|
+
shift: settings.loginShift,
|
|
200
|
+
refundPassword: settings.refundPassword
|
|
201
|
+
))
|
|
202
|
+
```
|
|
203
|
+
Keep the Verifone login fields **separate from the emulator settings** (their own
|
|
204
|
+
stored keys, their own editable fields in Settings) — cleaner than reusing the
|
|
205
|
+
emulator's. All terminals are treated as **generic Verifone** (same AGPA app, same
|
|
206
|
+
PSDK connect/login), so no per-terminal configuration is needed beyond IP + login.
|
|
207
|
+
|
|
208
|
+
#### After creating PathPaymentTerminalAdapter.swift — confirm the SDK is linked
|
|
209
|
+
|
|
210
|
+
Before triggering the first build, run:
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
grep -c "PBXFrameworksBuildPhase" $(find . -maxdepth 2 -name "project.pbxproj" ! -path "*/DerivedData/*" | head -1)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
If the count is **0**, the Frameworks build phase is missing and the build will
|
|
217
|
+
fail with `Undefined symbol` linker errors for PathTerminalSDK / PathEmulatorAdapter
|
|
218
|
+
/ PathCoreModels. Fix it now — do not wait for the build to fail.
|
|
219
|
+
|
|
220
|
+
**How to add the missing Frameworks build phase:**
|
|
221
|
+
|
|
222
|
+
1. Find the four package product UUIDs from the target's `packageProductDependencies`:
|
|
223
|
+
```
|
|
224
|
+
grep -A 8 "packageProductDependencies" $(find . -maxdepth 2 -name "project.pbxproj" ! -path "*/DerivedData/*" | head -1)
|
|
225
|
+
```
|
|
226
|
+
You will see something like:
|
|
227
|
+
```
|
|
228
|
+
packageProductDependencies = (
|
|
229
|
+
AAAA1111BBBB2222 /* PathCoreModels */,
|
|
230
|
+
CCCC3333DDDD4444 /* PathEmulatorAdapter */,
|
|
231
|
+
EEEE5555FFFF6666 /* PathTerminalSDK */,
|
|
232
|
+
7777888899990000 /* PathVerifonePSDKAdapter */,
|
|
233
|
+
);
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
2. In the `/* Begin PBXBuildFile section */`, add one entry per product. Use
|
|
237
|
+
**fresh UUIDs** for the build file entries (not the productRef UUIDs):
|
|
238
|
+
```
|
|
239
|
+
A1A1A1A1B2B2B2B2 /* PathCoreModels in Frameworks */ = {isa = PBXBuildFile; productRef = AAAA1111BBBB2222 /* PathCoreModels */; };
|
|
240
|
+
C3C3C3C3D4D4D4D4 /* PathEmulatorAdapter in Frameworks */ = {isa = PBXBuildFile; productRef = CCCC3333DDDD4444 /* PathEmulatorAdapter */; };
|
|
241
|
+
E5E5E5E5F6F6F6F6 /* PathTerminalSDK in Frameworks */ = {isa = PBXBuildFile; productRef = EEEE5555FFFF6666 /* PathTerminalSDK */; };
|
|
242
|
+
1212343456567878 /* PathVerifonePSDKAdapter in Frameworks */ = {isa = PBXBuildFile; productRef = 7777888899990000 /* PathVerifonePSDKAdapter */; };
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
3. Add a `PBXFrameworksBuildPhase` section (before `PBXResourcesBuildPhase`):
|
|
246
|
+
```
|
|
247
|
+
/* Begin PBXFrameworksBuildPhase section */
|
|
248
|
+
F7F7F7F7A8A8A8A8 /* Frameworks */ = {
|
|
249
|
+
isa = PBXFrameworksBuildPhase;
|
|
250
|
+
buildActionMask = 2147483647;
|
|
251
|
+
files = (
|
|
252
|
+
A1A1A1A1B2B2B2B2 /* PathCoreModels in Frameworks */,
|
|
253
|
+
C3C3C3C3D4D4D4D4 /* PathEmulatorAdapter in Frameworks */,
|
|
254
|
+
E5E5E5E5F6F6F6F6 /* PathTerminalSDK in Frameworks */,
|
|
255
|
+
1212343456567878 /* PathVerifonePSDKAdapter in Frameworks */,
|
|
256
|
+
);
|
|
257
|
+
runOnlyForDeploymentPostprocessing = 0;
|
|
258
|
+
};
|
|
259
|
+
/* End PBXFrameworksBuildPhase section */
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
4. Add the Frameworks phase UUID to the target's `buildPhases` array:
|
|
263
|
+
```
|
|
264
|
+
buildPhases = (
|
|
265
|
+
<Sources UUID>,
|
|
266
|
+
F7F7F7F7A8A8A8A8 /* Frameworks */, ← add this line
|
|
267
|
+
<Resources UUID>,
|
|
268
|
+
);
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Replace every UUID shown above with freshly generated unique hex strings.
|
|
272
|
+
**Never reuse an existing UUID from the file.** The productRef values (step 1)
|
|
273
|
+
are the only values copied from the existing file.
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
### Step 3 — Wire sale and refund through the Path SDK
|
|
278
|
+
|
|
279
|
+
If `PathPaymentTerminalAdapter.swift` already exists and implements `submitSale()`
|
|
280
|
+
and `submitRefund()`, **no changes are needed to the payment flow** — once Step 1
|
|
281
|
+
is done, all sales and refunds automatically go through Path.
|
|
282
|
+
|
|
283
|
+
Call `validate_integration` with the existing `PathPaymentTerminalAdapter.swift`
|
|
284
|
+
to confirm it is correct. Fix any warnings before proceeding.
|
|
285
|
+
|
|
286
|
+
If `PathPaymentTerminalAdapter.swift` does not exist, create it:
|
|
287
|
+
- Call `get_code_example` with operation `"sale"` before writing sale code
|
|
288
|
+
- Call `get_code_example` with operation `"refund"` before writing refund code
|
|
289
|
+
- Call `validate_integration` on your code before finalising
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
### Step 4 — Verify it works end to end
|
|
294
|
+
|
|
295
|
+
1. Confirm the app entry point injects `PathPaymentTerminalAdapter()`.
|
|
296
|
+
If the old adapter is still the default, go back to Step 1.
|
|
297
|
+
|
|
298
|
+
2. Confirm Info.plist has BLE permissions (needed for the Bluetooth backend).
|
|
299
|
+
Call `get_info_plist_requirements` if unsure.
|
|
300
|
+
|
|
301
|
+
3. **Prove on the emulator first (the default path — Wi-Fi/IP):**
|
|
302
|
+
- Put the Path POS Emulator in Wi-Fi mode (Config → Connection on the device);
|
|
303
|
+
it shows its `IP:port` on the welcome screen.
|
|
304
|
+
- Run the app → Settings → Path POS Adapter → backend **Emulator (Wi-Fi)** →
|
|
305
|
+
type that IP → **Apply & Connect**. Connection state should reach `.connected`.
|
|
306
|
+
- (Optionally also prove **Emulator (Bluetooth)**: select it, **Scan**, **Connect**.)
|
|
307
|
+
|
|
308
|
+
4. Return to the EPOS. Add items to the cart. Trigger a card payment.
|
|
309
|
+
When prompted, present a card / tap the NFC tag on the Path POS Emulator.
|
|
310
|
+
|
|
311
|
+
5. Verify:
|
|
312
|
+
- `result.state == .approved`
|
|
313
|
+
- `result.transactionId` is non-nil
|
|
314
|
+
- Receipt data is populated (call `get_code_example` with operation `"receipt"`)
|
|
315
|
+
|
|
316
|
+
6. **Then prove the switch to Verifone:** select backend **Verifone**, enter the
|
|
317
|
+
terminal's IP and confirm the stored login, **Apply & Connect** — the SDK runs
|
|
318
|
+
the PSDK init + login. The same sale/refund/void flow now runs against the real
|
|
319
|
+
terminal, unchanged. (Only one client may connect to a Verifone terminal at a
|
|
320
|
+
time — disconnect any other POS first.)
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## SDK rules — always follow these
|
|
325
|
+
|
|
326
|
+
**ALWAYS:**
|
|
327
|
+
- Call `get_code_example` before writing any SDK code
|
|
328
|
+
- Use `TransactionRequest.sale(...)` and `TransactionRequest.refund(...)` factory methods
|
|
329
|
+
- Use `RequestEnvelope.create(sdkVersion:adapterVersion:)` — never build envelopes manually
|
|
330
|
+
- Handle all `TransactionState` cases: `.approved`, `.declined`, `.timedOut`, `.failed`
|
|
331
|
+
- For **refunds**, the success state is `.refunded` — **not** `.approved`. Using
|
|
332
|
+
`.approved` in a refund switch will never match and every refund will appear to fail.
|
|
333
|
+
- Catch `PathError` and check `.recoverable` — if true, retry is safe; if false, do not retry
|
|
334
|
+
- Call `validate_integration` on your code before presenting it to the developer
|
|
335
|
+
- Import both `PathTerminalSDK` and `PathCoreModels`
|
|
336
|
+
|
|
337
|
+
**NEVER:**
|
|
338
|
+
- Use the raw `TransactionRequest` initialiser — always use the factory methods
|
|
339
|
+
- Store or log card data (maskedPan is acceptable, nothing else)
|
|
340
|
+
- Retry a financial operation without a new `idempotencyKey`
|
|
341
|
+
- Leave the existing default adapter in place — Path must be the injected adapter
|
|
342
|
+
- Leave auto-connect-on-launch running — it will connect the old adapter and bypass Path
|
|
343
|
+
- Remove the existing adapter from the codebase — it must remain available alongside Path
|
|
344
|
+
- Skip `PathError` handling
|
|
345
|
+
- Use `[some Any]` or `[any Any]` to store discovered devices. This is always wrong:
|
|
346
|
+
```swift
|
|
347
|
+
// WRONG — will not compile when assigned from discoverDevices():
|
|
348
|
+
let devices: [some Any]
|
|
349
|
+
let devices: [any Any]
|
|
350
|
+
|
|
351
|
+
// CORRECT — use the concrete SDK type directly:
|
|
352
|
+
let devices: [DiscoveredDevice]
|
|
353
|
+
```
|
|
354
|
+
`DiscoveredDevice` is a concrete type from `PathCoreModels` with a `.name` property.
|
|
355
|
+
- Reference a type (e.g. `TerminalDeviceInfo`) in one file before adding it to the
|
|
356
|
+
models file — add shared types to the models file first, then reference them
|
|
357
|
+
- Parse device names from string descriptions — use `device.name` directly
|
|
358
|
+
- Write "Pico W" anywhere in code, comments, or UI strings — always "Path POS Emulator"
|
|
359
|
+
- Guess at UUIDs when editing `project.pbxproj` — always read existing UUIDs
|
|
360
|
+
from the file itself. All productRef UUIDs for the SDK packages already exist
|
|
361
|
+
in the target's `packageProductDependencies` array — use those exact values.
|
|
362
|
+
|
|
363
|
+
**AMOUNTS:** Minor currency units only.
|
|
364
|
+
- `100` = £1.00 GBP · `1250` = £12.50 GBP · Never pass decimals.
|
|
365
|
+
|
|
366
|
+
**TIMEOUTS:**
|
|
367
|
+
- Never wrap `terminal.submitSale()` or `terminal.submitRefund()` in any app-level
|
|
368
|
+
timeout shorter than 30 seconds — the Path POS Emulator needs time to wait for
|
|
369
|
+
card/NFC presentation. The SDK handles its own 30-second response ceiling.
|
|
370
|
+
- Never call `terminal.connect()` automatically inside the payment or refund flow.
|
|
371
|
+
If the terminal is not connected when a payment is triggered, show an error
|
|
372
|
+
directing the user to Settings → Payment Terminal to reconnect. Do not attempt
|
|
373
|
+
to auto-reconnect mid-flow — a silent reconnect attempt masquerading as a
|
|
374
|
+
payment timeout is confusing and hard to recover from.
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## After making changes
|
|
379
|
+
|
|
380
|
+
Summarise what you changed:
|
|
381
|
+
- Each file modified and the specific change made
|
|
382
|
+
- Confirm Step 1 (adapter injection) was completed
|
|
383
|
+
|
|
384
|
+
Then give **tailored connect instructions** — the user enters the connection
|
|
385
|
+
details themselves, so tell them exactly where you wired things in:
|
|
386
|
+
- The Settings screen / section name you added the backend switcher to
|
|
387
|
+
- How to connect to the emulator over **Wi-Fi/IP** (the default): which backend to
|
|
388
|
+
pick, where to type the emulator's IP, which button connects
|
|
389
|
+
- How to also connect over **Bluetooth** if they prefer (Scan → Connect)
|
|
390
|
+
- How to **switch to Verifone** when they're ready: pick Verifone, enter the
|
|
391
|
+
terminal IP, confirm the stored login, connect
|
|
392
|
+
|
|
393
|
+
Keep it brief and concrete — the user should be able to follow it without reading code.
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: >
|
|
3
|
+
Rules for AI agents integrating the Path Terminal SDK into a Windows 10 EPOS
|
|
4
|
+
application — the legacy WPF / .NET Framework 4.8 stack. Apply when the user asks
|
|
5
|
+
to integrate payments, connect to a payment terminal, wire a sale or refund, or
|
|
6
|
+
set up the Path emulator on a WPF app. For the modern WinUI 3 / .NET 10 stack use
|
|
7
|
+
path-integration-windows11.mdc instead.
|
|
8
|
+
globs:
|
|
9
|
+
- "**/*.cs"
|
|
10
|
+
- "**/*.csproj"
|
|
11
|
+
- "**/*.xaml"
|
|
12
|
+
- "**/app.manifest"
|
|
13
|
+
alwaysApply: false
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Path Terminal SDK — Windows 10 (WPF / .NET Framework 4.8) Integration Rules
|
|
17
|
+
|
|
18
|
+
You are integrating the Path Terminal SDK into an existing **WPF / .NET Framework 4.8**
|
|
19
|
+
Windows EPOS application. This app is a **developer EPOS system** that replicates a
|
|
20
|
+
production environment.
|
|
21
|
+
|
|
22
|
+
> **Which recipe am I in?** This is the **Windows 10** recipe — legacy WPF,
|
|
23
|
+
> `net48`, `<UseWPF>true`. If the project is WinUI 3 / `net10.0-windows`, STOP and
|
|
24
|
+
> use `path-integration-windows11.mdc` instead. The two stacks differ in app shell,
|
|
25
|
+
> UI threading, and settings storage.
|
|
26
|
+
|
|
27
|
+
You have access to the Path MCP server at mcp.path2ai.tech:
|
|
28
|
+
- Tools: `get_code_example` (use platform='windows'), `explain_error`. Always call
|
|
29
|
+
`get_code_example` before writing SDK code — never guess at SDK patterns.
|
|
30
|
+
|
|
31
|
+
> **Language override.** Never write "Pico W" in any output or code comment.
|
|
32
|
+
> Always use "Path POS Emulator" when referring to the physical device.
|
|
33
|
+
|
|
34
|
+
> **Confirm the correct project directory first.** Run `Get-Location` before anything.
|
|
35
|
+
> If the path contains `.Trash`, `tmp`, or `worktree`, stop — ask the developer to
|
|
36
|
+
> relaunch from the real project directory.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## The purpose of this integration
|
|
41
|
+
|
|
42
|
+
Wire the SDK so the user can **prove the payment functions against the Path POS
|
|
43
|
+
Emulator, then switch — themselves, in the app — to a real Verifone terminal when
|
|
44
|
+
ready.** So Settings gets a **backend switcher**, not one hardcoded transport.
|
|
45
|
+
|
|
46
|
+
> **You wire the capability; the user enters the connection details.** You build the
|
|
47
|
+
> switcher, the IP field, and the credential storage, and ship sensible test
|
|
48
|
+
> defaults. You do NOT ask the user for an IP or password during this run — you
|
|
49
|
+
> describe how to use the screen in your end-of-run summary (Step 4).
|
|
50
|
+
|
|
51
|
+
The Windows 10 **demo app** (`Path-epos-demo-sdk-Windows10`) already implements this
|
|
52
|
+
exact pattern — its `TerminalService.Build()` is the reference. Mirror it.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Step 0 — Reference the SDK (sibling-folder ProjectReference)
|
|
57
|
+
|
|
58
|
+
The SDK is consumed by relative `ProjectReference`, and the SDK repo (named
|
|
59
|
+
`Path-terminal-sdk-windows11` — one repo serves both Windows stacks) must be cloned as
|
|
60
|
+
a **sibling** of this app. On `net48` the build automatically picks the SDK's
|
|
61
|
+
`netstandard2.0` core flavour and the `net48` adapter flavours. Call `get_code_example`
|
|
62
|
+
with operation='install' and platform='windows', then add these six references to the
|
|
63
|
+
app's `.csproj` (adjust the `..\` depth to your project's location):
|
|
64
|
+
|
|
65
|
+
```xml
|
|
66
|
+
<ItemGroup>
|
|
67
|
+
<ProjectReference Include="..\..\..\Path-terminal-sdk-windows11\Path.Terminal.CoreModels\Path.Terminal.CoreModels.csproj" />
|
|
68
|
+
<ProjectReference Include="..\..\..\Path-terminal-sdk-windows11\Path.Terminal.Sdk\Path.Terminal.Sdk.csproj" />
|
|
69
|
+
<ProjectReference Include="..\..\..\Path-terminal-sdk-windows11\Path.Terminal.EmulatorAdapter\Path.Terminal.EmulatorAdapter.csproj" />
|
|
70
|
+
<ProjectReference Include="..\..\..\Path-terminal-sdk-windows11\Path.Terminal.MockAdapter\Path.Terminal.MockAdapter.csproj" />
|
|
71
|
+
<ProjectReference Include="..\..\..\Path-terminal-sdk-windows11\Path.Terminal.Diagnostics\Path.Terminal.Diagnostics.csproj" />
|
|
72
|
+
<!-- Verifone backend (Step 2c) — the net48 flavour of the PSDK adapter (PSDK 3.68.27 .NET FW binding). -->
|
|
73
|
+
<ProjectReference Include="..\..\..\Path-terminal-sdk-windows11\Path.Terminal.PsdkAdapter\Path.Terminal.PsdkAdapter.csproj" />
|
|
74
|
+
</ItemGroup>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
> **Bluetooth:** WPF/.NET FW reaches BLE through WinRT interop, which the EmulatorAdapter
|
|
78
|
+
> already brings in (`Microsoft.Windows.SDK.Contracts`). The generic `app.manifest`
|
|
79
|
+
> needs **no** Bluetooth declaration. The emulator Wi-Fi (TCP) and Verifone backends use
|
|
80
|
+
> no Bluetooth at all.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Your job — four steps, in this order
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
### Step 1 — Disconnect the existing loopback wiring
|
|
89
|
+
|
|
90
|
+
The harness ships with a **loopback** adapter, `OCPayTerminalAdapter`, wired up where the
|
|
91
|
+
terminal service is constructed (look in `MainWindow` / `App.xaml.cs` / the terminal
|
|
92
|
+
service). **Read the actual code** — a comment is not proof.
|
|
93
|
+
|
|
94
|
+
Create a Path-backed terminal service that builds `PathTerminal` from the selected
|
|
95
|
+
backend (mirror the demo's `TerminalService.Build()`), and swap the app to use it
|
|
96
|
+
instead of the OCPay wiring.
|
|
97
|
+
|
|
98
|
+
> Keep `OCPayTerminalAdapter` in the codebase (it is the shipped loopback default) —
|
|
99
|
+
> just stop wiring it as the live adapter. Do not delete it.
|
|
100
|
+
|
|
101
|
+
**Also disable any auto-connect on launch** — the Path backends connect manually from
|
|
102
|
+
Settings, never automatically on start.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
### Step 2 — Add a backend switcher to Settings
|
|
107
|
+
|
|
108
|
+
The harness already has a Settings view. Add a **Path POS Adapter** section with a
|
|
109
|
+
backend picker. **Call `get_code_example` with operation='backend-switch' and
|
|
110
|
+
platform='windows' first** and reproduce its `BuildTerminal` shape (the demo's
|
|
111
|
+
`Build()` uses the same construction):
|
|
112
|
+
|
|
113
|
+
| Backend | Transport | Adapter | Needs |
|
|
114
|
+
|---|---|---|---|
|
|
115
|
+
| **Emulator (Wi-Fi)** — *default* | TCP/IP | `new TcpPathTerminalAdapter(host, …)` | emulator IP |
|
|
116
|
+
| **Emulator (Bluetooth)** | BLE | `new BlePathTerminalAdapter(…)` | scan + connect |
|
|
117
|
+
| **Verifone** | TCP/IP (PSDK) | `new VerifonePsdkAdapter(config, …)` | terminal IP + stored login |
|
|
118
|
+
|
|
119
|
+
```csharp
|
|
120
|
+
public enum TerminalBackend { EmulatorBle, EmulatorWifi, Verifone } // EmulatorWifi = the default selection
|
|
121
|
+
```
|
|
122
|
+
(The demo also includes a `Mock` case — optional; not required for this integration.)
|
|
123
|
+
|
|
124
|
+
**Persist the selection and connection details** in the harness's settings store (the
|
|
125
|
+
demo uses an `AppSettings` class writing JSON to
|
|
126
|
+
`%LOCALAPPDATA%\<App>\settings.json` via a DTO). Store: `Backend`, `EmulatorHost`,
|
|
127
|
+
`VerifoneHost`, `LoginUsername`, `LoginPassword`, `LoginShift`, `RefundPassword` — with
|
|
128
|
+
test defaults (`VerifoneHost` "192.168.1.88", username "user", password "password123",
|
|
129
|
+
shift "shift123") so a fresh clone connects without typing.
|
|
130
|
+
|
|
131
|
+
#### Step 2a — Emulator (Wi-Fi, the default) and Verifone — IP entry
|
|
132
|
+
|
|
133
|
+
For the two IP-addressed backends show a **host/IP text box** (label "Emulator IP" or
|
|
134
|
+
"Verifone terminal IP" by selection) and an **Apply & Connect** button. On connect,
|
|
135
|
+
build the matching adapter from the stored host, dispose the previous `PathTerminal`,
|
|
136
|
+
and build a new one (only **one** client per terminal / per emulator Wi-Fi session).
|
|
137
|
+
TCP and Verifone need no scan — `DiscoverDevicesAsync()` returns one synthetic device
|
|
138
|
+
for the host, so Apply & Connect connects directly.
|
|
139
|
+
|
|
140
|
+
#### Step 2b — Emulator (Bluetooth) — scan and connect
|
|
141
|
+
|
|
142
|
+
For the BLE backend keep a scan/connect flow: a **Scan** button → `DiscoverDevicesAsync()`
|
|
143
|
+
→ list devices → **Connect** → live connection state from the `Events` stream. Marshal
|
|
144
|
+
UI updates onto the WPF UI thread (`Application.Current.Dispatcher`).
|
|
145
|
+
|
|
146
|
+
#### Step 2c — Verifone backend and stored credentials
|
|
147
|
+
|
|
148
|
+
Call `get_code_example` with operation='verifone-init' and platform='windows'.
|
|
149
|
+
Construct it from a `VerifoneTerminalConfig` built from the **stored** login — never
|
|
150
|
+
hardcode credentials inline:
|
|
151
|
+
```csharp
|
|
152
|
+
using Path.Terminal.PsdkAdapter;
|
|
153
|
+
var adapter = new VerifonePsdkAdapter(
|
|
154
|
+
new VerifoneTerminalConfig {
|
|
155
|
+
Host = settings.VerifoneHost,
|
|
156
|
+
Username = settings.LoginUsername,
|
|
157
|
+
Password = settings.LoginPassword,
|
|
158
|
+
Shift = settings.LoginShift,
|
|
159
|
+
RefundPassword = settings.RefundPassword,
|
|
160
|
+
},
|
|
161
|
+
log: Log);
|
|
162
|
+
```
|
|
163
|
+
Keep the Verifone login fields **separate** (their own stored keys + editable fields).
|
|
164
|
+
All terminals are treated as **generic Verifone** (same AGPA app, same PSDK
|
|
165
|
+
connect/login) — no per-terminal config beyond IP + login.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
### Step 3 — Wire sale, refund and void through the Path SDK
|
|
170
|
+
|
|
171
|
+
Once Step 1 is done, the harness's existing payment flow calls the Path-backed service.
|
|
172
|
+
Confirm the transaction methods map correctly:
|
|
173
|
+
- Call `get_code_example` operation='sale' (platform='windows') — handle `TransactionState.Approved`, `Declined`, `TimedOut`, `Failed`, default.
|
|
174
|
+
- Call `get_code_example` operation='refund' — pass `originalTransactionId` from the sale result.
|
|
175
|
+
- Call `get_code_example` operation='void' — full reversal; success state is `Reversed`.
|
|
176
|
+
- Amounts are **minor units** (`420` = £4.20). Never pass decimals.
|
|
177
|
+
- Catch `PathException` and check whether it is recoverable before retrying.
|
|
178
|
+
- SDK calls are async (`Task`). From WPF event handlers use `async`/`await`; never block
|
|
179
|
+
the UI thread with `.Result` / `.Wait()`.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
### Step 4 — Verify it works end to end
|
|
184
|
+
|
|
185
|
+
1. Confirm the app composes the Path-backed service, not `OCPayTerminalAdapter`.
|
|
186
|
+
2. **Prove on the emulator first (the default — Wi-Fi/IP):**
|
|
187
|
+
- Put the Path POS Emulator in Wi-Fi mode (Config → Connection); note the `IP:port`
|
|
188
|
+
on its welcome screen.
|
|
189
|
+
- Run the app → Settings → Path POS Adapter → **Emulator (Wi-Fi)** → type that IP →
|
|
190
|
+
**Apply & Connect**. Connection state should reach Connected.
|
|
191
|
+
- (Optionally prove **Emulator (Bluetooth)**: select it, **Scan**, **Connect**.)
|
|
192
|
+
3. Run a sale (amountMinor `100` = £1.00), present a card / tap the emulator's NFC tag,
|
|
193
|
+
verify `result.State == Approved`, `TransactionId` is non-null, and receipt data is
|
|
194
|
+
populated (`get_code_example` operation='receipt'). Run a refund and a void.
|
|
195
|
+
4. **Then prove the switch to Verifone:** select **Verifone**, enter the terminal IP and
|
|
196
|
+
confirm the stored login, **Apply & Connect** — the SDK runs the PSDK init + login and
|
|
197
|
+
the same flow runs against the real terminal, unchanged. One client per terminal —
|
|
198
|
+
disconnect any other POS first.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## SDK rules — always follow these
|
|
203
|
+
|
|
204
|
+
**ALWAYS:**
|
|
205
|
+
- Call `get_code_example` (platform='windows') before writing SDK code
|
|
206
|
+
- Use `TransactionRequest.Sale(...)` / `.Refund(...)` / `.Void(...)` factory methods
|
|
207
|
+
- Use `RequestEnvelope.Create(...)` — never build envelopes by hand
|
|
208
|
+
- For **refunds** the success state is the refunded/approved state, and for **voids** it is `Reversed` — not the sale's `Approved`
|
|
209
|
+
- Dispose the old `PathTerminal` before building a new one on a backend change (the demo's
|
|
210
|
+
`TerminalService` disposes synchronously — do the same; do not rely on `await using` on net48)
|
|
211
|
+
- Marshal UI updates from `Events` onto the WPF UI thread (`Application.Current.Dispatcher`)
|
|
212
|
+
|
|
213
|
+
**NEVER:**
|
|
214
|
+
- Use a raw `TransactionRequest` constructor — use the factory methods
|
|
215
|
+
- Store or log card data (masked PAN only)
|
|
216
|
+
- Retry a financial operation without a fresh `RequestEnvelope` (new idempotency key)
|
|
217
|
+
- Leave `OCPayTerminalAdapter` wired as the live adapter — Path must be the live one
|
|
218
|
+
- Leave auto-connect-on-launch running
|
|
219
|
+
- Block the UI thread on async SDK calls with `.Result` / `.Wait()`
|
|
220
|
+
- Set `OnCardRead` / `OnAck` on an adapter — `PathTerminal` owns those
|
|
221
|
+
- Write "Pico W" anywhere
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## After making changes
|
|
226
|
+
|
|
227
|
+
Summarise each file changed and confirm Step 1 (the live adapter is now Path-backed).
|
|
228
|
+
|
|
229
|
+
Then give **tailored connect instructions** — the user enters the connection details, so
|
|
230
|
+
tell them exactly where you wired things in:
|
|
231
|
+
- The Settings section you added the backend switcher to
|
|
232
|
+
- How to connect to the emulator over **Wi-Fi/IP** (the default): which backend, where to
|
|
233
|
+
type the emulator's IP, which button connects
|
|
234
|
+
- How to connect over **Bluetooth** instead (Scan → Connect)
|
|
235
|
+
- How to **switch to Verifone** when ready: pick Verifone, enter the terminal IP, confirm
|
|
236
|
+
the stored login, connect
|
|
237
|
+
|
|
238
|
+
Keep it brief and concrete — the user should follow it without reading code.
|