@pithy-sh/ui-react-native 0.0.0-stage → 0.2.0

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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/dist/generatedConfig.d.ts +195 -0
  3. package/dist/generatedConfig.d.ts.map +1 -0
  4. package/dist/generatedConfig.js +323 -0
  5. package/dist/templates.d.ts +48 -0
  6. package/dist/templates.d.ts.map +1 -0
  7. package/dist/templates.js +85 -0
  8. package/dist/version.d.ts +5 -0
  9. package/dist/version.d.ts.map +1 -0
  10. package/dist/version.js +10 -0
  11. package/package.json +83 -4
  12. package/src/generatedConfig.ts +419 -0
  13. package/src/templates.ts +92 -0
  14. package/src/version.ts +13 -0
  15. package/templates/app/[...pithy].tsx +7 -0
  16. package/templates/app/_layout.tsx +31 -0
  17. package/templates/app/index.tsx +4 -0
  18. package/templates/app.config.ts +56 -0
  19. package/templates/metro-proxy.cjs +289 -0
  20. package/templates/metro.config.js +6 -0
  21. package/templates/src/payments-store.native.ts +20 -0
  22. package/templates/src/payments-store.ts +16 -0
  23. package/templates/src/payments.tsx +769 -0
  24. package/templates/src/pithy-config.generated.ts +99 -0
  25. package/templates/src/pithy-config.ts +169 -0
  26. package/templates/src/pithy-locale.tsx +90 -0
  27. package/templates/src/platform.ts +111 -0
  28. package/templates/src/route-modules.ts +76 -0
  29. package/templates/src/router.tsx +387 -0
  30. package/templates/src/routes/app/home.bare.tsx +75 -0
  31. package/templates/src/routes/app/home.tsx +55 -0
  32. package/templates/src/routes/pithy/account-deleted.tsx +69 -0
  33. package/templates/src/routes/pithy/account-suspended.tsx +70 -0
  34. package/templates/src/routes/pithy/agent-consent.tsx +260 -0
  35. package/templates/src/routes/pithy/callback.tsx +74 -0
  36. package/templates/src/routes/pithy/choose-organization.tsx +230 -0
  37. package/templates/src/routes/pithy/connected-applications.tsx +153 -0
  38. package/templates/src/routes/pithy/consent.tsx +453 -0
  39. package/templates/src/routes/pithy/invitation.tsx +239 -0
  40. package/templates/src/routes/pithy/organization-suspended.tsx +51 -0
  41. package/templates/src/routes/pithy/otp.tsx +223 -0
  42. package/templates/src/routes/pithy/paywall.tsx +253 -0
  43. package/templates/src/routes/pithy/pricing.tsx +317 -0
  44. package/templates/src/routes/pithy/sign-in.tsx +674 -0
  45. package/templates/src/routes/pithy/subscription.tsx +240 -0
  46. package/templates/src/session.tsx +349 -0
  47. package/templates/src/theme.ts +100 -0
  48. package/templates/src/transport.ts +290 -0
  49. package/templates/src/turnstile.tsx +117 -0
  50. package/templates/src/useAppFocus.ts +20 -0
  51. package/templates/src/useStoreTransactions.ts +16 -0
  52. package/templates/src/useTurnstileWeb.ts +104 -0
  53. package/templates/tsconfig.json +10 -0
  54. package/README.md +0 -3
