Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,13 @@ jobs:
run: |
package_version="$(node -p "require('./package.json').version")"
test "${GITHUB_REF_NAME#v}" = "$package_version"
- name: Verify deployed hosted runtime
run: |
runtime="$(sed -n "s/.*seatLayerHostedWebVersion = '\(.*\)';/\1/p" src/types.ts)"
metadata="$(curl --fail --silent --show-error "https://cdn.seatlayer.io/seatlayer-js@$runtime/release.json")"
METADATA="$metadata" RUNTIME="$runtime" node -e \
'const m=JSON.parse(process.env.METADATA); if(m.version!==process.env.RUNTIME || m.promotable!==true) process.exit(1)'
curl --fail --silent --show-error --output /dev/null \
"https://cdn.seatlayer.io/seatlayer-js@$runtime/mobile.html"
- run: pnpm validate
- run: npm publish --access public
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## 0.2.0

- Uses the pinned hosted `seatlayer-js@0.66.0/mobile.html` document at
`https://cdn.seatlayer.io`; buyer access tokens must be minted for that exact
allowed origin. Private configuration fails closed unless the bridge advertises
`native-access-provider`.
- Adds programmatic selection/category controls, exact-count validators, typed
validity/access events, selected-object unavailability, and view-mode parity.
- Reloads the WebView when configuration identity changes without serializing
credentials into React keys; callback-only rerenders no longer restart the
handshake.
- Removes the unused legacy inline-document generation pipeline and reports the
production dependency as `seatLayerHostedWebVersion`.

## 0.1.3

