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.
- superroute_sapp-0.2.1/LICENSE +21 -0
- superroute_sapp-0.2.1/PKG-INFO +400 -0
- superroute_sapp-0.2.1/README.md +381 -0
- superroute_sapp-0.2.1/pyproject.toml +36 -0
- superroute_sapp-0.2.1/setup.cfg +4 -0
- superroute_sapp-0.2.1/src/sapp/__init__.py +25 -0
- superroute_sapp-0.2.1/src/sapp/_operation.py +28 -0
- superroute_sapp-0.2.1/src/sapp/core.py +613 -0
- superroute_sapp-0.2.1/src/sapp/generated/__init__.py +1 -0
- superroute_sapp-0.2.1/src/sapp/generated/open_api.py +272 -0
- superroute_sapp-0.2.1/src/sapp/generated/partner_api.py +1452 -0
- superroute_sapp-0.2.1/src/sapp/py.typed +0 -0
- superroute_sapp-0.2.1/src/sapp/webhooks.py +237 -0
- superroute_sapp-0.2.1/src/superroute_sapp.egg-info/PKG-INFO +400 -0
- superroute_sapp-0.2.1/src/superroute_sapp.egg-info/SOURCES.txt +18 -0
- superroute_sapp-0.2.1/src/superroute_sapp.egg-info/dependency_links.txt +1 -0
- superroute_sapp-0.2.1/src/superroute_sapp.egg-info/requires.txt +3 -0
- superroute_sapp-0.2.1/src/superroute_sapp.egg-info/top_level.txt +1 -0
- superroute_sapp-0.2.1/tests/test_sdk.py +341 -0
- superroute_sapp-0.2.1/tests/test_webhooks.py +33 -0
|
@@ -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
|
+
```
|