@whop/react-native 0.0.10 → 0.0.12

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.
package/dist/cli/index.js CHANGED
@@ -25,15 +25,16 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
25
25
 
26
26
  // src/cli/index.ts
27
27
  var import_node_fs2 = require("fs");
28
- var import_node_path2 = __toESM(require("path"));
28
+ var import_node_path4 = __toESM(require("path"));
29
29
  var import_node_util = require("util");
30
30
  var import_find_up2 = require("find-up");
31
+ var import_qrcode_terminal = __toESM(require("qrcode-terminal"));
31
32
  var import_rimraf = require("rimraf");
32
33
 
33
34
  // src/cli/mobile.ts
34
35
  var import_node_fs = require("fs");
35
- var import_promises = require("fs/promises");
36
- var import_node_path = __toESM(require("path"));
36
+ var import_promises2 = require("fs/promises");
37
+ var import_node_path2 = __toESM(require("path"));
37
38
  var import_metro_config = require("@react-native/metro-config");
38
39
  var import_find_up = require("find-up");
39
40
  var import_jszip = __toESM(require("jszip"));
@@ -84,7 +85,25 @@ async function getChecksum(data) {
84
85
  }
85
86
 
86
87
  // src/cli/valid-view-type.ts
88
+ var import_promises = require("fs/promises");
89
+ var import_node_path = __toESM(require("path"));
87
90
  var VALID_VIEW_TYPES = ["experience-view", "discover-view"];
91
+ async function getSupportedAppViewTypes(root) {
92
+ const views = await (0, import_promises.readdir)(import_node_path.default.join(root, "src", "views"), {
93
+ withFileTypes: true,
94
+ recursive: false
95
+ });
96
+ const files = views.filter((file) => file.isFile()).map((file) => file.name.split(".")[0]).filter((file) => !!file);
97
+ const validViews = files.filter(
98
+ (file) => VALID_VIEW_TYPES.includes(file)
99
+ );
100
+ if (validViews.length === 0) {
101
+ throw new Error(
102
+ `No valid views found, please create a view in the src/views folder and name it with a valid view type: ${VALID_VIEW_TYPES.join(", ")}`
103
+ );
104
+ }
105
+ return validViews;
106
+ }
88
107
 
89
108
  // src/cli/mobile.ts
