superroute-sapp 0.2.1__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Goodneo Solutions
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,400 @@
1
+ Metadata-Version: 2.4
2
+ Name: superroute-sapp
3
+ Version: 0.2.1
4
+ Summary: sApp for Python: sign-in, the platform app API, Open Platform capabilities and webhook verification for Superroute.
5
+ Author: Goodneo Solutions
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://open.superroute.ca/developer/sapp
8
+ Project-URL: Documentation, https://open.superroute.ca/developer/sapp
9
+ Keywords: superroute,logistics,delivery,sdk,webhooks
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Typing :: Typed
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Provides-Extra: test
17
+ Requires-Dist: pytest>=7; extra == "test"
18
+ Dynamic: license-file
19
+
20
+ <picture>
21
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Goodneo-Solutions/sapp-releases/main/assets/sapp-logo-dark.svg">
22
+ <img src="https://raw.githubusercontent.com/Goodneo-Solutions/sapp-releases/main/assets/sapp-logo.svg" alt="sApp" height="56">
23
+ </picture>
24
+
25
+ # sApp
26
+
27
+ Build apps on Superroute: customer and driver apps that run for one or more service providers, and apps that use
28
+ Open Platform capabilities such as routing, geocoding and ETA. MIT licence.
29
+
30
+ Documentation: [open.superroute.ca/developer/sapp](https://open.superroute.ca/developer/sapp). Swift packages, the
31
+ Maven repository, the Go module and the example apps: [Goodneo-Solutions/sapp-releases](https://github.com/Goodneo-Solutions/sapp-releases).
32
+ PHP: [Goodneo-Solutions/sapp-php](https://github.com/Goodneo-Solutions/sapp-php).
33
+
34
+ | Package | Platform | Status |
35
+ |---|---|---|
36
+ | [`@superrouteca/sapp`](https://www.npmjs.com/package/@superrouteca/sapp) | JavaScript and TypeScript: web, Node 20+, React Native | 0.2.1 |
37
+ | [`@superrouteca/sapp-react`](https://www.npmjs.com/package/@superrouteca/sapp-react) | React: provider and hooks | 0.2.1 |
38
+ | [`@superrouteca/sapp-react-native`](https://www.npmjs.com/package/@superrouteca/sapp-react-native) | React Native: hooks, secure token store | 0.2.1 |
39
+ | [`sapp`](https://pub.dev/packages/sapp) | Dart and Flutter | 0.2.1 |
40
+ | [`SApp`](https://github.com/Goodneo-Solutions/sapp-releases) (Swift Package Manager) | iOS 15+, macOS 12+ | 0.2.1 |
41
+ | [`ca.superroute:sapp` / `sapp-android`](https://github.com/Goodneo-Solutions/sapp-releases) (Maven) | Kotlin on the JVM and Android 7+ | 0.2.1 |
42
+ | [`Superroute.SApp`](https://www.nuget.org/packages/Superroute.SApp) (NuGet) | .NET 8: Windows apps and servers | 0.2.1 |
43
+ | [`superroute-sapp`](https://pypi.org/project/superroute-sapp/) (PyPI, `import sapp`) | Python 3.9+: servers and scripts | 0.2.1 |
44
+ | [`superroute/sapp`](https://packagist.org/packages/superroute/sapp) (Packagist) | PHP 8.1+: servers | 0.2.1 |
45
+ | [`github.com/Goodneo-Solutions/sapp-releases/go`](https://github.com/Goodneo-Solutions/sapp-releases/tree/main/go) | Go 1.21+: servers | 0.2.1 |
46
+ | [`ca.superroute:sapp-java`](https://github.com/Goodneo-Solutions/sapp-releases) (Maven) | Java 17+: servers, no dependencies | 0.2.1 |
47
+ | [`sapp_ui`](https://pub.dev/packages/sapp_ui) | Flutter UI kit: sign-in, notifications, wallet, orders, pickups | 0.2.1 |
48
+ | [`@superrouteca/sapp-ui`](https://www.npmjs.com/package/@superrouteca/sapp-ui) | Web UI kit: the same components as custom elements | 0.2.1 |
49
+ | `@superrouteca/sapp-react/ui` | React components over the web UI kit | 0.2.1 |
50
+ | [`SAppUI`](https://github.com/Goodneo-Solutions/sapp-releases) (Swift Package Manager) | SwiftUI UI kit: iOS 15+, macOS 12+ | 0.2.1 |
51
+ | [`ca.superroute:sapp-ui-android`](https://github.com/Goodneo-Solutions/sapp-releases) (Maven) | Jetpack Compose UI kit: Android 7+ | 0.2.1 |
52
+ | `@superrouteca/sapp-react-native/ui` | React Native UI kit | 0.2.1 |
53
+ | [`Superroute.SApp.Wpf`](https://www.nuget.org/packages/Superroute.SApp.Wpf) / [`Superroute.SApp.WinForms`](https://www.nuget.org/packages/Superroute.SApp.WinForms) (NuGet) | WPF and WinForms UI kits over `Superroute.SApp.UI` | 0.2.1 |
54
+
55
+ Every package of a release carries the same version. Version numbers never contain the digits 3 or 4.
56
+
57
+ ## Profiles
58
+
59
+ - **Partner** — an app registered on the Open Platform console (`open.superroute.ca/open-api/apps`). It acts for one
60
+ person at one service provider, with that person's sign-in and the capabilities the app was approved for and the
61
+ provider switched on.
62
+ - **Open** — Open Platform capabilities (`open.superroute.ca/v1`). Inside an app, pass `getToken`, answered by your
63
+ own backend with a short-lived token; an Open Platform API key is a server secret and never belongs in an app.
64
+
65
+ What a caller may do is decided by the server, never by the package installed.
66
+
67
+ ## JavaScript
68
+
69
+ ```js
70
+ import { createPartnerClient } from '@superrouteca/sapp';
71
+
72
+ const sr = createPartnerClient({
73
+ appCode: 'p_acme_parcels',
74
+ clientCode: 'k3m9q2', // the service provider's public client code
75
+ appKey: 'sp_appkey_…', // restricted key for calls before sign-in
76
+ tokenStore, // your secure storage: { get, set, clear }
77
+ locale: 'en',
78
+ });
79
+
80
+ // Sign in with Google or Apple in the app…
81
+ await sr.auth.signInWithIdToken(googleIdToken);
82
+ // …or on the Superroute sign-in page:
83
+ const { url, verifier } = await sr.auth.authorizationUrl({ redirectUri: 'com.acme.parcels:/oauth' });
84
+ // open `url` in the system browser, then
85
+ await sr.auth.exchangeCode({ code, redirectUri: 'com.acme.parcels:/oauth', verifier });
86
+
87
+ const notifications = await sr.api.platformAppNotifications();
88
+ const me = await sr.graphql('{ partnerAppMe { app_code user_name } }');
89
+ ```
90
+
91
+ ## React and React Native
92
+
93
+ ```jsx
94
+ import { SuperrouteProvider, useApi, useSession, createSecureTokenStore } from '@superrouteca/sapp-react-native';
95
+
96
+ const sr = createPartnerClient({ appCode: 'p_acme_parcels', clientCode, appKey, tokenStore: createSecureTokenStore(secureStorage) });
97
+
98
+ function Inbox() {
99
+ const { signedIn } = useSession();
100
+ const { data, loading, error, reload } = useApi((api) => api.platformAppNotifications(), [], { enabled: signedIn === true });
101
+ …
102
+ }
103
+
104
+ <SuperrouteProvider client={sr}><Inbox /></SuperrouteProvider>
105
+ ```
106
+
107
+ Sign-in on the Superroute page from a phone: `sr.auth.signInWithBrowser({ redirectUri, open })`, where `open(url,
108
+ redirectUri)` shows the page in an in-app browser session and answers the URL it returned to (null when closed).
109
+ `sr.auth.subscribe(fn)` is told when a session starts and ends.
110
+
111
+ ## Dart and Flutter
112
+
113
+ ```dart
114
+ import 'package:sapp/sapp.dart';
115
+
116
+ final sr = PartnerClient(appCode: 'p_acme_parcels', clientCode: 'k3m9q2', appKey: 'sp_appkey_…', tokenStore: store);
117
+ await sr.signInWithIdToken(googleIdToken);
118
+ final notifications = await sr.api.platformAppNotifications();
119
+ ```
120
+
121
+ ## Swift
122
+
123
+ ```swift
124
+ // Package.swift
125
+ .package(url: "https://github.com/Goodneo-Solutions/sapp-releases", from: "0.2.1")
126
+ // in the target: .product(name: "SApp", package: "sapp-releases"), and .product(name: "SAppUI", package: "sapp-releases") for the UI kit
127
+ ```
128
+
129
+ In Xcode: File > Add Package Dependencies…, `https://github.com/Goodneo-Solutions/sapp-releases`.
130
+
131
+ ```swift
132
+ import SApp
133
+
134
+ let sr = PartnerClient(appCode: "p_acme_parcels", clientCode: "k3m9q2", appKey: "sp_appkey_…", tokenStore: KeychainTokenStore())
135
+ try await sr.signIn(idToken: googleIdToken)
136
+ let notifications = try await sr.api.platformAppNotifications()
137
+
138
+ // The Superroute sign-in page in an ASWebAuthenticationSession:
139
+ let request = try await sr.authorizationRequest(redirectURI: "com.acme.parcels:/oauth")
140
+ // … present request.url, then with the URL the session returned:
141
+ try await sr.completeSignIn(request, returnedURL: callbackURL)
142
+ ```
143
+
144
+ Answers are `JSON` values (`answer["data"]?["items"]?.array`). `onSessionChange` is told when a session starts and ends.
145
+
146
+ ## Kotlin and Android
147
+
148
+ ```kotlin
149
+ // settings.gradle.kts
150
+ dependencyResolutionManagement {
151
+ repositories {
152
+ google()
153
+ mavenCentral()
154
+ maven { url = uri("https://goodneo-solutions.github.io/sapp-releases/maven") }
155
+ }
156
+ }
157
+
158
+ // build.gradle.kts: ca.superroute:sapp on the JVM, sapp-android on Android, sapp-ui-android for the Compose kit
159
+ implementation("ca.superroute:sapp-android:0.2.1")
160
+ implementation("ca.superroute:sapp-ui-android:0.2.1")
161
+ ```
162
+
163
+ ```kotlin
164
+
165
+ val sr = PartnerClient(appCode = "p_acme_parcels", clientCode = "k3m9q2", appKey = "sp_appkey_…", tokenStore = KeystoreTokenStore(context))
166
+ sr.signInWithIdToken(googleIdToken)
167
+ val notifications = sr.api.platformAppNotifications() // a kotlinx.serialization JsonElement
168
+ ```
169
+
170
+ The sign-in page: open `sr.authorizationRequest(redirectUri).url` in a Custom Tab, then `sr.completeSignIn(request,
171
+ returnedUrl)`. `KeystoreTokenStore` encrypts the tokens with a key held in the Android Keystore.
172
+
173
+ ## C# and .NET
174
+
175
+ ```csharp
176
+ using Superroute.SApp;
177
+
178
+ using var sr = new PartnerClient("p_acme_parcels", clientCode: "k3m9q2", appKey: "sp_appkey_…", tokenStore: new ProtectedDataTokenStore());
179
+ await sr.SignInWithIdTokenAsync(googleIdToken);
180
+ var notifications = await sr.Api.PlatformAppNotificationsAsync(); // a System.Text.Json JsonNode
181
+ ```
182
+
183
+ `ProtectedDataTokenStore` keeps the tokens encrypted for the current Windows user. `OnSessionChange` is told when a
184
+ session starts and ends.
185
+
186
+ ## Python
187
+
188
+ ```python
189
+ from sapp import PartnerClient, OpenClient
190
+
191
+ sr = PartnerClient('p_acme_parcels', client_code='k3m9q2', app_key='sp_appkey_…')
192
+ sr.sign_in_with_id_token(google_id_token)
193
+ notifications = sr.api.platform_app_notifications()
194
+
195
+ open = OpenClient(api_key=os.environ['SUPERROUTE_OPEN_KEY']) # on a server
196
+ place = open.api.geo_forward(body={'address': '100 King St W, Toronto'})
197
+ ```
198
+
199
+ Methods and arguments are snake_case (`platform_app_mark_notification_read(notification_id=…)`); answers are the
200
+ parsed JSON. The client is safe to share between threads: concurrent calls share one refresh.
201
+
202
+ ## PHP
203
+
204
+ ```php
205
+ use Superroute\SApp\PartnerClient;
206
+ use Superroute\SApp\OpenClient;
207
+
208
+ $sr = new PartnerClient('p_acme_parcels', clientCode: 'k3m9q2', appKey: 'sp_appkey_…');
209
+ $sr->signInWithIdToken($googleIdToken);
210
+ $notifications = $sr->api->platformAppNotifications();
211
+
212
+ $open = new OpenClient(apiKey: getenv('SUPERROUTE_OPEN_KEY'));
213
+ $place = $open->api->geoForward(['address' => '100 King St W, Toronto']);
214
+ ```
215
+
216
+ `composer require superroute/sapp`. Optional parameters are named arguments; answers are decoded JSON arrays. Requests
217
+ go through curl when it is loaded, else PHP streams; pass your own `Transport` to use another HTTP client.
218
+
219
+ ## Go
220
+
221
+ ```go
222
+ import sapp "github.com/Goodneo-Solutions/sapp-releases/go"
223
+
224
+ sr, err := sapp.NewPartnerClient(sapp.PartnerOptions{AppCode: "p_acme_parcels", ClientCode: "k3m9q2", AppKey: "sp_appkey_…"})
225
+ _, err = sr.SignInWithIDToken(ctx, googleIDToken, nil)
226
+ notifications, err := sr.API.PlatformAppNotifications(ctx, sapp.PlatformAppNotificationsParams{})
227
+
228
+ open, err := sapp.NewOpenClient(sapp.OpenOptions{APIKey: os.Getenv("SUPERROUTE_OPEN_KEY")})
229
+ place, err := open.API.GeoForward(ctx, sapp.GeoForwardParams{Body: map[string]any{"address": "100 King St W, Toronto"}})
230
+ ```
231
+
232
+ Each operation takes its parameters in a struct; optional ones are pointers (`sapp.String("ios")`, `sapp.Bool(true)`).
233
+ Answers decode into `any` (numbers as `float64`). Failures are `*sapp.Error`.
234
+
235
+ ## Java
236
+
237
+ ```java
238
+ // build.gradle.kts: repositories { maven("https://goodneo-solutions.github.io/sapp-releases/maven") }
239
+ implementation("ca.superroute:sapp-java:0.2.1")
240
+
241
+ PartnerClient sr = PartnerClient.builder("p_acme_parcels").clientCode("k3m9q2").appKey("sp_appkey_…").build();
242
+ sr.signInWithIdToken(googleIdToken);
243
+ Object notifications = sr.api.platformAppNotifications();
244
+
245
+ OpenClient open = OpenClient.builder().apiKey(System.getenv("SUPERROUTE_OPEN_KEY")).build();
246
+ Object place = open.api.geoForward(Map.of("address", "100 King St W, Toronto"));
247
+ ```
248
+
249
+ Required parameters are arguments; optional ones, an optional body and files go in `Args`
250
+ (`api.x(Args.of().with("page", 2))`). The artifact has no dependencies: answers are plain `Map`, `List`, `String`,
251
+ `Long`, `Double`, `Boolean` or null (the built-in `Json` reads and writes them), so it never conflicts with the
252
+ Jackson or Gson version a server already carries.
253
+
254
+ ## Webhooks
255
+
256
+ Every language verifies Superroute webhooks: the signature over the raw body, a timestamp inside the tolerance (300
257
+ seconds) so an old request cannot be replayed, and the event parsed from the native body or a CloudEvents envelope.
258
+
259
+ ```js
260
+ import { verifyWebhook } from '@superrouteca/sapp'; // also from sapp-react and sapp-react-native
261
+
262
+ const event = await verifyWebhook(rawBody, req.headers, process.env.SUPERROUTE_WEBHOOK_SECRET);
263
+ if (await alreadyHandled(event.id)) return; // an event can arrive more than once
264
+ ```
265
+
266
+ | Language | Call |
267
+ |---|---|
268
+ | Dart | `verifyWebhook(body, headers, [secret])` |
269
+ | Swift | `try Webhooks.verify(body:headers:secrets:)` |
270
+ | Kotlin | `Webhooks.verify(body, headers, listOf(secret))` |
271
+ | C# | `Webhooks.Verify(body, headers, new[] { secret })` |
272
+ | Python | `verify_webhook(body, headers, secret)` |
273
+ | PHP | `Webhooks::verify($body, $headers, $secret)` |
274
+ | Go | `sapp.VerifyWebhook(body, r.Header, []string{secret}, sapp.WebhookOptions{})` |
275
+ | Java | `Webhooks.verify(body, headers, secret, WebhookOptions.defaults())` |
276
+
277
+ Pass the body exactly as received, never a re-encoded object. The secret is the endpoint's signing secret, as
278
+ `whsec_…` or plain; during a key rotation pass both. The scheme is the way the endpoint signs:
279
+
280
+ | Scheme | Sender | Headers |
281
+ |---|---|---|
282
+ | `standard` (default) | any endpoint with Standard Webhooks switched on | `webhook-id`, `webhook-timestamp`, `webhook-signature` |
283
+ | `tenant` | order, tracking, driver and device-order webhooks | `X-Webhook-Signature-V2`, `X-Webhook-Timestamp` |
284
+ | `partner_locker`, `tpd` | partner locker deliveries, third-party delivery providers | `X-Webhook-Signature: v1=…`, `X-Webhook-Delivery-Id`, `X-Webhook-Timestamp` |
285
+ | `loading_plan` | loading plan callbacks | `X-Webhook-Signature: v1=…`, `X-Webhook-Timestamp` |
286
+ | `open_platform` | Open Platform job callbacks | `X-Open-Signature`, `X-Open-Timestamp` |
287
+ | `dataset` | dataset webhooks | `X-Webhook-Signature` (the timestamp is not signed) |
288
+
289
+ Tenant webhooks keep the time of the first try on their retries, so their tolerance is 12,600 seconds. The event
290
+ carries `id`, `type` (without the CloudEvents `ca.superroute.` prefix), `time`, `subject`, `source`, `business`,
291
+ `attempt`, `format` (`native`, `ce_binary`, `ce_structured`) and `data`. A failure is a
292
+ `WebhookVerificationError` / `WebhookVerificationException` with a `code`: `missing_header`, `invalid_timestamp`,
293
+ `timestamp_out_of_tolerance`, `invalid_signature`, `invalid_secret`, `invalid_payload` or `unknown_scheme`. The test
294
+ vectors are in `testdata/webhooks/vectors.json` (`tools/webhook_vectors.py`).
295
+
296
+ ## Flutter UI kit
297
+
298
+ ```dart
299
+ SuperrouteScope(
300
+ client: sr,
301
+ theme: ThemeTokens.fromBranding(bootstrap['client']['settings']['branding']),
302
+ onSignInRequired: () => router.go('/sign-in'),
303
+ child: SrPickupList(onShowCode: (value) => showQr(value)),
304
+ )
305
+ ```
306
+
307
+ Components, for customer apps: `SrSignIn`, `SrNotificationInbox`, `SrWalletSummary`, `SrOrderList`, `SrPickupList`,
308
+ `SrOrderDetail`, `SrTracking`, `SrNotificationPreferences`, `SrAccountPrivacy`, `SrLockerMap` (with
309
+ `SrSMapAdapter(controller)` for the app's sMap map), `SrReturnList`, `SrReturnForm`, `SrShippingOrderForm`, the store
310
+ and mall (`SrStoreDirectory`, `SrStorePage`, `SrProductDetail`, `SrCart`, `SrCheckout`) and group buy
311
+ (`SrGroupBuyList`, `SrGroupBuy`, `SrGroupBuyOrders`, `SrGroupBuyOrder`, and for group leaders `SrGroupBuyLead`,
312
+ `SrGroupBuyLeading`, `SrGroupBuyLeadingGroup`); for driver apps: `SrDriverRoutes`,
313
+ `SrRouteStops`, `SrStop` (photos, a signature pad, the delivery PIN), `SrDriverDuty`, `SrDriverPool`, `SrRouteBoards`,
314
+ `SrRouteBoard`, `SrBoardClaims`, `SrDriverEarnings`, `SrScanLookup`. Files, the position, the scanner and card payments
315
+ are the app's: a component takes a callback for them (`pickFile`, `pickPhoto`, `getPosition`, `scan`) or hands the
316
+ payment or a link to share to the app (`onPayment`, `onShare`). They raise events and
317
+ never navigate; what each shows, its states and strings are in the [UI kit reference](https://open.superroute.ca/developer/sapp), the same on every
318
+ platform. Strings in 14 languages live in `ui/strings/` (`tools/ui_strings.py`).
319
+
320
+ ## Web and React UI kit
321
+
322
+ ```html
323
+ <script type="module">
324
+ import { configure } from '@superrouteca/sapp-ui';
325
+ configure({ client: sr, theme: themeFromBranding(branding), locale: 'en' });
326
+ </script>
327
+ <sr-pickup-list></sr-pickup-list>
328
+ ```
329
+
330
+ Elements: `sr-sign-in`, `sr-notification-inbox`, `sr-wallet-summary`, `sr-order-list`, `sr-pickup-list`,
331
+ `sr-order-detail`, `sr-tracking`, `sr-notification-preferences`, `sr-account-privacy`, `sr-locker-map` (`map =
332
+ smapAdapter(sMapMap)`), `sr-return-list`, `sr-return-form`, `sr-shipping-order-form`, `sr-store-directory`,
333
+ `sr-store-page`, `sr-product-detail`, `sr-cart`, `sr-checkout`, `sr-group-buy-list`, `sr-group-buy`,
334
+ `sr-group-buy-orders`, `sr-group-buy-order`, for group leaders `sr-group-buy-lead`, `sr-group-buy-leading`,
335
+ `sr-group-buy-leading-group`, and for driver apps `sr-driver-routes`, `sr-route-stops`, `sr-stop`,
336
+ `sr-driver-duty`, `sr-driver-pool`, `sr-route-boards`, `sr-route-board`, `sr-board-claims`, `sr-driver-earnings`,
337
+ `sr-scan-lookup`. Each raises `sr-*` events (`sr-open`, `sr-created`, `sr-payment`, `sr-completed`, …, listed in the
338
+ types) and `sr-sign-in-required`. The kit never draws a map: the locker map uses the app's own sMap map through the adapter. In React:
339
+
340
+ ```jsx
341
+ import { PickupList } from '@superrouteca/sapp-react/ui';
342
+ <SuperrouteProvider client={sr}><PickupList onShowCode={showQr} /></SuperrouteProvider>
343
+ ```
344
+
345
+ ## Native UI kits
346
+
347
+ The same components on SwiftUI, Jetpack Compose, React Native, WPF and WinForms, following the [UI kit reference](https://open.superroute.ca/developer/sapp)
348
+ exactly as the Flutter and web kits do, with the same strings in 14 languages (generated from `ui/strings/` by
349
+ `tools/ui_strings.py`). Each component keeps its state and calls in a testable model (an `ObservableObject` on SwiftUI,
350
+ a state holder on Compose, a view model in `Superroute.SApp.UI` shared by WPF and WinForms) and draws it with the
351
+ platform's own controls. The app supplies what the kit leaves to it: images on Compose, the in-app browser for sign-in,
352
+ files, photos, the position, the scanner, card payments and the sMap map through its adapter.
353
+
354
+ ```swift
355
+ import SAppUI
356
+ SuperrouteScope(client: client, theme: ThemeTokens(branding: branding)) {
357
+ PickupList(onShowCode: { code in showQR(code) })
358
+ }
359
+ ```
360
+
361
+ ```kotlin
362
+ SuperrouteScope(client = client, theme = ThemeTokens(branding)) {
363
+ PickupList(onShowCode = { code -> showQr(code) })
364
+ }
365
+ ```
366
+
367
+ ```jsx
368
+ import { PickupList } from '@superrouteca/sapp-react-native/ui';
369
+ <SuperrouteProvider client={sr}><PickupList onShowCode={(code) => showQr(code)} /></SuperrouteProvider>
370
+ ```
371
+
372
+ Example apps, built against the published packages: [SwiftUI](https://github.com/Goodneo-Solutions/sapp-releases/tree/main/examples/swiftui), [Android](https://github.com/Goodneo-Solutions/sapp-releases/tree/main/examples/android), [React Native](https://github.com/Goodneo-Solutions/sapp-releases/tree/main/examples/react-native), [WPF](https://github.com/Goodneo-Solutions/sapp-releases/tree/main/examples/wpf) and [WinForms](https://github.com/Goodneo-Solutions/sapp-releases/tree/main/examples/winforms).
373
+
374
+ ## Errors
375
+
376
+ Every failed call throws one error type (`SuperrouteError` / `SuperrouteException`) with the HTTP `status`, the
377
+ server's `code` (`invalid_token`, `insufficient_scope`, `access_denied`, `APP_ACCESS_DENIED`, …) and the parsed body.
378
+ `signInRequired` is true when the person must sign in again. Repeatable calls are retried on a lost connection and
379
+ on temporary server errors; writes are never retried.
380
+
381
+ ## How the clients are made
382
+
383
+ The API methods are generated from the specs Superroute publishes (`specs/`): one method per operation. Never edit
384
+ `*/generated/*`; run `tools/sync-specs.sh /path/to/Superroute` to copy newer specs and regenerate. CI fails when the
385
+ generated files do not match the specs.
386
+
387
+ ## Tests
388
+
389
+ ```sh
390
+ npm install && npm test # js, react, react-native
391
+ cd dart && dart pub get && dart test
392
+ swift test
393
+ cd kotlin && ./gradlew :sapp:test
394
+ dotnet test dotnet/tests/Superroute.SApp.Tests/Superroute.SApp.Tests.csproj
395
+ pip install -e python && pytest python/tests
396
+ cd php && composer install && vendor/bin/phpunit
397
+ cd go && go test ./...
398
+ cd java && ./gradlew test
399
+ cd flutter_ui && flutter test
400
+ ```