@@ -0,0 +1,92 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ /**
8
+ * The React Native templates `pithy ui add react-native` copies into an Expo app.
9
+ *
10
+ * A template library, not a runtime library: it ships text, and nothing imports it at runtime except the CLI's
11
+ * scaffolder. The screens need React Native's toolchain to be checked — its types, a program with no DOM lib, and
12
+ * `react-native-web` to render them under test — so they live here with it rather than in the CLI.
13
+ *
14
+ * The tree mirrors the scaffolded layout exactly, so a template's relative imports resolve here as they will in
15
+ * the app, and the whole library typechecks in place.
16
+ */
17
+
18
+ /** Absolute path to the template tree. Resolved from this module, so it holds in a workspace and once published. */
19
+ export const TEMPLATE_DIR: string = join(dirname(fileURLToPath(import.meta.url)), "..", "templates");
20
+
21
+ /**
22
+ * The named file groups this library offers, each keyed by path within {@link TEMPLATE_DIR}, grouped as
23
+ * `@pithy-sh/ui-react`'s are so a screen set maps one to one.
24
+ *
25
+ * `base` is written on every scaffold: the Expo app's entry and config, the router, the transport, the locale and
26
+ * the theme. Every other group is a screen set that rides on a capability being composed, with the helpers only
27
+ * those screens use. The home screen is not in a group: {@link HOME_SCREEN} picks one of its two variants.
28
+ *
29
+ * Tests for these files live in this package's `src/`, not in the tree, so nothing here is a test.
30
+ */
31
+ export const TEMPLATE_GROUPS = {
32
+ base: [
33
+ "app.config.ts",
34
+ "metro.config.js",
35
+ "metro-proxy.cjs",
36
+ "tsconfig.json",
37
+ "app/_layout.tsx",
38
+ "app/index.tsx",
39
+ "app/[...pithy].tsx",
40
+ "src/theme.ts",
41
+ "src/platform.ts",
42
+ "src/pithy-config.ts",
43
+ "src/pithy-config.generated.ts",
44
+ "src/pithy-locale.tsx",
45
+ "src/router.tsx",
46
+ "src/route-modules.ts",
47
+ "src/transport.ts",
48
+ ],
49
+ auth: [
50
+ "src/session.tsx",
51
+ "src/turnstile.tsx",
52
+ "src/useTurnstileWeb.ts",
53
+ "src/routes/pithy/sign-in.tsx",
54
+ "src/routes/pithy/otp.tsx",
55
+ "src/routes/pithy/callback.tsx",
56
+ "src/routes/pithy/account-suspended.tsx",
57
+ "src/routes/pithy/account-deleted.tsx",
58
+ "src/routes/pithy/organization-suspended.tsx",
59
+ ],
60
+ organization: ["src/routes/pithy/choose-organization.tsx", "src/routes/pithy/invitation.tsx"],
61
+ mcp: ["src/routes/pithy/agent-consent.tsx", "src/routes/pithy/connected-applications.tsx"],
62
+ payments: [
63
+ "src/payments.tsx",
64
+ "src/payments-store.ts",
65
+ "src/payments-store.native.ts",
66
+ "src/useStoreTransactions.ts",
67
+ "src/useAppFocus.ts",
68
+ "src/routes/pithy/paywall.tsx",
69
+ "src/routes/pithy/pricing.tsx",
70
+ "src/routes/pithy/subscription.tsx",
71
+ ],
72
+ consent: ["src/routes/pithy/consent.tsx"],
73
+ } as const satisfies Record<string, readonly string[]>;
74
+
75
+ /** One of {@link TEMPLATE_GROUPS}. */
76
+ export type TemplateGroup = keyof typeof TEMPLATE_GROUPS;
77
+
78
+ /**
79
+ * The adopter's own home screen, in two variants: the auth one carries the signed-in guard and a sign-out, the
80
+ * bare one asks the Worker's health route. Exactly one is written, always to the same place.
81
+ */
82
+ export const HOME_SCREEN = {
83
+ target: "src/routes/app/home.tsx",
84
+ auth: "src/routes/app/home.tsx",
85
+ bare: "src/routes/app/home.bare.tsx",
86
+ } as const;
87
+
88
+ /** The token the templates use where the name of the Worker the app calls belongs. */
89
+ export const WORKER_TOKEN = "__PITHY_WORKER__";
90
+
91
+ /** The token `app.config.ts` uses where the app's own name belongs: its `apps/<name>` directory. */
92
+ export const APP_TOKEN = "__PITHY_APP__";
package/src/version.ts ADDED
@@ -0,0 +1,13 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import { readFileSync } from "node:fs";
5
+ import { dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ /** This package's own version, read from its manifest beside `src/` and `dist/` alike. Node only: the scaffolder reads it. */
9
+ export const UI_REACT_NATIVE_VERSION: string = (
10
+ JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8")) as {
11
+ version: string;
12
+ }
13
+ ).version;
@@ -0,0 +1,7 @@
1
+ import type { ReactNode } from "react";
2
+ import { Router, useExpoRouterAdapter } from "../src/router";
3
+
4
+ /** Every path but `/`, handed to the kit's router. `src/router.tsx` reads the path and the query through the adapter. */
5
+ export default function Pithy(): ReactNode {
6
+ return <Router adapter={useExpoRouterAdapter()} />;
7
+ }
@@ -0,0 +1,31 @@
1
+ import { Slot } from "expo-router";
2
+ import { type ReactNode, StrictMode } from "react";
3
+ import { PithyLocale } from "../src/pithy-locale";
4
+ import { storeTransactions } from "../src/route-modules";
5
+
6
+ /**
7
+ * The app's root: one language over every screen, and the route `expo-router` resolved. Every path lands on
8
+ * `[...pithy].tsx` or `index.tsx`, which hand it to the kit's router, so screens register by dropping a file into
9
+ * `src/routes/`, exactly as they do in the React app.
10
+ *
11
+ * With the payments screens written, the store's listener runs here for the whole launch, so a purchase the store
12
+ * hands back at launch reaches the account before any screen asks for it.
13
+ *
14
+ * Your own catalogs go on `<PithyLocale messages={{ fr: … }}>`.
15
+ */
16
+ export default function Layout(): ReactNode {
17
+ return (
18
+ <StrictMode>
19
+ {storeTransactions && <StoreTransactions listen={storeTransactions} />}
20
+ <PithyLocale>
21
+ <Slot />
22
+ </PithyLocale>
23
+ </StrictMode>
24
+ );
25
+ }
26
+
27
+ /** Runs the store listener. A component of its own, so the layout calls the hook only when it was written. */
28
+ function StoreTransactions(props: { listen: () => void }): ReactNode {
29
+ props.listen();
30
+ return null;
31
+ }
@@ -0,0 +1,4 @@
1
+ import Pithy from "./[...pithy]";
2
+
3
+ /** `/`, which a catch-all route does not match, handed to the same router. */
4
+ export default Pithy;
@@ -0,0 +1,56 @@
1
+ import type { ExpoConfig } from "expo/config";
2
+ import generated from "./src/pithy-config.generated";
3
+
4
+ /** The host whose https links open the app, once `universalLinkOrigin` names one. */
5
+ const universalHost = generated.universalLinkOrigin?.replace(/^https:\/\//, "") ?? null;
6
+
7
+ /** Google's iOS sign-in returns on the reversed client id, as a URL scheme. */
8
+ const googleIosScheme = generated.google.iosClientId
9
+ ? generated.google.iosClientId.split(".").reverse().join(".")
10
+ : null;
11
+
12
+ /**
13
+ * The Expo app's config, read from the same file the screens read. The scheme a sign-in returns to and the origin
14
+ * a checkout returns through are stated once, in `src/pithy-config.generated.ts`, so the app cannot answer to a
15
+ * link the config does not name.
16
+ *
17
+ * The name and the bundle identifiers are yours: set `ios.bundleIdentifier` and `android.package` before a store
18
+ * build, and give Apple sign-in the same identifier the Worker's `appBundleIdentifier` names.
19
+ */
20
+ export default (): ExpoConfig => ({
21
+ name: "__PITHY_APP__",
22
+ slug: "__PITHY_APP__",
23
+ scheme: generated.appScheme,
24
+ version: "0.1.0",
25
+ orientation: "portrait",
26
+ userInterfaceStyle: "automatic",
27
+ ios: {
28
+ supportsTablet: true,
29
+ usesAppleSignIn: true,
30
+ associatedDomains: universalHost ? [`applinks:${universalHost}`] : [],
31
+ },
32
+ android: {
33
+ intentFilters: universalHost
34
+ ? [
35
+ {
36
+ action: "VIEW",
37
+ autoVerify: true,
38
+ data: [{ scheme: "https", host: universalHost }],
39
+ category: ["BROWSABLE", "DEFAULT"],
40
+ },
41
+ ]
42
+ : [],
43
+ },
44
+ web: { output: "single" },
45
+ plugins: [
46
+ "expo-router",
47
+ "expo-secure-store",
48
+ "expo-localization",
49
+ "expo-web-browser",
50
+ "expo-apple-authentication",
51
+ "expo-iap",
52
+ ...(googleIosScheme
53
+ ? [["@react-native-google-signin/google-signin", { iosUrlScheme: googleIosScheme }] as [string, unknown]]
54
+ : []),
55
+ ],
56
+ });
@@ -0,0 +1,289 @@
1
+ // The web build's way to the Worker under `pithy dev`.
2
+ //
3
+ // The web build calls the Worker by relative path, and in production that is right: the build is served from the
4
+ // Worker's own origin. Under `pithy dev` it is served by Metro, on Metro's own port, so a relative call reaches
5
+ // Metro. This sends the requests under the Worker's paths on to `origins.dev` in `src/pithy-config.generated.ts`,
6
+ // and hands Metro everything else.
7
+ //
8
+ // A request goes on as it came: method, headers and body, with its path and query resolved the way the Worker
9
+ // resolves them, so a path is matched where it lands. Its `Host` stays the page's, so the Worker answers it as
10
+ // same-origin with the page that sent it and sets its cookies there. The answer comes back as the Worker gave it:
11
+ // status, every header and every `Set-Cookie`, and the body.
12
+ //
13
+ // Metro runs this middleware only while it serves. `expo export` builds without it. Metro reads the config when it
14
+ // starts, so restart it after `pithy ui sync --app` rewrites the file.
15
+ //
16
+ // Node's own modules only: Metro loads this file before anything is bundled.
17
+
18
+ const fs = require("node:fs");
19
+ const http = require("node:http");
20
+ const https = require("node:https");
21
+ const path = require("node:path");
22
+ const { pipeline } = require("node:stream");
23
+
24
+ /** Where the generated config sits, relative to the app. */
25
+ const GENERATED_CONFIG = "src/pithy-config.generated.ts";
26
+
27
+ /** Where the kit's clients call each capability by default, for a generated config that lists no paths. */
28
+ const KIT_PATHS = {
29
+ organization: "/organizations",
30
+ consent: "/consent",
31
+ mcp: "/mcp",
32
+ health: "/health",
33
+ turnstile: "/turnstile",
34
+ };
35
+
36
+ /** Where an MCP client discovers the Worker's authorization server. Auth serves them. */
37
+ const OAUTH_DISCOVERY = ["/.well-known/oauth-authorization-server", "/.well-known/oauth-protected-resource"];
38
+
39
+ /**
40
+ * The app's own screens whose path is also a capability's base path. Metro serves the screen; the routes under
41
+ * it are the Worker's.
42
+ */
43
+ const SCREEN_PATHS = ["/consent"];
44
+
45
+ /** The line the generated value starts on. The value runs from there to `export default config`. */
46
+ const OPEN = "const config: GeneratedPithyConfig = ";
47
+ const CLOSE = /;\s*export default config;\s*$/;
48
+
49
+ /** A literal this reader cannot read. */
50
+ class Unreadable extends Error {}
51
+
52
+ /**
53
+ * The object literal in `text`: JSON, plus what a formatter or a hand edit leaves in a TypeScript literal, which is
54
+ * a key without quotes, a single-quoted string, a trailing comma and a comment. Anything else, such as an
55
+ * expression, throws: the file is read, never run.
56
+ */
57
+ function parseLiteral(text) {
58
+ let at = 0;
59
+ const skip = () => {
60
+ for (;;) {
61
+ while (at < text.length && /\s/.test(text[at])) at += 1;
62
+ if (text.startsWith("//", at)) {
63
+ const end = text.indexOf("\n", at);
64
+ at = end === -1 ? text.length : end + 1;
65
+ } else if (text.startsWith("/*", at)) {
66
+ const end = text.indexOf("*/", at + 2);
67
+ if (end === -1) throw new Unreadable();
68
+ at = end + 2;
69
+ } else return;
70
+ }
71
+ };
72
+ const consume = (char) => {
73
+ skip();
74
+ if (text[at] !== char) throw new Unreadable();
75
+ at += 1;
76
+ };
77
+ const string = () => {
78
+ const quote = text[at];
79
+ let end = at + 1;
80
+ let raw = "";
81
+ while (end < text.length && text[end] !== quote) {
82
+ const char = text[end];
83
+ if (char === "\n") throw new Unreadable();
84
+ if (char === "\\") {
85
+ const next = text[end + 1] ?? "";
86
+ raw += next === "'" ? "'" : char + next;
87
+ end += 2;
88
+ continue;
89
+ }
90
+ raw += char === '"' ? '\\"' : char;
91
+ end += 1;
92
+ }
93
+ if (end >= text.length) throw new Unreadable();
94
+ at = end + 1;
95
+ try {
96
+ return JSON.parse(`"${raw}"`);
97
+ } catch {
98
+ throw new Unreadable();
99
+ }
100
+ };
101
+ const value = () => {
102
+ skip();
103
+ const char = text[at];
104
+ if (char === "{") {
105
+ at += 1;
106
+ const object = {};
107
+ for (;;) {
108
+ skip();
109
+ if (text[at] === "}") {
110
+ at += 1;
111
+ return object;
112
+ }
113
+ let key;
114
+ if (text[at] === '"' || text[at] === "'") key = string();
115
+ else {
116
+ const name = /^[A-Za-z_$][A-Za-z0-9_$]*/.exec(text.slice(at))?.[0];
117
+ if (name === undefined) throw new Unreadable();
118
+ key = name;
119
+ at += name.length;
120
+ }
121
+ consume(":");
122
+ Object.defineProperty(object, key, { value: value(), enumerable: true, writable: true, configurable: true });
123
+ skip();
124
+ if (text[at] === ",") at += 1;
125
+ else if (text[at] !== "}") throw new Unreadable();
126
+ }
127
+ }
128
+ if (char === "[") {
129
+ at += 1;
130
+ const array = [];
131
+ for (;;) {
132
+ skip();
133
+ if (text[at] === "]") {
134
+ at += 1;
135
+ return array;
136
+ }
137
+ array.push(value());
138
+ skip();
139
+ if (text[at] === ",") at += 1;
140
+ else if (text[at] !== "]") throw new Unreadable();
141
+ }
142
+ }
143
+ if (char === '"' || char === "'") return string();
144
+ const word = /^(?:true|false|null|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)(?![A-Za-z0-9_$.])/.exec(
145
+ text.slice(at),
146
+ )?.[0];
147
+ if (word === undefined) throw new Unreadable();
148
+ at += word.length;
149
+ return JSON.parse(word);
150
+ };
151
+ const parsed = value();
152
+ skip();
153
+ if (at !== text.length) throw new Unreadable();
154
+ return parsed;
155
+ }
156
+
157
+ /**
158
+ * The value in a generated config's text, or `null` when the text is not one Pithy wrote. A file the project's
159
+ * formatter has reformatted, or an adopter has edited by hand, still reads.
160
+ */
161
+ function readGeneratedConfig(text) {
162
+ const start = text.indexOf(OPEN);
163
+ const close = CLOSE.exec(text);
164
+ if (start === -1 || close === null || close.index < start) return null;
165
+ try {
166
+ const value = parseLiteral(text.slice(start + OPEN.length, close.index));
167
+ return value && typeof value === "object" && value.origins && typeof value.origins.dev === "string" ? value : null;
168
+ } catch {
169
+ return null;
170
+ }
171
+ }
172
+
173
+ /**
174
+ * The paths the Worker answers. The generated config lists every base path the Worker's route table serves, as the
175
+ * CLI composed it; a config written before it carried the list falls back to what each capability's `dev`
176
+ * projection composes, since `pithy dev` serves `dev`, and the kit's defaults for the rest.
177
+ */
178
+ function proxiedPaths(config) {
179
+ if (Array.isArray(config.workerPaths) && config.workerPaths.every((path) => typeof path === "string")) {
180
+ return config.workerPaths;
181
+ }
182
+ const dev = (field) => config[field]?.dev;
183
+ const consent = dev("consent")?.enabled ? dev("consent").basePath : KIT_PATHS.consent;
184
+ const paths = [KIT_PATHS.organization, consent, KIT_PATHS.mcp, KIT_PATHS.health];
185
+ if (dev("auth")?.enabled) paths.push(dev("auth").basePath, ...OAUTH_DISCOVERY);
186
+ if (dev("payments")?.enabled) paths.push(dev("payments").basePath);
187
+ if (dev("turnstile")?.enabled) paths.push(KIT_PATHS.turnstile);
188
+ return paths;
189
+ }
190
+
191
+ /** The base a request target is resolved against. A target that names any other origin is not a path. */
192
+ const LOCAL = "http://metro.invalid";
193
+
194
+ /**
195
+ * The path and query to send the Worker for `url`, a request's target, or `null` when it stays with Metro. The
196
+ * target is resolved the way the Worker resolves it, dot segments and backslashes included, so a path that climbs
197
+ * out of a base path is matched where it lands.
198
+ */
199
+ function workerPath(url, paths) {
200
+ let resolved;
201
+ try {
202
+ resolved = new URL(url, LOCAL);
203
+ } catch {
204
+ return null;
205
+ }
206
+ if (resolved.origin !== LOCAL) return null;
207
+ const { pathname, search } = resolved;
208
+ if (SCREEN_PATHS.includes(pathname)) return null;
209
+ const under = paths.some((base) => pathname === base || pathname.startsWith(`${base}/`));
210
+ return under ? `${pathname}${search}` : null;
211
+ }
212
+
213
+ /** Whether `url`, a request's target, goes to the Worker: under one of `paths`, and not one of the app's screens. */
214
+ function isProxied(url, paths) {
215
+ return workerPath(url, paths) !== null;
216
+ }
217
+
218
+ /** A middleware that sends a request under `paths` to `target`, and any other to the next middleware. */
219
+ function createDevProxy({ target, paths }) {
220
+ const upstream = new URL(target);
221
+ const send = upstream.protocol === "https:" ? https.request : http.request;
222
+ return (req, res, next) => {
223
+ const forward = workerPath(req.url || "/", paths);
224
+ if (forward === null) {
225
+ next();
226
+ return;
227
+ }
228
+ const outgoing = send(
229
+ {
230
+ protocol: upstream.protocol,
231
+ hostname: upstream.hostname.replace(/^\[|\]$/g, ""),
232
+ port: upstream.port,
233
+ method: req.method,
234
+ path: forward,
235
+ headers: req.headers,
236
+ },
237
+ (answer) => {
238
+ res.writeHead(answer.statusCode || 502, answer.statusMessage, answer.rawHeaders);
239
+ pipeline(answer, res, () => undefined);
240
+ },
241
+ );
242
+ res.on("close", () => {
243
+ if (!res.writableFinished) outgoing.destroy();
244
+ });
245
+ outgoing.on("error", (error) => {
246
+ if (res.headersSent) {
247
+ res.destroy(error);
248
+ return;
249
+ }
250
+ res.writeHead(502, { "content-type": "text/plain; charset=utf-8" });
251
+ res.end(`The Worker did not answer at ${target}. Start it with pithy dev. ${error.message}\n`);
252
+ });
253
+ req.pipe(outgoing);
254
+ };
255
+ }
256
+
257
+ /**
258
+ * `config` with the dev proxy in front of Metro's middleware, and of any enhancer already there. A generated
259
+ * config it cannot read leaves `config` as it was, and says why.
260
+ */
261
+ function withDevProxy(config, appDir, warn = console.warn) {
262
+ let generated = null;
263
+ try {
264
+ generated = readGeneratedConfig(fs.readFileSync(path.join(appDir, GENERATED_CONFIG), "utf8"));
265
+ } catch {
266
+ generated = null;
267
+ }
268
+ if (generated === null) {
269
+ warn(
270
+ `The web build cannot reach the Worker under Metro: ${GENERATED_CONFIG} is not a file Pithy wrote. Run pithy ui sync --app.`,
271
+ );
272
+ return config;
273
+ }
274
+ const proxy = createDevProxy({ target: generated.origins.dev, paths: proxiedPaths(generated) });
275
+ const server = config.server || {};
276
+ const earlier = server.enhanceMiddleware;
277
+ return {
278
+ ...config,
279
+ server: {
280
+ ...server,
281
+ enhanceMiddleware: (middleware, metro) => {
282
+ const inner = earlier ? earlier(middleware, metro) : middleware;
283
+ return (req, res, next) => proxy(req, res, () => inner(req, res, next));
284
+ },
285
+ },
286
+ };
287
+ }
288
+
289
+ module.exports = { KIT_PATHS, readGeneratedConfig, proxiedPaths, isProxied, createDevProxy, withDevProxy };
@@ -0,0 +1,6 @@
1
+ // Expo's Metro config, with the web build's way to the Worker under `pithy dev`: Metro serves the web build on its
2
+ // own port, and `metro-proxy.cjs` sends the calls meant for the Worker on to it.
3
+ const { getDefaultConfig } = require("expo/metro-config");
4
+ const { withDevProxy } = require("./metro-proxy.cjs");
5
+
6
+ module.exports = withDevProxy(getDefaultConfig(__dirname), __dirname);
@@ -0,0 +1,20 @@
1
+ import type * as ExpoIap from "expo-iap";
2
+
3
+ /**
4
+ * The platform's store, StoreKit or Play Billing, through `expo-iap`: the file Metro resolves `./payments-store` to
5
+ * on a phone. The module loads on the first store call, so an app that never sells through a store never loads it.
6
+ */
7
+
8
+ /** `expo-iap`, as the store calls reach it. */
9
+ export type StoreModule = typeof ExpoIap;
10
+
11
+ let loading: Promise<StoreModule | null> | null = null;
12
+
13
+ /** The store on this platform, loaded once. A failed load is forgotten, so the next call tries again. */
14
+ export function loadStore(): Promise<StoreModule | null> {
15
+ loading ??= import("expo-iap").catch(() => {
16
+ loading = null;
17
+ return null;
18
+ });
19
+ return loading;
20
+ }
@@ -0,0 +1,16 @@
1
+ import type * as ExpoIap from "expo-iap";
2
+
3
+ /**
4
+ * The platform's store, StoreKit or Play Billing, through `expo-iap`.
5
+ *
6
+ * Metro resolves `./payments-store` to `payments-store.native.ts` on a phone and to this file on the web, so the
7
+ * web bundle never carries `expo-iap`: the web sells on a hosted page and has no store to reach.
8
+ */
9
+
10
+ /** `expo-iap`, as the store calls reach it. */
11
+ export type StoreModule = typeof ExpoIap;
12
+
13
+ /** The store on this platform. The web has none. */
14
+ export function loadStore(): Promise<StoreModule | null> {
15
+ return Promise.resolve(null);
16
+ }