90
109
  async function buildAndPublish(root, platform, {
@@ -103,14 +122,14 @@ async function buildAndPublish(root, platform, {
103
122
  }
104
123
  async function bundle(root, platform) {
105
124
  await makeEntrypoint(root, platform);
106
- const outputFile = import_node_path.default.join(
125
+ const outputFile = import_node_path2.default.join(
107
126
  root,
108
127
  "build",
109
128
  "output",
110
129
  platform,
111
130
  "main_js_bundle"
112
131
  );
113
- await (0, import_promises.mkdir)(import_node_path.default.dirname(outputFile), { recursive: true });
132
+ await (0, import_promises2.mkdir)(import_node_path2.default.dirname(outputFile), { recursive: true });
114
133
  const defaultConfig = (0, import_metro_config.getDefaultConfig)(root);
115
134
  const babelLocation = require.resolve("@babel/runtime/package");
116
135
  const bableNodeModules = await (0, import_find_up.findUp)("node_modules", {
@@ -130,7 +149,7 @@ async function bundle(root, platform) {
130
149
  },
131
150
  watchFolders: [
132
151
  root,
133
- import_node_path.default.resolve(root, "node_modules"),
152
+ import_node_path2.default.resolve(root, "node_modules"),
134
153
  bableNodeModules
135
154
  ],
136
155
  reporter: new CustomReporter(),
@@ -151,30 +170,14 @@ async function bundle(root, platform) {
151
170
  out: outputFile
152
171
  }
153
172
  );
154
- await (0, import_promises.rename)(
173
+ await (0, import_promises2.rename)(
155
174
  `${outputFile}.js`,
156
- import_node_path.default.join(root, "build", "output", platform, "main_js_bundle.hbc")
175
+ import_node_path2.default.join(root, "build", "output", platform, "main_js_bundle.hbc")
157
176
  );
158
177
  console.log(` \u2714\uFE0E [${platform}] bundle created`);
159
178
  }
160
- async function getSupportedAppViewTypes(root) {
161
- const views = await (0, import_promises.readdir)(import_node_path.default.join(root, "src", "views"), {
162
- withFileTypes: true,
163
- recursive: false
164
- });
165
- const files = views.filter((file) => file.isFile()).map((file) => file.name.split(".")[0]).filter((file) => !!file);
166
- const validViews = files.filter(
167
- (file) => VALID_VIEW_TYPES.includes(file)
168
- );
169
- if (validViews.length === 0) {
170
- throw new Error(
171
- `No valid views found, please create a view in the src/views folder and name it with a valid view type: ${VALID_VIEW_TYPES.join(", ")}`
172
- );
173
- }
174
- return validViews;
175
- }
176
179
  async function makeEntrypoint(root, platform) {
177
- const entrypoint = import_node_path.default.join(
180
+ const entrypoint = import_node_path2.default.join(
178
181
  root,
179
182
  "build",
180
183
  "entrypoints",
@@ -194,16 +197,16 @@ ${imports.join("\n")}
194
197
 
195
198
  ${registry.join("\n")}
196
199
  `;
197
- const entrypointDir = import_node_path.default.dirname(entrypoint);
198
- await (0, import_promises.mkdir)(entrypointDir, { recursive: true });
199
- await (0, import_promises.writeFile)(entrypoint, entrypointContent, "utf-8");
200
+ const entrypointDir = import_node_path2.default.dirname(entrypoint);
201
+ await (0, import_promises2.mkdir)(entrypointDir, { recursive: true });
202
+ await (0, import_promises2.writeFile)(entrypoint, entrypointContent, "utf-8");
200
203
  console.log(` \u2714\uFE0E [${platform}] entrypoint created`);
201
204
  return entrypoint;
202
205
  }
203
206
  async function createMobileBuild(root, platform) {
204
207
  const viewTypes = await getSupportedAppViewTypes(root);
205
- const fullDirectory = import_node_path.default.join(root, "build", "output", platform);
206
- const mainJsBundle = import_node_path.default.join(fullDirectory, "main_js_bundle.hbc");
208
+ const fullDirectory = import_node_path2.default.join(root, "build", "output", platform);
209
+ const mainJsBundle = import_node_path2.default.join(fullDirectory, "main_js_bundle.hbc");
207
210
  if (!(0, import_node_fs.existsSync)(mainJsBundle)) {
208
211
  throw new Error(`main_js_bundle.hbc not found in ${fullDirectory}`);
209
212
  }
@@ -226,7 +229,7 @@ async function createMobileBuild(root, platform) {
226
229
  console.log(
227
230
  ` \u2714\uFE0E [${platform}] uploaded build: ${fileName} (${(zipData.length / 1024).toFixed(0)} KB)`
228
231
  );
229
- const build = await whopSdk.apps.createAppBuild({
232
+ const build2 = await whopSdk.apps.createAppBuild({
230
233
  attachment: { directUploadId: uploadedFile.directUploadId },
231
234
  checksum,
232
235
  platform,
@@ -237,25 +240,25 @@ async function createMobileBuild(root, platform) {
237
240
  })[view]
238
241
  )
239
242
  });
240
- if (!build) {
243
+ if (!build2) {
241
244
  throw new Error("Failed to create app build");
242
245
  }
243
- const dashboardUrl = `https://whop.com/dashboard/${COMPANY_ID}/developer/apps/${APP_ID}/builds/`;
246
+ const dashboardUrl = `https://whop.com/dashboard/${COMPANY_ID}/developer/apps/${APP_ID}/builds/?platform=${platform}`;
244
247
  console.log(`
245
248
  \u2714\uFE0E [${platform}] deployed as development build \u2714\uFE0E
246
- - build id: ${build.id}
249
+ - build id: ${build2.id}
247
250
  - view types: ${viewTypes.join(", ")}
248
251
  - promote to production here: ${dashboardUrl}
249
252
  `);
250
- return build;
253
+ return build2;
251
254
  }
252
255
  async function zipDirectory(directory) {
253
256
  const zip = new import_jszip.default();
254
257
  function addFilesToZip(currentPath, relativePath = "") {
255
258
  const items = (0, import_node_fs.readdirSync)(currentPath);
256
259
  for (const item of items) {
257
- const fullPath = import_node_path.default.join(currentPath, item);
258
- const zipPath = relativePath ? import_node_path.default.join(relativePath, item) : item;
260
+ const fullPath = import_node_path2.default.join(currentPath, item);
261
+ const zipPath = relativePath ? import_node_path2.default.join(relativePath, item) : item;
259
262
  const stats = (0, import_node_fs.statSync)(fullPath);
260
263
  if (stats.isDirectory()) {
261
264
  addFilesToZip(fullPath, zipPath);
@@ -274,6 +277,317 @@ var CustomReporter = class {
274
277
  }
275
278
  };
276
279
 
280
+ // src/cli/web.ts
281
+ var import_promises3 = require("fs/promises");
282
+ var import_node_path3 = __toESM(require("path"));
283
+ var import_esbuild = require("esbuild");
284
+
285
+ // src/cli/reanimated-bable.ts
286
+ var fs = __toESM(require("fs/promises"));
287
+ var path3 = __toESM(require("path"));
288
+ var babel = __toESM(require("@babel/core"));
289
+ var JS_RE = /\.(m|c)?(t|j)sx?$/;
290
+ function reanimatedBabelPlugin() {
291
+ const shouldTransform = (p) => {
292
+ if (!JS_RE.test(p)) return false;
293
+ if (p.includes(`${path3.sep}react-native-reanimated${path3.sep}`))
294
+ return true;
295
+ if (p.includes(`${path3.sep}node_modules${path3.sep}`)) return false;
296
+ return p.includes(`${path3.sep}src${path3.sep}`);
297
+ };
298
+ return {
299
+ name: "reanimated-babel",
300
+ setup(b) {
301
+ b.onLoad({ filter: JS_RE }, async (args) => {
302
+ if (!shouldTransform(args.path)) {
303
+ return null;
304
+ }
305
+ const code = await fs.readFile(args.path, "utf8");
306
+ const result = await babel.transformAsync(code, {
307
+ filename: args.path,
308
+ sourceMaps: false,
309
+ babelrc: false,
310
+ configFile: false,
311
+ // ORDER MATTERS: Reanimated plugin MUST BE LAST
312
+ plugins: [
313
+ // Needed by Reanimated on web per docs
314
+ "@babel/plugin-transform-export-namespace-from",
315
+ // Handle Flow types present in some RN libs
316
+ [
317
+ "@babel/plugin-transform-flow-strip-types",
318
+ { allowDeclareFields: true }
319
+ ],
320
+ // MUST be last
321
+ [
322
+ "react-native-reanimated/plugin",
323
+ { relativeSourceLocation: true }
324
+ ]
325
+ ],
326
+ presets: [],
327
+ // esbuild handles TS/JSX syntax; no preset-env/preset-react
328
+ caller: { name: "esbuild" },
329
+ // Let Babel parse TS/JSX/Flow; keep it broad
330
+ parserOpts: { plugins: ["jsx", "typescript"] },
331
+ generatorOpts: { decoratorsBeforeExport: true }
332
+ });
333
+ return {
334
+ // biome-ignore lint/style/noNonNullAssertion: <explanation>
335
+ contents: result.code,
336
+ // biome-ignore lint/suspicious/noExplicitAny: <explanation>
337
+ loader: pickLoader(args.path)
338
+ };
339
+ });
340
+ }
341
+ };
342
+ }
343
+ function pickLoader(file) {
344
+ const ext = path3.extname(file).toLowerCase();
345
+ if (ext === ".tsx") return "tsx";
346
+ if (ext === ".ts") return "ts";
347
+ if (ext === ".jsx") return "jsx";
348
+ return "jsx";
349
+ }
350
+
351
+ // src/cli/strip-flow.ts
352
+ var fs2 = __toESM(require("fs/promises"));
353
+ var babel2 = __toESM(require("@babel/core"));
354
+ function stripFlowWithBabel() {
355
+ const filter = /\.(m|c)?jsx?$/;
356
+ return {
357
+ name: "strip-flow-with-babel",
358
+ // biome-ignore lint/suspicious/noExplicitAny: <explanation>
359
+ setup(b) {
360
+ b.onLoad({ filter }, async (args) => {
361
+ const code = await fs2.readFile(args.path, "utf8");
362
+ const out = await babel2.transformAsync(code, {
363
+ filename: args.path,
364
+ babelrc: false,
365
+ configFile: false,
366
+ plugins: [
367
+ [
368
+ "@babel/plugin-transform-flow-strip-types",
369
+ { allowDeclareFields: true }
370
+ ]
371
+ ],
372
+ parserOpts: { plugins: ["jsx", "flow"] },
373
+ sourceMaps: false
374
+ });
375
+ return { contents: out.code, loader: "jsx" };
376
+ });
377
+ }
378
+ };
379
+ }
380
+
381
+ // src/cli/web.ts
382
+ function aliasReactNativePlugin() {
383
+ return {
384
+ name: "alias-react-native",
385
+ setup(b) {
386
+ b.onResolve({ filter: /^react-native$/ }, () => ({
387
+ path: require.resolve("react-native-web")
388
+ }));
389
+ }
390
+ };
391
+ }
392
+ function forceSingleReact() {
393
+ const map = /* @__PURE__ */ new Map([
394
+ ["react", require.resolve("react")],
395
+ ["react/jsx-runtime", require.resolve("react/jsx-runtime")],
396
+ ["react/jsx-dev-runtime", require.resolve("react/jsx-dev-runtime")],
397
+ ["react-dom", require.resolve("react-dom")],
398
+ ["react-dom/client", require.resolve("react-dom/client")]
399
+ ]);
400
+ const rx = /^(react(?:\/jsx-(?:dev-)?runtime)?|react-dom(?:\/client)?)$/;
401
+ return {
402
+ name: "force-single-react",
403
+ setup(b) {
404
+ b.onResolve({ filter: rx }, (args) => ({ path: map.get(args.path) }));
405
+ }
406
+ };
407
+ }
408
+ function toPascalCase(str) {
409
+ return str.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
410
+ }
411
+ async function makeWebEntrypoint(root) {
412
+ const files = await getSupportedAppViewTypes(root);
413
+ const packageJsonPath = import_node_path3.default.join(root, "package.json");
414
+ const packageJson = JSON.parse(await (0, import_promises3.readFile)(packageJsonPath, "utf-8"));
415
+ const hasReactNativeReanimated = packageJson.dependencies?.["react-native-reanimated"];
416
+ const imports = files.map(
417
+ (file) => `import { ${toPascalCase(file)} } from "../../../src/views/${file}";`
418
+ );
419
+ const registry = files.map(
420
+ (file) => `AppRegistry.registerComponent("${toPascalCase(file)}", () => WhopNavigationWrapper(React, "${toPascalCase(file)}", ${toPascalCase(file)}));`
421
+ );
422
+ const defaultKey = toPascalCase(files[0] ?? "experience-view");
423
+ const reanimatedImport = hasReactNativeReanimated ? `import "react-native-reanimated";` : "";
424
+ const entry = `import { AppRegistry } from "react-native";
425
+ import * as React from "react";
426
+ import { WhopNavigationWrapper } from "@whop/react-native/web";
427
+ ${reanimatedImport}
428
+
429
+ ${imports.join("\n")}
430
+
431
+ ${registry.join("\n")}
432
+
433
+ const root = document.getElementById("root") || (() => {
434
+ const d = document.createElement("div");
435
+ d.id = "root";
436
+ document.body.appendChild(d);
437
+ return d;
438
+ })();
439
+ AppRegistry.runApplication("${defaultKey}", { rootTag: root });
440
+ `;
441
+ const entryFile = import_node_path3.default.join(root, "build", "entrypoints", "web", "index.tsx");
442
+ await (0, import_promises3.mkdir)(import_node_path3.default.dirname(entryFile), { recursive: true });
443
+ await (0, import_promises3.writeFile)(entryFile, entry, "utf-8");
444
+ return entryFile;
445
+ }
446
+ async function bundleWeb(root) {
447
+ const entry = await makeWebEntrypoint(root);
448
+ const outDir = import_node_path3.default.join(root, "build", "output", "web");
449
+ await (0, import_promises3.mkdir)(outDir, { recursive: true });
450
+ await (0, import_esbuild.build)({
451
+ entryPoints: [entry],
452
+ outfile: import_node_path3.default.join(outDir, "main.js"),
453
+ bundle: true,
454
+ minify: false,
455
+ format: "esm",
456
+ platform: "browser",
457
+ sourcemap: false,
458
+ jsx: "automatic",
459
+ mainFields: ["browser", "module", "main"],
460
+ conditions: ["browser", "import", "default"],
461
+ define: {
462
+ process: "{}",
463
+ "process.env": "{}",
464
+ "process.env.NODE_ENV": '"production"',
465
+ __DEV__: "false",
466
+ "process.env.NEXT_PUBLIC_WHOP_APP_ID": `"${APP_ID}"`,
467
+ // Some RN libraries (e.g., RNGH) expect a Node-like global in the browser
468
+ global: "globalThis"
469
+ },
470
+ resolveExtensions: [
471
+ ".web.tsx",
472
+ ".web.ts",
473
+ ".web.js",
474
+ ".tsx",
475
+ ".ts",
476
+ ".jsx",
477
+ ".js"
478
+ ],
479
+ loader: {
480
+ ".png": "dataurl",
481
+ ".jpg": "dataurl",
482
+ ".jpeg": "dataurl",
483
+ ".svg": "dataurl",
484
+ ".ttf": "dataurl",
485
+ ".woff": "dataurl",
486
+ ".woff2": "dataurl",
487
+ ".js": "jsx",
488
+ ".jsx": "jsx"
489
+ },
490
+ plugins: [
491
+ forceSingleReact(),
492
+ aliasReactNativePlugin(),
493
+ reanimatedBabelPlugin(),
494
+ stripFlowWithBabel(),
495
+ {
496
+ name: "force-native-web-stub",
497
+ setup(b) {
498
+ b.onResolve({ filter: /^\.\/native-whop-core$/ }, (args) => {
499
+ return {
500
+ path: import_node_path3.default.join(args.resolveDir, "native-whop-core"),
501
+ namespace: "file"
502
+ };
503
+ });
504
+ }
505
+ }
506
+ ]
507
+ });
508
+ const html = `<!doctype html>
509
+ <html>
510
+ <head>
511
+ <meta charset="utf-8" />
512
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
513
+ <title>Whop App (Web)</title>
514
+ <style>
515
+ #root {
516
+ width: 100vw;
517
+ height: 100vh;
518
+ margin: 0;
519
+ padding: 0;
520
+ overflow: hidden;
521
+ display: flex;
522
+ flex-direction: column;
523
+ align-items: stretch;
524
+ justify-content: start;
525
+ }
526
+ </style>
527
+ </head>
528
+ <body>
529
+ <div id="root"></div>
530
+ <script type="module" src="./main.js"></script>
531
+ </body>
532
+ </html>`;
533
+ await (0, import_promises3.writeFile)(import_node_path3.default.join(outDir, "index.html"), html, "utf-8");
534
+ console.log(" \u2714\uFE0E [web] bundle created at build/output/web/main.js");
535
+ }
536
+ async function buildAndPublish2(root, {
537
+ shouldBuild = true,
538
+ shouldUpload = true
539
+ } = {
540
+ shouldBuild: true,
541
+ shouldUpload: true
542
+ }) {
543
+ if (shouldBuild) {
544
+ await bundleWeb(root);
545
+ }
546
+ if (shouldUpload) {
547
+ await createWebBuild(root);
548
+ }
549
+ }
550
+ async function createWebBuild(root) {
551
+ const fullDirectory = import_node_path3.default.join(root, "build", "output", "web");
552
+ const mainJsFile = import_node_path3.default.join(fullDirectory, "main.js");
553
+ try {
554
+ await (0, import_promises3.readFile)(mainJsFile);
555
+ } catch {
556
+ throw new Error(`main.js not found in ${fullDirectory}`);
557
+ }
558
+ const buf = await (0, import_promises3.readFile)(mainJsFile);
559
+ const checksum = await getChecksum(buf);
560
+ console.log(` \u2714\uFE0E [web] build checksummed: ${checksum}`);
561
+ const fileName = `rnweb_${checksum}.js`;
562
+ const uploadedFile = await uploadFile(
563
+ buf,
564
+ fileName,
565
+ "application/javascript"
566
+ );
567
+ console.log(
568
+ ` \u2714\uFE0E [web] uploaded build: ${fileName} (${(buf.length / 1024).toFixed(0)} KB)`
569
+ );
570
+ const build2 = await whopSdk.apps.createAppBuild({
571
+ attachment: { directUploadId: uploadedFile.directUploadId },
572
+ checksum,
573
+ platform: "web",
574
+ supportedAppViewTypes: ["hub"]
575
+ });
576
+ if (!build2) {
577
+ throw new Error("Failed to create app build");
578
+ }
579
+ const dashboardUrl = `https://whop.com/dashboard/${COMPANY_ID}/developer/apps/${APP_ID}/builds/?platform=web`;
580
+ console.log(
581
+ `
582
+ \u2714\uFE0E [web] deployed as development build \u2714\uFE0E
583
+ - build id: ${build2.id}
584
+ - view types: hub
585
+ - promote to production here: ${dashboardUrl}
586
+ `
587
+ );
588
+ return build2;
589
+ }
590
+
277
591
  // src/cli/index.ts
278
592
  async function main() {
279
593
  const args = (0, import_node_util.parseArgs)({
@@ -293,6 +607,10 @@ async function main() {
293
607
  args: process.argv.slice(2)
294
608
  });
295
609
  const [command] = args.positionals;
610
+ if (command === "install") {
611
+ await handleInstall();
612
+ return;
613
+ }
296
614
  let shouldBuild = true;
297
615
  let shouldUpload = true;
298
616
  let shouldClean = true;
@@ -331,12 +649,12 @@ async function main() {
331
649
  promises.push(buildAndPublish(root, "android", opts));
332
650
  }
333
651
  if (args.values.web || !didProvidePlatform) {
334
- console.warn(" - [web] builds for web are not supported yet - coming soon");
652
+ promises.push(buildAndPublish2(root, opts));
335
653
  }
336
654
  await Promise.all(promises);
337
655
  }
338
656
  async function cleanBuildDirectory(root) {
339
- const buildDirectory = import_node_path2.default.join(root, "build");
657
+ const buildDirectory = import_node_path4.default.join(root, "build");
340
658
  if ((0, import_node_fs2.existsSync)(buildDirectory)) {
341
659
  await (0, import_rimraf.rimraf)(buildDirectory);
342
660
  }
@@ -349,9 +667,20 @@ async function getRootProjectDirectory() {
349
667
  "please run this command inside a whop react native project"
350
668
  );
351
669
  }
352
- const root = import_node_path2.default.dirname(file);
670
+ const root = import_node_path4.default.dirname(file);
353
671
  return root;
354
672
  }
673
+ async function handleInstall() {
674
+ const appId = env("NEXT_PUBLIC_WHOP_APP_ID");
675
+ const installLink = `https://whop.com/apps/${appId}/install`;
676
+ console.log(`
677
+ Open this link in your browser to install the app into your whop.
678
+ ${installLink}
679
+
680
+ Or scan the QR code with your iPhone:
681
+ `);
682
+ import_qrcode_terminal.default.generate(installLink, { small: true });
683
+ }
355
684
  main().catch((err) => {
356
685
  console.error(err);
357
686
  process.exit(1);