Skip to content

Latest commit

 

History

66 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

platformkit-mobile

PlatformKit Mobile is an Expo and React Native client for PlatformKit. It signs in to a running server, reads GET /api/v1/admin/resources, and derives list, detail, create and edit screens from the resources returned for that session. The server remains responsible for authorization and validation on every request. This repository builds independently; it has no build-time dependency on the Go repository or on private modules. The source is under the Apache License, Version 2.0 (LICENSE, NOTICE), as PlatformKit is.

Run locally

Install Git, the Node.js version in .nvmrc and npm, then run these commands from this repository. npm ci installs the exact versions in package-lock.json:

npm ci
npm run check
EXPO_PUBLIC_API_URL=https://platformkit.example.com npm start

Replace the example URL with the tenant's server origin, without an /api/v1 suffix. The sign-in screen also lets you enter or change the server URL. Use an existing account on that tenant. To start a local reference server, follow the PlatformKit quick start. The device must be able to reach the server; localhost on a phone refers to the phone itself. EXPO_PUBLIC_API_URL is bundled client configuration and must contain no secret.

npm start starts Expo's development server. npm run android and npm run ios build and run native projects using the corresponding local platform toolchain. npm run web starts a browser preview, but the current session transport is native: it reads Set-Cookie and sends Cookie explicitly. Browser cookie restrictions make that preview unsuitable as proof that sign-in works on a device.

Find the owner of a change

app/_layout.tsx supplies the server URL and renderer pack to Shell, wraps the screens in the theme that follows the device's appearance, and configures the native stack whose headers every screen uses. src/shell.tsx owns session restoration, sign-in, sign-out, catalog loading and a count of what this app wrote to each resource, so a list or a detail knows when what it shows is stale. The resource route files delegate to src/route.tsx, which finds the catalog entry and chooses a custom or generated screen.

src/core/ holds the catalog types and validator, screen derivation, the pure value helpers (an instant, a number, what a form may refuse before the server does) and the lifecycle reducer. These functions run in plain Node without React. src/effects/api.ts owns HTTP requests and API error decoding; src/effects/session.ts orders secure-storage operations and binds the server URL and session cookie in one versioned record. src/effects/native-session.ts supplies Expo SecureStore. Each shell operation has a generation; the core rejects obsolete responses after sign-out, another sign-in or a newer refresh.

src/ui/ is the component library, in one direction of imports that ESLint enforces (eslint.config.mjs): atoms/ are single controls and words, molecules/ are rows and fields, organisms/ are the generated list, detail, form, home and sign-in as pure components of props, and templates/ own the scroll, the large-title inset, the safe area and the keyboard. Nothing in src/ui/ reaches for the shell, a transport or the router; effects arrive as callbacks. src/ui/theme.tsx decides colours and faces from the device's appearance; its palette, src/ui/tokens.ts, is generated by npm run tokens from testdata/design-tokens.json, a checked-in projection of the public repository's design export whose origin testdata/design-tokens.source.json records: the upstream commit, the command that projected it and the fixture's SHA-256. Both files are build inputs the native fingerprint hashes. Refresh them deliberately, as Verify a change describes; the token tests fail when the fixture, its provenance and the generated file disagree. Distances live in src/ui/scale.ts. The gallery (src/ui/gallery.tsx, served by app/gallery.tsx in development or in a build with EXPO_PUBLIC_GALLERY=1) shows every atom and molecule in both modes.

Keep UI imports pointed toward the primitives. Atoms may compose other atoms; molecules add atoms; templates supply layout through children or render callbacks and may compose atoms and molecules. Organisms combine these layers. Shared theme, scale and token helpers import no components, and components never import the gallery. ESLint checks these boundaries along with the UI's effect boundary; tests/layers.test.ts exercises permitted composition and rejected imports.

src/screens/ is where an effect becomes a prop: one hook per generated screen (useResourceList, useResourceDetail, useResourceForm) owns its requests, its generation guard and its phase, and one composition per screen sets the native header and renders the organism. A form is presented as a sheet; Save exists only once the row has loaded; a dirty sheet asks before it is dismissed. A select asks the platform's own chooser, an instant its own date and time controls, and an instant reads in the device's zone, which is the one place this shell deliberately reads differently from the web shell.

Only the current atomic session record is supported. Older installations require a fresh server selection and sign-in. Failed saves prevent sign-in completion. Sign-out immediately clears the active client; if secure storage cannot be cleared, the sign-in screen reports the failure and provides a retry. Until clearing succeeds, reopening the app may restore the previous saved record.