- Updated the vendored buyer runtime to `seatlayer-js@0.59.0` (sha256
Expand Down
33 changes: 22 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ events over a versioned WebView bridge.
[Native Android SDK](https://github.com/seatlayer/seatlayer-android) ·
[AI Toolkit](https://github.com/seatlayer/seatlayer-ai-toolkit)

> **Public preview:** Validate `0.1.x` using a SeatLayer test event and physical
> **Public preview:** Validate `0.2.x` using a SeatLayer test event and physical
> iOS and Android devices before production rollout.

## Install
Expand Down Expand Up @@ -117,8 +117,11 @@ try {

`hold` · `resumeHold` · `extendHold` · `release` · `releaseLabels` ·
`bestAvailable` · `holdGA` · `setSeatTier` · `getSelection` ·
`getCurrentHold` · `getGAAreas` · `getFloors` · `setFloor` ·
`setColorblindSafe` · `zoomIn` · `zoomOut` · `zoomToFit` · `destroy`
`selectObjects` · `deselectObjects` · `clearSelection` · `selectCategories` ·
`deselectCategories` · `setSelectableObjects` · `setMaxSelection` ·
`getSelectionValidity` · `refreshAccess` · `getCurrentHold` · `getGAAreas` ·
`getFloors` · `setFloor` · `setColorblindSafe` · `setViewMode` ·
`getViewMode` · `zoomIn` · `zoomOut` · `zoomToFit` · `destroy`

All asynchronous command failures reject with `SeatLayerError`. Inventory
outcomes such as `sold_out`, `not_enough_together`, expired holds and conflicts
Expand All @@ -143,8 +146,10 @@ useEffect(() => {
```

Events: `ready` · `selectionChanged` · `holdChanged` · `holdRestored` ·
`holdExpired` · `error` · `hint` · `gaClick` · `seatHover` · `deckTap` ·
`checkout` · `unknownEvent`
`holdExpired` · `selectionValidityChanged` · `selectionValid` ·
`selectionInvalid` · `selectionLimit` · `accessExpired` · `accessUnavailable` ·
`selectedObjectsUnavailable` · `error` · `hint` · `gaClick` · `seatHover` ·
`deckTap` · `checkout` · `unknownEvent`

Unknown future events remain observable through `unknownEvent`; adding a bundle
event does not crash an older app.
Expand All @@ -165,10 +170,15 @@ before connecting a payment flow.

## How the bridge works

The npm package embeds the verified `seatlayer-js@0.59.0` bundle (sha256 `89bc29fb…`) in generated
inline HTML. This avoids the inconsistent local-file behavior of iOS and Android
WebViews while keeping the SDK JavaScript independent of a runtime CDN download.
Chart data and live inventory still come from the configured SeatLayer API.
The SDK loads the immutable hosted `seatlayer-js@0.66.0/mobile.html` document
from `https://cdn.seatlayer.io`. This gives every platform the mintable HTTPS
origin `https://cdn.seatlayer.io` and lets the browser runtime load its pinned
lazy assets. Buyer-access sessions must be minted for that exact allowed origin;
tokens never belong in a URL, JavaScript source, or app logs.

Configure `buyerAccessTokenProvider` with a function that calls your own backend
mint endpoint. Private configuration and selection policies fail closed if the
loaded runtime does not advertise the required bridge capabilities.

The protocol guarantees:

Expand Down Expand Up @@ -197,8 +207,9 @@ pnpm validate
cd example && pnpm install && pnpm start
```

`pnpm validate` regenerates the embedded document, type-checks, runs protocol
tests, builds ESM/CommonJS/types, and validates the npm tarball.
`pnpm validate` type-checks, runs protocol tests, builds ESM/CommonJS/types,
and validates the npm tarball. Production loads the exact hosted runtime; the
package no longer generates or ships an unused inline Web document.

## Related resources

Expand Down
45 changes: 0 additions & 45 deletions assets/index.html

This file was deleted.

807 changes: 0 additions & 807 deletions assets/seatlayer.js

This file was deleted.

6 changes: 2 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@seatlayer/react-native",
"version": "0.1.3",
"version": "0.2.0",
"description": "Official React Native SDK for interactive SeatLayer reserved-seating maps on iOS and Android.",
"license": "MIT",
"homepage": "https://docs.seatlayer.io/buyer-sdk/mobile/",
Expand Down Expand Up @@ -35,14 +35,12 @@
],
"sideEffects": false,
"scripts": {
"generate:web": "node scripts/generate-web-document.mjs",
"prebuild": "pnpm generate:web",
"build": "tsup",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest",
"lint:package": "publint && attw --pack .",
"validate": "pnpm generate:web && pnpm typecheck && pnpm test && pnpm build && pnpm lint:package",
"validate": "pnpm typecheck && pnpm test && pnpm build && pnpm lint:package",
"prepublishOnly": "pnpm validate"
},
"peerDependencies": {
Expand Down
29 changes: 0 additions & 29 deletions scripts/generate-web-document.mjs

This file was deleted.

51 changes: 32 additions & 19 deletions src/SeatLayerView.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useLayoutEffect, useMemo, useRef } from 'react';
import React, { useLayoutEffect, useRef } from 'react';
import { StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
import {
WebView,
Expand All @@ -9,12 +9,9 @@ import {
import type { BridgeTransport } from './bridge/client';
import { encodeEnvelope, type Envelope } from './bridge/envelope';
import { SeatLayerError } from './errors';
import { seatLayerWebDocument } from './generated/webDocument';
import type { ReadyInfo, SeatLayerConfiguration } from './types';
import { seatLayerMobileOrigin, seatLayerMobilePageUrl, type ReadyInfo, type SeatLayerConfiguration } from './types';
import type { SeatLayerController } from './controller';

const documentBaseUrl = 'https://seatlayer.local/';

export interface SeatLayerViewProps {
controller: SeatLayerController;
configuration: SeatLayerConfiguration;
Expand All @@ -38,6 +35,17 @@ const NativeWebView = WebView as unknown as React.ForwardRefExoticComponent<
WebViewProps & React.RefAttributes<WebViewHandle>
>;

const configurationIds = new WeakMap<object, number>();
let nextConfigurationId = 0;

function configurationIdentity(configuration: object): number {
const existing = configurationIds.get(configuration);
if (existing !== undefined) return existing;
nextConfigurationId += 1;
configurationIds.set(configuration, nextConfigurationId);
return nextConfigurationId;
}

export function SeatLayerView({
controller,
configuration,
Expand All @@ -49,22 +57,27 @@ export function SeatLayerView({
onLoadError,
}: SeatLayerViewProps): React.ReactElement {
const webView = useRef<WebViewHandle | null>(null);
const configurationKey = useMemo(
() => JSON.stringify(configuration),
[configuration],
);
const viewKey = `${configurationKey}:${String(reloadKey ?? '')}`;
const onReadyRef = useRef(onReady);
const onLoadErrorRef = useRef(onLoadError);
onReadyRef.current = onReady;
onLoadErrorRef.current = onLoadError;
// Credentials and provider functions must never be serialized into a React
// key. Depending on the configuration object reloads safely when either is
// replaced, without putting the token in a string, log, or native view key.
const viewKey = `${configurationIdentity(configuration)}:${String(
reloadKey ?? 'seatlayer',
)}`;

useLayoutEffect(() => {
const transport = new ReactNativeWebViewTransport(() => webView.current);
let active = true;
controller.beginHandshake(transport, configuration).then(
(info) => {
if (active) onReady?.(info);
if (active) onReadyRef.current?.(info);
},
(error: unknown) => {
if (!active) return;
onLoadError?.(
onLoadErrorRef.current?.(
error instanceof SeatLayerError
? error
: SeatLayerError.transport('SeatLayer handshake failed.', error),
Expand All @@ -78,10 +91,12 @@ export function SeatLayerView({
false,
);
};
}, [configurationKey, controller, onLoadError, onReady, reloadKey]);
}, [configuration, controller, reloadKey]);

const onMessage = (event: WebViewMessageEvent): void => {
controller.ingestRaw(event.nativeEvent.data);
if (event.nativeEvent.url === seatLayerMobilePageUrl) {
controller.ingestRaw(event.nativeEvent.data);
}
};

const reportLoadFailure = (message: string): void => {
Expand All @@ -95,8 +110,8 @@ export function SeatLayerView({
testID={testID}
accessibilityLabel={accessibilityLabel}
style={[styles.webView, style]}
source={{ html: seatLayerWebDocument, baseUrl: documentBaseUrl }}
originWhitelist={['*']}
source={{ uri: seatLayerMobilePageUrl }}
originWhitelist={[seatLayerMobileOrigin]}
javaScriptEnabled
domStorageEnabled
mixedContentMode="never"
Expand All @@ -118,9 +133,7 @@ export function SeatLayerView({
)
}
onShouldStartLoadWithRequest={(request) =>
request.url === 'about:blank' ||
request.url.startsWith(documentBaseUrl) ||
request.url.startsWith('data:text/html')
request.url === seatLayerMobilePageUrl
}
/>
);
Expand Down
Loading
Loading