sapp 0.1.0
sapp: ^0.1.0 copied to clipboard
sApp for Dart and Flutter: sign-in, the platform app API and Open Platform capabilities.
sApp #
Build apps on Superroute: customer and driver apps that run for one or more service providers, and apps that use Open Platform capabilities such as routing, geocoding and ETA. MIT licence.
| Package | Platform | Status |
|---|---|---|
@superrouteca/sapp |
JavaScript and TypeScript: web, Node 20+, React Native | 0.1.0 |
@superrouteca/sapp-react |
React: provider and hooks | 0.1.0 |
@superrouteca/sapp-react-native |
React Native: hooks, secure token store | 0.1.0 |
sapp |
Dart and Flutter | 0.1.0 |
SApp (Swift package at this repository's root) |
iOS 15+, macOS 12+ | 0.1.0 |
sapp / sapp-android (JitPack, kotlin/) |
Kotlin on the JVM and Android 7+ | 0.1.0 |
Superroute.SApp (NuGet) |
.NET 8: Windows apps and servers | 0.1.0 |
sapp_ui |
Flutter UI kit: sign-in, notifications, wallet, orders, pickups | 0.1.0 |
@superrouteca/sapp-ui |
Web UI kit: the same components as custom elements | 0.1.0 |
@superrouteca/sapp-react/ui |
React components over the web UI kit | 0.1.0 |
| UI kit on the other platforms | SwiftUI, Compose, React Native, Windows | planned |
Every package of a release carries the same version. Version numbers never contain the digits 3 or 4.
Profiles #
- Partner — an app registered on the Open Platform console (
open.superroute.ca/open-api/apps). It acts for one person at one service provider, with that person's sign-in and the capabilities the app was approved for and the provider switched on. - Open — Open Platform capabilities (
open.superroute.ca/v1). Inside an app, passgetToken, answered by your own backend with a short-lived token; an Open Platform API key is a server secret and never belongs in an app.
What a caller may do is decided by the server, never by the package installed.
JavaScript #
import { createPartnerClient } from '@superrouteca/sapp';
const sr = createPartnerClient({
appCode: 'p_acme_parcels',
clientCode: 'k3m9q2', // the service provider's public client code
appKey: 'sp_appkey_…', // restricted key for calls before sign-in
tokenStore, // your secure storage: { get, set, clear }
locale: 'en',
});
// Sign in with Google or Apple in the app…
await sr.auth.signInWithIdToken(googleIdToken);
// …or on the Superroute sign-in page:
const { url, verifier } = await sr.auth.authorizationUrl({ redirectUri: 'com.acme.parcels:/oauth' });
// open `url` in the system browser, then
await sr.auth.exchangeCode({ code, redirectUri: 'com.acme.parcels:/oauth', verifier });
const notifications = await sr.api.platformAppNotifications();
const me = await sr.graphql('{ partnerAppMe { app_code user_name } }');
React and React Native #
import { SuperrouteProvider, useApi, useSession, createSecureTokenStore } from '@superrouteca/sapp-react-native';
const sr = createPartnerClient({ appCode: 'p_acme_parcels', clientCode, appKey, tokenStore: createSecureTokenStore(secureStorage) });
function Inbox() {
const { signedIn } = useSession();
const { data, loading, error, reload } = useApi((api) => api.platformAppNotifications(), [], { enabled: signedIn === true });
…
}
<SuperrouteProvider client={sr}><Inbox /></SuperrouteProvider>
Sign-in on the Superroute page from a phone: sr.auth.signInWithBrowser({ redirectUri, open }), where open(url, redirectUri) shows the page in an in-app browser session and answers the URL it returned to (null when closed).
sr.auth.subscribe(fn) is told when a session starts and ends.
Dart and Flutter #
import 'package:sapp/sapp.dart';
final sr = PartnerClient(appCode: 'p_acme_parcels', clientCode: 'k3m9q2', appKey: 'sp_appkey_…', tokenStore: store);
await sr.signInWithIdToken(googleIdToken);
final notifications = await sr.api.platformAppNotifications();
Swift #
import SApp
let sr = PartnerClient(appCode: "p_acme_parcels", clientCode: "k3m9q2", appKey: "sp_appkey_…", tokenStore: KeychainTokenStore())
try await sr.signIn(idToken: googleIdToken)
let notifications = try await sr.api.platformAppNotifications()
// The Superroute sign-in page in an ASWebAuthenticationSession:
let request = try await sr.authorizationRequest(redirectURI: "com.acme.parcels:/oauth")
// … present request.url, then with the URL the session returned:
try await sr.completeSignIn(request, returnedURL: callbackURL)
Answers are JSON values (answer["data"]?["items"]?.array). onSessionChange is told when a session starts and ends.
Kotlin and Android #
// build.gradle.kts, with maven("https://jitpack.io") in the repositories
implementation("com.github.Goodneo-Solutions.sApp:sapp-android:0.1.0")
val sr = PartnerClient(appCode = "p_acme_parcels", clientCode = "k3m9q2", appKey = "sp_appkey_…", tokenStore = KeystoreTokenStore(context))
sr.signInWithIdToken(googleIdToken)
val notifications = sr.api.platformAppNotifications() // a kotlinx.serialization JsonElement
The sign-in page: open sr.authorizationRequest(redirectUri).url in a Custom Tab, then sr.completeSignIn(request, returnedUrl). KeystoreTokenStore encrypts the tokens with a key held in the Android Keystore.
C# and .NET #
using Superroute.SApp;
using var sr = new PartnerClient("p_acme_parcels", clientCode: "k3m9q2", appKey: "sp_appkey_…", tokenStore: new ProtectedDataTokenStore());
await sr.SignInWithIdTokenAsync(googleIdToken);
var notifications = await sr.Api.PlatformAppNotificationsAsync(); // a System.Text.Json JsonNode
ProtectedDataTokenStore keeps the tokens encrypted for the current Windows user. OnSessionChange is told when a
session starts and ends.
Flutter UI kit #
SuperrouteScope(
client: sr,
theme: ThemeTokens.fromBranding(bootstrap['client']['settings']['branding']),
onSignInRequired: () => router.go('/sign-in'),
child: SrPickupList(onShowCode: (value) => showQr(value)),
)
Components, for customer apps: SrSignIn, SrNotificationInbox, SrWalletSummary, SrOrderList, SrPickupList,
SrOrderDetail, SrTracking, SrNotificationPreferences, SrAccountPrivacy, SrLockerMap (with
SrSMapAdapter(controller) for the app's sMap map), SrReturnList, SrReturnForm, SrShippingOrderForm, the store
and mall (SrStoreDirectory, SrStorePage, SrProductDetail, SrCart, SrCheckout) and group buy
(SrGroupBuyList, SrGroupBuy, SrGroupBuyOrders, SrGroupBuyOrder, and for group leaders SrGroupBuyLead,
SrGroupBuyLeading, SrGroupBuyLeadingGroup); for driver apps: SrDriverRoutes,
SrRouteStops, SrStop (photos, a signature pad, the delivery PIN), SrDriverDuty, SrDriverPool, SrRouteBoards,
SrRouteBoard, SrBoardClaims, SrDriverEarnings, SrScanLookup. Files, the position, the scanner and card payments
are the app's: a component takes a callback for them (pickFile, pickPhoto, getPosition, scan) or hands the
payment or a link to share to the app (onPayment, onShare). They raise events and
never navigate; what each shows, its states and strings are in UI-KIT-SPEC.md, the same on every
platform. Strings in 14 languages live in ui/strings/ (tools/ui_strings.py).
Web and React UI kit #
<script type="module">
import { configure } from '@superrouteca/sapp-ui';
configure({ client: sr, theme: themeFromBranding(branding), locale: 'en' });
</script>
<sr-pickup-list></sr-pickup-list>
Elements: sr-sign-in, sr-notification-inbox, sr-wallet-summary, sr-order-list, sr-pickup-list,
sr-order-detail, sr-tracking, sr-notification-preferences, sr-account-privacy, sr-locker-map (map = smapAdapter(sMapMap)), sr-return-list, sr-return-form, sr-shipping-order-form, sr-store-directory,
sr-store-page, sr-product-detail, sr-cart, sr-checkout, sr-group-buy-list, sr-group-buy,
sr-group-buy-orders, sr-group-buy-order, for group leaders sr-group-buy-lead, sr-group-buy-leading,
sr-group-buy-leading-group, and for driver apps sr-driver-routes, sr-route-stops, sr-stop,
sr-driver-duty, sr-driver-pool, sr-route-boards, sr-route-board, sr-board-claims, sr-driver-earnings,
sr-scan-lookup. Each raises sr-* events (sr-open, sr-created, sr-payment, sr-completed, …, listed in the
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:
import { PickupList } from '@superrouteca/sapp-react/ui';
<SuperrouteProvider client={sr}><PickupList onShowCode={showQr} /></SuperrouteProvider>
Errors #
Every failed call throws one error type (SuperrouteError / SuperrouteException) with the HTTP status, the
server's code (invalid_token, insufficient_scope, access_denied, APP_ACCESS_DENIED, …) and the parsed body.
signInRequired is true when the person must sign in again. Repeatable calls are retried on a lost connection and
on temporary server errors; writes are never retried.
How the clients are made #
The API methods are generated from the specs Superroute publishes (specs/): one method per operation. Never edit
*/generated/*; run tools/sync-specs.sh /path/to/Superroute to copy newer specs and regenerate. CI fails when the
generated files do not match the specs.
Tests #
npm install && npm test # js, react, react-native
cd dart && dart pub get && dart test
swift test
cd kotlin && ./gradlew :sapp:test
dotnet test dotnet/tests/Superroute.SApp.Tests/Superroute.SApp.Tests.csproj
cd flutter_ui && flutter test