A custom screen belongs in the renderer pack passed to Shell in app/_layout.tsx. The Renderers type maps module/entity to optional list, detail, form and command components. Each receives an entry and, when applicable, an id; command screens also receive the verb. Any omitted component falls back to the generated screen. Custom components belong in src/screens/, where effects become props for the atomic UI layers. Add a custom screen when the workflow needs something the resource schema cannot express.

Custom screens use api.request(path, options) from the current shell API for module-owned JSON routes. path is an encoded path under /api/v1/, with an optional query string; the selected server and session stay with the shell. The method defaults to GET. Writes specify POST, PUT, PATCH or DELETE and may include a JSON body; an omitted body sends no content. Responses are unknown (or undefined for an empty body), so validate domain data in src/core/ before turning it into UI props. The request uses the same cookie, deadline and ApiError decoding as resource operations.

Pass an AbortSignal to cancel a screen's obsolete request; cancellation rejects with AbortError. A timeout remains an ApiError with status 0. Neither the transport nor cancellation proves that a write was rolled back, and the client does not retry requests automatically. Recover uncertain writes through the module's persisted read contract before offering another submission. Screen generation guards still decide whether a response belongs to the current view.

Consume the shared native source

package.json exposes the existing implementation through package subpaths: platformkit-mobile/shell, renderers, route, effects/api, core/catalog, core/derive, screens/<component> and ui/{atoms,molecules,organisms,templates}/<component>. Theme and scale come from platformkit-mobile/ui/theme and platformkit-mobile/ui/scale. These exports resolve directly to the TypeScript source used by this reference application. The consuming Expo application supplies its routes, identity, renderer pack and product screens, and composes one shared Shell and ThemeProvider.

platformkit-mobile/screens/SignIn takes an optional title, the product's name over the form, and draws the shell's phases itself: a spinner while a saved sign-in is restored, and, when a saved sign-in could not be opened, the reason with Retry and a way to another server. A product that composes its own sign-in screen gets the same from SignInForm through title, booting, failed and onRetry; without them it renders the form as before.

Pass palette to ThemeProvider to use the application's complete light and dark palettes. Pass fonts for native display, body and mono faces already available on the device; load bundled fonts before rendering the provider. An undefined face selects the system font. Omitted props use the reference palette and platform faces. Changing either prop updates the shared components; keep their objects stable between changes. The provider's mode prop still overrides the device appearance when the product requires it.

Keep the application's design export in testdata/design-tokens.json. Reuse render from platformkit-mobile/tools/tokens in its scripts/tokens.ts:

import { readFileSync, writeFileSync } from "node:fs";
import { render } from "platformkit-mobile/tools/tokens";

const document: unknown = JSON.parse(readFileSync("testdata/design-tokens.json", "utf8"));
writeFileSync("src/ui/tokens.ts", render(document));

Run that script with the application's locked tsx tool and @types/node development dependency. Include "types": ["node"] in the script's TypeScript configuration. Import palette from the generated src/ui/tokens.ts in the composition root. Type checking against the provider requires every shared color role in both modes. CSS font stacks in the export are data; the application supplies their resolved native faces through fonts. The generator runs during development and verification. Native runtime code consumes its generated output; ESLint rejects imports of the build tool or Node APIs into runtime layers.

The application owns its native dependencies and lockfile. Declare the native packages directly in that application at the versions used here so autolinking finds them and React, the router and their contexts have one installed instance. Pin the shared package to an exact published release or an HTTPS source archive whose URL names a full published commit ID. Commit the resulting application lockfile, including the resolved URL and integrity hash; branch archives and local checkout paths do not identify a fixed dependency.

No npm registry release is published; private: true remains set. For local review, npm pack --ignore-scripts --pack-destination /tmp creates a source dependency archive. Its contents are src/, scripts/tokens.ts, the shared scripts/android/ recipe, LICENSE, NOTICE and npm's package metadata and documentation. It contains the shared implementation, including generated design tokens. The reference application's routes, Docker setup and test tooling belong to the complete application handoff below. npm run check packs the dependency in a removed temporary directory, verifies public imports from the extracted package and checks that its relative source dependencies are present. Consumer type checking, bundling and device journeys remain separate verification steps.

Build a consuming Android application

The exported platformkit-mobile/tools/android entry is the same Bash recipe used by this repository. After installing the consuming app's locked dependencies, run this from that app's root in the Android build environment:

bash "$(node -p "require.resolve('platformkit-mobile/tools/android')")" "$PWD"

The target needs package.json, package-lock.json and an Expo app configuration. The recipe runs that app's installed Expo CLI and uses Gradle settings and signing rules from this versioned dependency. It never copies the reference app's routes or identity. The optional application argument defaults to the reference app when running its own scripts/android/build.sh.

This command regenerates the target's android/ with expo prebuild --clean, then builds the Gradle tasks selected by TARGETS. Keep native configuration in the app config and config plugins. Build a source export outside the development checkout to leave that checkout's generated native files untouched. The SDK, JDK, native npm dependencies and Gradle downloads are still build prerequisites; the pinned image under Build a binary supplies the toolchain. ABIS, PK_PROFILE, PK_VERSION and signing variables retain the meanings documented below, with app identity resolved by the target's own configuration. Outputs stay under the target's android/app/build/outputs/. A successful build still needs device verification.

Hand off source

The application owner commits its identity in app.config.ts, its screens through the renderer pack in app/_layout.tsx, and its design export in testdata/design-tokens.json. Keep native dependencies in this application's package.json and lockfile. Custom screens use src/screens/, domain decisions use src/core/, HTTP uses src/effects/, and presentation composes the existing atomic layers in src/ui/. A branded shell still needs its product workflows and device verification before it is a complete application.

After npm run check passes, commit the application and export its full commit ID. Run this from the repository root; the destination's parent must exist and the destination must be new and outside the workspace:

git rev-parse HEAD
npm run source -- --revision <full-commit-id> --output /tmp/mobile-source

scripts/source.ts reads the committed files, preserving their executable modes. It includes routes, source, assets, tests, the lockfile, native fingerprint and build recipe. Hosting workflows are excluded. Local dependencies, workspace links, symlinks and tracked local configuration cause an error; working-tree edits, ignored files and installed dependencies are never copied. npm run check:source, part of npm run check, applies the same manifest and lockfile rules to the working tree without exporting, so a local dependency or an unpinned package is refused on every check rather than at the handoff. SOURCE.json records the source revision and each delivered file's SHA-256, byte count and mode. It proves provenance, not publication or runtime behavior. Verify that revision is published before distributing a release, and retain the receipt when you archive the exported directory.

From the exported directory, install and verify without the original checkout:

npm ci
npm run check
npm run export:check

Installation downloads the locked npm packages; native builds also need the platform toolchain and its dependencies. The export is source, not an offline dependency cache or a backend server. Both bundle exports must compile; use the binary recipe below and the device flows to verify the application's actual journeys. A recipient can edit the exported files directly. When importing into its own repository, retain the received receipt as UPSTREAM.json before committing: SOURCE.json is reserved for the next export.

Build a binary

The Android build is one recipe, scripts/android/build.sh, run inside the pinned build image by Dockerfile.android, so neither a runner nor a developer machine needs the SDK:

make apk                          # unsigned arm64 release APK in out/
make apk ABIS=x86_64 PROFILE=ci   # the verification build an emulator runs
make aab                          # unsigned AAB and universal APK, all ABIs
make apk-mirror                   # the same recipe under the CI runner's 2 CPU / 4 GiB

app.config.ts decides the identity: PK_VERSION=v0.2.0 gives version 0.2.0 and a version code that only grows; PK_PROFILE=ci builds PlatformKit CI under its own package, with the gallery route, so it can sit beside a real install. Release builds are unsigned by the recipe (scripts/android/signing.gradle); make apk-debug-sign signs one with the SDK's debug key for an emulator, and make sign signs the release outputs with a keystore named by PK_KEYSTORE and its three companions, verifies the certificates and writes SHA256SUMS. The key never leaves the machine that holds it. iOS is built on a Mac with npx expo prebuild --platform ios and npx expo run:ios --configuration Release; there is no iOS build in CI.

The android workflow builds the verification APK whenever a native input changed (the fingerprint, the configuration, the lockfile, the recipe) and keeps it as a run artifact for two weeks. It needs a verification environment with about 6 GiB of memory: one architecture, built serially with the settings in scripts/android/, peaks at 4.1 GiB by itself, and a job shares its allowance with the builder, which is where 4 GiB failed. At 6 GiB a cold job builds in about seven and a half minutes. The job runs only when the repository variable ANDROID_BUILD is on, so an environment without the memory reports nothing rather than failing; make apk builds the same thing locally either way.

Verify a change

The repository CI runs npm run check on pull requests and main pushes. npm run check runs eight gates in order, and stops at the first that fails: check:sdk (EXPO_OFFLINE=1 expo install --check: every installed native package against the ranges the pinned expo bundles, read from node_modules/expo/bundledNativeModules.json), typecheck (TypeScript), lint (ESLint, including the layer rules), format:check (Prettier), test (the Node suite, tsx --test tests/*.test.ts, then the Jest suite, every tests/**/*.test.tsx rendering components, the shell, the route dispatcher and the screen hooks), check:fingerprint, check:flows (every id a device flow names is a testID a component sets) and check:source (the manifest and lockfile install from the registry alone). fingerprint.json is the hash of everything a binary is built from: the app configuration, the native modules in the lockfile and their config plugins, the Android recipe, the bundler configuration and the design export. When a change moves it, run npm run fingerprint and say why in the commit, because that change needs a new binary. CI also exports both bundles, fails on a high or critical dependency advisory, and scans the history for secrets; a weekly workflow reports what drifted without blocking anything. Node is pinned once, in .nvmrc. Use expo install for native dependencies so they match that SDK. The app owns the native font, module core, Reanimated and Worklets dependencies used by its router and tests; their SDK-compatible versions must resolve once at the app root. Commit the lockfile with every dependency change.

The SDK gate is deliberately offline. expo install --check otherwise asks Expo's live version service, so a patch published upstream turns an unchanged commit red: that happened twice in one week and is somebody else's release, not a defect here. With EXPO_OFFLINE=1 the same command compares the installed packages against bundledNativeModules.json inside the pinned expo, a file npm ci reproduces byte for byte, so the gate answers the same for a given tree on any machine and any day. It still refuses a package that does not match the pinned SDK, which is what it is for. Only expo itself is outside that manifest, because it is the pin everything else is compared against.

Asking what Expo now recommends is an owner's action, not a gate:

npm run sdk:latest          # the online check: what upstream would change
npx expo install --fix      # take it, then commit package.json and the lockfile
npm run fingerprint         # a native pin moved, so the binary identity moves

Do that deliberately, in its own commit, and say in the commit why the native project changed. The weekly drift workflow runs the online check too and reports what it finds without blocking anything.

Starting Expo generates route types under the ignored .expo/ directory, which TypeScript also checks. The individual commands are in package.json. npm run format formats TypeScript in app/, src/ and tests/.

The Node tests cover catalog parsing, screen derivation, the token generator and its provenance, the distance guard, out-of-order lifecycle events, interrupted storage writes, HTTP behavior with supplied effects and the source gates. The Jest tests (npm run test:ui, the Expo preset and React Native Testing Library) render the atoms, molecules and organisms with sample props and assert what a screen reader would find (tests/ui/), drive the shell over an in-memory secure store and a fake fetch (tests/shell.test.tsx), the route dispatcher over a mocked router (tests/route.test.tsx) and the screen hooks over a fake Api (tests/screens/). Neither suite launches Expo, exercises a native device or connects to a live server. For a screen or session change, also exercise the affected journey on the target platform and report what you ran.

testdata/catalog.json is a checked-in copy of PlatformKit's ui/screens/testdata/catalog.json, and testdata/catalog.source.json records the commit it was copied from, in the same shape the design-token provenance uses. The suite checks the bytes against that record offline, so editing the copy to make a test pass fails instead of fixing anything. npx tsx scripts/catalog.ts refresh rewrites both files; the pin itself moves only by editing the recorded commit, which is a diff somebody reviews. What no local check can see is whether the server has moved, so the contract-drift schedule asks the public repository and the module proxy and reports without blocking, for the reason at the head of .gitea/workflows/drift.yml. A proxy it cannot reach is reported as unknown and never as agreement: the first version of scripts/catalog.ts asked the proxy for septagon-oss/platformkit instead of github.com/septagon-oss/platformkit, took a 404, and printed "ok, this is the latest version".

Unlike the design tokens, this fixture is a test input and not a build input — no app code reads it, and scripts/fingerprint.ts names its sources rather than sweeping testdata/, so refreshing the copy is not a binary change and the android workflow has nothing to rebuild. A stale copy blinds the suite and the review; it cannot reach the person. What can reach them is a server answering with a shape this build has never seen, so parseCatalog refuses a catalogVersion newer than SUPPORTED_CATALOG_VERSION in src/core/catalog.ts rather than drawing a screen from the fields it happens to recognise; a server old enough not to stamp is the server this shell was built against, and is accepted.

testdata/design-tokens.json is the palette's source and a build input: the native fingerprint hashes it, so a token change is a binary change the android workflow rebuilds for. To refresh it, project the export at a published PlatformKit commit, record that commit, then regenerate the palette and the fingerprint. From that repository's root and this one:

go run ./tools/designexport | jq '{schema, themes}' > "$MOBILE/testdata/design-tokens.json"
git rev-parse HEAD                                    # the commit to record
npm run tokens -- --upstream <full-commit-id>         # writes design-tokens.source.json and src/ui/tokens.ts
npm run fingerprint

Without --upstream, npm run tokens regenerates the palette only, and refuses when the fixture is no longer the export its provenance describes. Commit the fixture, its provenance, the generated palette and the fingerprint together, and say in the commit which commit the export came from and what changed in the palette. Read AGENTS.md before contributing.

About

PlatformKit's schema-driven native shell: an Expo app that generates its screens from GET /api/v1/admin/resources

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages