@mrrocket/sdk 0.13.0 → 0.14.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.
@@ -414,6 +414,116 @@ interface FlowsController {
414
414
  };
415
415
  }
416
416
 
417
+ /**
418
+ * Paywalls with several pages (SDK 0.14.0): what MRRocket's config serves under `paywalls` (one per placement, built
419
+ * in MRRocket › Conversion › Paywalls) and a small controller that walks the person through its pages, page after page,
420
+ * until the last one hands the chosen plan to the app. No DOM and no React: the web renderer (`mountPaywall` in
421
+ * flows-ui.ts, loaded only when a paywall opens) and the React Native one (native/flows-view.ts) draw `view()` and
422
+ * call the actions.
423
+ *
424
+ * MRRocket never takes a payment. The last page's button calls the app's `onPurchase(plan)`, which buys through the
425
+ * app's own store code (StoreKit or Play through RevenueCat or Adapty, or Stripe Checkout on the web); only when it
426
+ * resolves is the purchase recorded. It throwing keeps the paywall open with an error, resolving `false` (the person
427
+ * closed the store sheet) keeps it open without one.
428
+ *
429
+ * A running test on the paywall (`experiment`) gives each person their variant's pages, by the same stable hash as
430
+ * every other test (`assign`), and records `$exposure` when the paywall opens. What it records, as normal events under
431
+ * the SDK's analytics consent:
432
+ * - `$paywall` `{paywall, placement, variant, page, index, action: "page"}` the first time each page shows in a run,
433
+ * so MRRocket shows how many reach page 2 and 3;
434
+ * - `{action: "purchased", plan}` after `onPurchase` went through (a test's purchase goal counts it), and
435
+ * `{action: "closed", index}` when the person closed it.
436
+ */
437
+
438
+ /** One page of a paywall. Plans pages list the plans and buy; the others say why and go on. */
439
+ interface PaywallPage {
440
+ id: string;
441
+ /** "benefits": a list of what the person gets; "proof": quotes of happy customers; "plans": the plans and the buy button. */
442
+ kind: "benefits" | "proof" | "plans";
443
+ title: string;
444
+ body?: string;
445
+ /** Benefits: one line each. */
446
+ items?: string[];
447
+ /** Proof: what customers said, and who. */
448
+ quotes?: {
449
+ text: string;
450
+ by?: string;
451
+ }[];
452
+ /** The button: "Continue" on the way, the buy button on the plans page ("Start free trial"). */
453
+ button?: string;
454
+ }
455
+ /** A plan the paywall offers. `id` is what the app buys: the store product id or the Stripe price, as set in MRRocket. */
456
+ interface PaywallPlan {
457
+ id: string;
458
+ name: string;
459
+ /** As shown ("€59.99 / year"). The app's own store prices win when it passes `plans`. */
460
+ price: string;
461
+ /** A line under it ("€5 a month", "7 days free"). */
462
+ note?: string;
463
+ }
464
+ /** A published paywall as the config serves it. */
465
+ interface PaywallConfig {
466
+ id: string;
467
+ /** The placement's key (`onboarding_end`), what the app opens: `paywall.open("onboarding_end")`. */
468
+ placement: string;
469
+ /** The running test on it, a key of `experiments`; each of its variants has pages under `variants`. */
470
+ experiment?: string;
471
+ /** Control (the live paywall) first. */
472
+ variants: {
473
+ id: VariantId;
474
+ pages: PaywallPage[];
475
+ }[];
476
+ plans: PaywallPlan[];
477
+ /** The plan picked before the person picks one. */
478
+ highlight?: string;
479
+ }
480
+ interface PaywallResult {
481
+ /**
482
+ * "purchased": `onPurchase` went through; "chosen": the person picked a plan and there is no `onPurchase`, so the app
483
+ * buys it now (the store's purchase still counts for a test through RevenueCat or Adapty); "closed": the person
484
+ * closed it (or there was no paywall).
485
+ */
486
+ outcome: "purchased" | "chosen" | "closed";
487
+ plan?: PaywallPlan;
488
+ paywall?: string;
489
+ variant?: VariantId;
490
+ /** The page the person was on, from 0. */
491
+ index: number;
492
+ }
493
+ interface PaywallOptions {
494
+ /**
495
+ * Buy the plan in your code (RevenueCat `purchasePackage`, Adapty `makePurchase`, Stripe Checkout). Throwing keeps
496
+ * the paywall open with an error; `false` keeps it open without one (the person cancelled the store sheet).
497
+ */
498
+ onPurchase?: (plan: PaywallPlan) => unknown;
499
+ /** Your store's plans with their local prices, by the same ids; they replace what MRRocket has for them. */
500
+ plans?: PaywallPlan[];
501
+ onClose?: (result: PaywallResult) => void;
502
+ }
503
+ interface PaywallView {
504
+ paywall: PaywallConfig;
505
+ variant: VariantId;
506
+ page: PaywallPage;
507
+ index: number;
508
+ total: number;
509
+ last: boolean;
510
+ plans: PaywallPlan[];
511
+ /** The picked plan's id. */
512
+ plan?: string;
513
+ busy: boolean;
514
+ error?: string;
515
+ }
516
+ interface PaywallHost {
517
+ /** Opens the paywall of a placement; resolves when it is bought or closed (at once with "closed" when there is none). */
518
+ open(placement: string, options?: PaywallOptions): Promise<PaywallResult>;
519
+ view(): PaywallView | null;
520
+ subscribe(listener: () => void): () => void;
521
+ next(): void;
522
+ back(): void;
523
+ pick(planId: string): void;
524
+ close(): void;
525
+ }
526
+
417
527
  /**
418
528
  * The part of the SDK that runs anywhere: experiment config, variant assignment, preview links and event shapes.
419
529
  * No React, no browser APIs, so the server helper, the proxy helper and the client share it.
@@ -421,7 +531,7 @@ interface FlowsController {
421
531
 
422
532
  type VariantId = string;
423
533
  /** This build of the SDK. Sent with the config request so MRRocket knows what the site runs (error tracking, flags). */
424
- declare const SDK_VERSION = "0.13.0";
534
+ declare const SDK_VERSION = "0.14.0";
425
535
  /** One variant of an experiment. `changes` replaces texts that the page marks with `<Text id>`. */
426
536
  interface VariantConfig {
427
537
  id: VariantId;
@@ -478,6 +588,8 @@ interface CockpitConfig {
478
588
  messages?: boolean;
479
589
  /** Page redirects set in MRRocket; the SDK sends visitors on before the page shows (`redirectFor`). SDK 0.7.0+. */
480
590
  redirects?: RedirectConfig[];
591
+ /** Published paywalls with their pages, per placement (MRRocket › Conversion › Paywalls), for `paywall.open()`. SDK 0.14.0+. */
592
+ paywalls?: PaywallConfig[];
481
593
  }
482
594
  /** One condition of a flag's audience, on a property of the person (`identify(id, props)`), the id or the page. */
483
595
  interface FlagCondition {
@@ -668,6 +780,14 @@ interface MRRocketClient {
668
780
  openCancelFlow(options?: CancelFlowOptions): Promise<CancelFlowResult>;
669
781
  /** Starts an onboarding flow now (the first live one without an id). False when there is none. */
670
782
  startOnboarding(id?: string): boolean;
783
+ /**
784
+ * Shows the paywall of a placement built in MRRocket › Paywalls page by page (drawn by `<MRRocketProvider>`). Its
785
+ * last page hands the chosen plan to `onPurchase`, where your code buys it (RevenueCat, Adapty, StoreKit, Play);
786
+ * MRRocket never takes a payment. Pass your store's `plans` to show their local prices.
787
+ */
788
+ openPaywall(placement: string, options?: PaywallOptions): Promise<PaywallResult>;
789
+ /** The open paywall for a renderer (the provider's, or your own). */
790
+ paywall: PaywallHost;
671
791
  /**
672
792
  * Push notifications from your Lifecycle journeys for the signed-in person (call `identify` first). Asks the
673
793
  * permission (unless `ask: false`), gets the Expo push token and sends it to MRRocket. Needs `expo-notifications`.
@@ -720,8 +840,9 @@ interface NativeAppearance {
720
840
  theme?: "light" | "dark" | "auto";
721
841
  accent?: string;
722
842
  }
723
- declare const MRRocketFlows: ({ flows, appearance }: {
843
+ declare const MRRocketFlows: ({ flows, paywall, appearance }: {
724
844
  flows: FlowsController;
845
+ paywall?: PaywallHost;
725
846
  appearance?: NativeAppearance;
726
847
  }) => react.FunctionComponentElement<Record<string, unknown>> | null;
727
848
 
@@ -891,6 +1012,16 @@ declare const usePush: () => {
891
1012
  register: MRRocketClient["registerForPush"];
892
1013
  unregister: MRRocketClient["unregisterForPush"];
893
1014
  };
1015
+ /**
1016
+ * The paywalls built in MRRocket › Paywalls, page by page; the provider draws them. The last page hands the chosen plan
1017
+ * to `onPurchase`, where your code buys it. MRRocket never takes a payment.
1018
+ *
1019
+ * const paywall = usePaywall();
1020
+ * paywall.open("onboarding_end", { plans: storePlans, onPurchase: (plan) => Purchases.purchasePackage(packages[plan.id]) });
1021
+ */
1022
+ declare const usePaywall: () => {
1023
+ open: MRRocketClient["openPaywall"];
1024
+ };
894
1025
  /** Starts an onboarding flow now (the first live one without an id), whatever its trigger. */
895
1026
  declare const useOnboarding: () => {
896
1027
  start: (id?: string) => boolean;
@@ -929,4 +1060,4 @@ declare const useExpoRouterTracking: () => void;
929
1060
  */
930
1061
  declare const useNavigationTracking: () => (state: unknown) => void;
931
1062
 
932
- export { type AppContext, type CancelFlowConfig, type CancelFlowOptions, type CancelFlowResult, type CancelOffer, type ChatMessage, type ChatThread, type CockpitConfig, type FeedbackPost, type FlagConfig, type FlowView, type InAppMessage, type KeyValueStorage, type MRRocketClient, MRRocketFeedback, MRRocketFlows, MRRocketMask, MRRocketMessenger, type MRRocketOptions, MRRocketProvider, type MRRocketProviderProps, MRRocketSurveys, type NativeAppearance, type NativeConsent, type NativeFeedbackOptions, type NativeMessengerOptions, type NativePushResult, type NativePushStatus, type OnboardingFlow, type OnboardingStep, type RegisterForPushOptions, SDK_VERSION, type SurveyDefinition, type VariantId, activeRouteName, createMRRocket, getMRRocket, useCancelFlow, useExpoRouterTracking, useFeedback, useFlag, useFlagVariant, useMRRocket, useMessenger, useNavigationTracking, useOnboarding, usePush, useText, useVariant };
1063
+ export { type AppContext, type CancelFlowConfig, type CancelFlowOptions, type CancelFlowResult, type CancelOffer, type ChatMessage, type ChatThread, type CockpitConfig, type FeedbackPost, type FlagConfig, type FlowView, type InAppMessage, type KeyValueStorage, type MRRocketClient, MRRocketFeedback, MRRocketFlows, MRRocketMask, MRRocketMessenger, type MRRocketOptions, MRRocketProvider, type MRRocketProviderProps, MRRocketSurveys, type NativeAppearance, type NativeConsent, type NativeFeedbackOptions, type NativeMessengerOptions, type NativePushResult, type NativePushStatus, type OnboardingFlow, type OnboardingStep, type PaywallConfig, type PaywallOptions, type PaywallPage, type PaywallPlan, type PaywallResult, type RegisterForPushOptions, SDK_VERSION, type SurveyDefinition, type VariantId, activeRouteName, createMRRocket, getMRRocket, useCancelFlow, useExpoRouterTracking, useFeedback, useFlag, useFlagVariant, useMRRocket, useMessenger, useNavigationTracking, useOnboarding, usePaywall, usePush, useText, useVariant };
@@ -414,6 +414,116 @@ interface FlowsController {
414
414
  };
415
415
  }
416
416
 
417
+ /**
418
+ * Paywalls with several pages (SDK 0.14.0): what MRRocket's config serves under `paywalls` (one per placement, built
419
+ * in MRRocket › Conversion › Paywalls) and a small controller that walks the person through its pages, page after page,
420
+ * until the last one hands the chosen plan to the app. No DOM and no React: the web renderer (`mountPaywall` in
421
+ * flows-ui.ts, loaded only when a paywall opens) and the React Native one (native/flows-view.ts) draw `view()` and
422
+ * call the actions.
423
+ *
424
+ * MRRocket never takes a payment. The last page's button calls the app's `onPurchase(plan)`, which buys through the
425
+ * app's own store code (StoreKit or Play through RevenueCat or Adapty, or Stripe Checkout on the web); only when it
426
+ * resolves is the purchase recorded. It throwing keeps the paywall open with an error, resolving `false` (the person
427
+ * closed the store sheet) keeps it open without one.
428
+ *
429
+ * A running test on the paywall (`experiment`) gives each person their variant's pages, by the same stable hash as
430
+ * every other test (`assign`), and records `$exposure` when the paywall opens. What it records, as normal events under
431
+ * the SDK's analytics consent:
432
+ * - `$paywall` `{paywall, placement, variant, page, index, action: "page"}` the first time each page shows in a run,
433
+ * so MRRocket shows how many reach page 2 and 3;
434
+ * - `{action: "purchased", plan}` after `onPurchase` went through (a test's purchase goal counts it), and
435
+ * `{action: "closed", index}` when the person closed it.
436
+ */
437
+
438
+ /** One page of a paywall. Plans pages list the plans and buy; the others say why and go on. */
439
+ interface PaywallPage {
440
+ id: string;
441
+ /** "benefits": a list of what the person gets; "proof": quotes of happy customers; "plans": the plans and the buy button. */
442
+ kind: "benefits" | "proof" | "plans";
443
+ title: string;
444
+ body?: string;
445
+ /** Benefits: one line each. */
446
+ items?: string[];
447
+ /** Proof: what customers said, and who. */
448
+ quotes?: {
449
+ text: string;
450
+ by?: string;
451
+ }[];
452
+ /** The button: "Continue" on the way, the buy button on the plans page ("Start free trial"). */
453
+ button?: string;
454
+ }
455
+ /** A plan the paywall offers. `id` is what the app buys: the store product id or the Stripe price, as set in MRRocket. */
456
+ interface PaywallPlan {
457
+ id: string;
458
+ name: string;
459
+ /** As shown ("€59.99 / year"). The app's own store prices win when it passes `plans`. */
460
+ price: string;
461
+ /** A line under it ("€5 a month", "7 days free"). */
462
+ note?: string;
463
+ }
464
+ /** A published paywall as the config serves it. */
465
+ interface PaywallConfig {
466
+ id: string;
467
+ /** The placement's key (`onboarding_end`), what the app opens: `paywall.open("onboarding_end")`. */
468
+ placement: string;
469
+ /** The running test on it, a key of `experiments`; each of its variants has pages under `variants`. */
470
+ experiment?: string;
471
+ /** Control (the live paywall) first. */
472
+ variants: {
473
+ id: VariantId;
474
+ pages: PaywallPage[];
475
+ }[];
476
+ plans: PaywallPlan[];
477
+ /** The plan picked before the person picks one. */
478
+ highlight?: string;
479
+ }
480
+ interface PaywallResult {
481
+ /**
482
+ * "purchased": `onPurchase` went through; "chosen": the person picked a plan and there is no `onPurchase`, so the app
483
+ * buys it now (the store's purchase still counts for a test through RevenueCat or Adapty); "closed": the person
484
+ * closed it (or there was no paywall).
485
+ */
486
+ outcome: "purchased" | "chosen" | "closed";
487
+ plan?: PaywallPlan;
488
+ paywall?: string;
489
+ variant?: VariantId;
490
+ /** The page the person was on, from 0. */
491
+ index: number;
492
+ }
493
+ interface PaywallOptions {
494
+ /**
495
+ * Buy the plan in your code (RevenueCat `purchasePackage`, Adapty `makePurchase`, Stripe Checkout). Throwing keeps
496
+ * the paywall open with an error; `false` keeps it open without one (the person cancelled the store sheet).
497
+ */
498
+ onPurchase?: (plan: PaywallPlan) => unknown;
499
+ /** Your store's plans with their local prices, by the same ids; they replace what MRRocket has for them. */
500
+ plans?: PaywallPlan[];
501
+ onClose?: (result: PaywallResult) => void;
502
+ }
503
+ interface PaywallView {
504
+ paywall: PaywallConfig;
505
+ variant: VariantId;
506
+ page: PaywallPage;
507
+ index: number;
508
+ total: number;
509
+ last: boolean;
510
+ plans: PaywallPlan[];
511
+ /** The picked plan's id. */
512
+ plan?: string;
513
+ busy: boolean;
514
+ error?: string;
515
+ }
516
+ interface PaywallHost {
517
+ /** Opens the paywall of a placement; resolves when it is bought or closed (at once with "closed" when there is none). */
518
+ open(placement: string, options?: PaywallOptions): Promise<PaywallResult>;
519
+ view(): PaywallView | null;
520
+ subscribe(listener: () => void): () => void;
521
+ next(): void;
522
+ back(): void;
523
+ pick(planId: string): void;
524
+ close(): void;
525
+ }
526
+
417
527
  /**
418
528
  * The part of the SDK that runs anywhere: experiment config, variant assignment, preview links and event shapes.
419
529
  * No React, no browser APIs, so the server helper, the proxy helper and the client share it.
@@ -421,7 +531,7 @@ interface FlowsController {
421
531
 
422
532
  type VariantId = string;
423
533
  /** This build of the SDK. Sent with the config request so MRRocket knows what the site runs (error tracking, flags). */
424
- declare const SDK_VERSION = "0.13.0";
534
+ declare const SDK_VERSION = "0.14.0";
425
535
  /** One variant of an experiment. `changes` replaces texts that the page marks with `<Text id>`. */
426
536
  interface VariantConfig {
427
537
  id: VariantId;
@@ -478,6 +588,8 @@ interface CockpitConfig {
478
588
  messages?: boolean;
479
589
  /** Page redirects set in MRRocket; the SDK sends visitors on before the page shows (`redirectFor`). SDK 0.7.0+. */
480
590
  redirects?: RedirectConfig[];
591
+ /** Published paywalls with their pages, per placement (MRRocket › Conversion › Paywalls), for `paywall.open()`. SDK 0.14.0+. */
592
+ paywalls?: PaywallConfig[];
481
593
  }
482
594
  /** One condition of a flag's audience, on a property of the person (`identify(id, props)`), the id or the page. */
483
595
  interface FlagCondition {
@@ -668,6 +780,14 @@ interface MRRocketClient {
668
780
  openCancelFlow(options?: CancelFlowOptions): Promise<CancelFlowResult>;
669
781
  /** Starts an onboarding flow now (the first live one without an id). False when there is none. */
670
782
  startOnboarding(id?: string): boolean;
783
+ /**
784
+ * Shows the paywall of a placement built in MRRocket › Paywalls page by page (drawn by `<MRRocketProvider>`). Its
785
+ * last page hands the chosen plan to `onPurchase`, where your code buys it (RevenueCat, Adapty, StoreKit, Play);
786
+ * MRRocket never takes a payment. Pass your store's `plans` to show their local prices.
787
+ */
788
+ openPaywall(placement: string, options?: PaywallOptions): Promise<PaywallResult>;
789
+ /** The open paywall for a renderer (the provider's, or your own). */
790
+ paywall: PaywallHost;
671
791
  /**
672
792
  * Push notifications from your Lifecycle journeys for the signed-in person (call `identify` first). Asks the
673
793
  * permission (unless `ask: false`), gets the Expo push token and sends it to MRRocket. Needs `expo-notifications`.
@@ -720,8 +840,9 @@ interface NativeAppearance {
720
840
  theme?: "light" | "dark" | "auto";
721
841
  accent?: string;
722
842
  }
723
- declare const MRRocketFlows: ({ flows, appearance }: {
843
+ declare const MRRocketFlows: ({ flows, paywall, appearance }: {
724
844
  flows: FlowsController;
845
+ paywall?: PaywallHost;
725
846
  appearance?: NativeAppearance;
726
847
  }) => react.FunctionComponentElement<Record<string, unknown>> | null;
727
848
 
@@ -891,6 +1012,16 @@ declare const usePush: () => {
891
1012
  register: MRRocketClient["registerForPush"];
892
1013
  unregister: MRRocketClient["unregisterForPush"];
893
1014
  };
1015
+ /**
1016
+ * The paywalls built in MRRocket › Paywalls, page by page; the provider draws them. The last page hands the chosen plan
1017
+ * to `onPurchase`, where your code buys it. MRRocket never takes a payment.
1018
+ *
1019
+ * const paywall = usePaywall();
1020
+ * paywall.open("onboarding_end", { plans: storePlans, onPurchase: (plan) => Purchases.purchasePackage(packages[plan.id]) });
1021
+ */
1022
+ declare const usePaywall: () => {
1023
+ open: MRRocketClient["openPaywall"];
1024
+ };
894
1025
  /** Starts an onboarding flow now (the first live one without an id), whatever its trigger. */
895
1026
  declare const useOnboarding: () => {
896
1027
  start: (id?: string) => boolean;
@@ -929,4 +1060,4 @@ declare const useExpoRouterTracking: () => void;
929
1060
  */
930
1061
  declare const useNavigationTracking: () => (state: unknown) => void;
931
1062
 
932
- export { type AppContext, type CancelFlowConfig, type CancelFlowOptions, type CancelFlowResult, type CancelOffer, type ChatMessage, type ChatThread, type CockpitConfig, type FeedbackPost, type FlagConfig, type FlowView, type InAppMessage, type KeyValueStorage, type MRRocketClient, MRRocketFeedback, MRRocketFlows, MRRocketMask, MRRocketMessenger, type MRRocketOptions, MRRocketProvider, type MRRocketProviderProps, MRRocketSurveys, type NativeAppearance, type NativeConsent, type NativeFeedbackOptions, type NativeMessengerOptions, type NativePushResult, type NativePushStatus, type OnboardingFlow, type OnboardingStep, type RegisterForPushOptions, SDK_VERSION, type SurveyDefinition, type VariantId, activeRouteName, createMRRocket, getMRRocket, useCancelFlow, useExpoRouterTracking, useFeedback, useFlag, useFlagVariant, useMRRocket, useMessenger, useNavigationTracking, useOnboarding, usePush, useText, useVariant };
1063
+ export { type AppContext, type CancelFlowConfig, type CancelFlowOptions, type CancelFlowResult, type CancelOffer, type ChatMessage, type ChatThread, type CockpitConfig, type FeedbackPost, type FlagConfig, type FlowView, type InAppMessage, type KeyValueStorage, type MRRocketClient, MRRocketFeedback, MRRocketFlows, MRRocketMask, MRRocketMessenger, type MRRocketOptions, MRRocketProvider, type MRRocketProviderProps, MRRocketSurveys, type NativeAppearance, type NativeConsent, type NativeFeedbackOptions, type NativeMessengerOptions, type NativePushResult, type NativePushStatus, type OnboardingFlow, type OnboardingStep, type PaywallConfig, type PaywallOptions, type PaywallPage, type PaywallPlan, type PaywallResult, type RegisterForPushOptions, SDK_VERSION, type SurveyDefinition, type VariantId, activeRouteName, createMRRocket, getMRRocket, useCancelFlow, useExpoRouterTracking, useFeedback, useFlag, useFlagVariant, useMRRocket, useMessenger, useNavigationTracking, useOnboarding, usePaywall, usePush, useText, useVariant };
@@ -41,6 +41,7 @@ __export(native_exports, {
41
41
  useMessenger: () => useMessenger,
42
42
  useNavigationTracking: () => useNavigationTracking,
43
43
  useOnboarding: () => useOnboarding,
44
+ usePaywall: () => usePaywall,
44
45
  usePush: () => usePush,
45
46
  useText: () => useText,
46
47
  useVariant: () => useVariant
@@ -49,7 +50,7 @@ module.exports = __toCommonJS(native_exports);
49
50
  var import_react3 = require("react");
50
51
 
51
52
  // src/core.ts
52
- var SDK_VERSION = "0.13.0";
53
+ var SDK_VERSION = "0.14.0";
53
54
  var propOf = (person, name) => {
54
55
  const want = name.trim().toLowerCase();
55
56
  for (const [k, v] of Object.entries(person)) if (k.toLowerCase() === want) return v;
@@ -805,6 +806,131 @@ var createFlows = (env) => {
805
806
  return controller;
806
807
  };
807
808
 
809
+ // src/paywall.ts
810
+ var PAYWALL_EVENT = "$paywall";
811
+ var paywallPages = (config, w, distinctId) => {
812
+ const e = w.experiment ? config?.experiments.find((x) => x.key === w.experiment) : void 0;
813
+ const id = e ? assign(e, distinctId) : w.variants[0]?.id;
814
+ const v = w.variants.find((x) => x.id === id) ?? w.variants[0];
815
+ return v ? { variant: v.id, pages: v.pages, test: e?.status === "running" && !e.winner ? e.key : void 0 } : null;
816
+ };
817
+ var createPaywallHost = (env) => {
818
+ let run = null;
819
+ const listeners = /* @__PURE__ */ new Set();
820
+ const emit = () => {
821
+ for (const l of [...listeners]) {
822
+ try {
823
+ l();
824
+ } catch {
825
+ }
826
+ }
827
+ };
828
+ const base = () => run ? { paywall: run.w.id, placement: run.w.placement, variant: run.variant } : {};
829
+ const show = () => {
830
+ if (run && !run.seen.has(run.index)) {
831
+ run.seen.add(run.index);
832
+ env.track(PAYWALL_EVENT, { ...base(), page: run.pages[run.index].id, index: run.index, action: "page" });
833
+ }
834
+ emit();
835
+ };
836
+ const end = (r) => {
837
+ const c = run;
838
+ if (!c) return;
839
+ run = null;
840
+ if (r.outcome === "closed") c.options.onClose?.(r);
841
+ c.resolve(r);
842
+ emit();
843
+ };
844
+ const result = (outcome) => ({
845
+ outcome,
846
+ index: run?.index ?? 0,
847
+ ...run ? { paywall: run.w.id, variant: run.variant } : {},
848
+ ...outcome !== "closed" && run ? { plan: run.plans.find((p) => p.id === run.plan) } : {}
849
+ });
850
+ const buy = () => {
851
+ const c = run;
852
+ const plan = c?.plans.find((p) => p.id === c.plan);
853
+ if (!c || !plan || c.busy) return;
854
+ if (!c.options.onPurchase) return end(result("chosen"));
855
+ c.busy = true;
856
+ c.error = void 0;
857
+ emit();
858
+ Promise.resolve().then(() => c.options.onPurchase?.(plan)).then(
859
+ (ok) => {
860
+ c.busy = false;
861
+ if (run !== c) return;
862
+ if (ok === false) return emit();
863
+ env.track(PAYWALL_EVENT, { ...base(), page: c.pages[c.index].id, index: c.index, plan: plan.id, action: "purchased" });
864
+ end(result("purchased"));
865
+ },
866
+ () => {
867
+ c.busy = false;
868
+ c.error = "The purchase did not go through. Please try again.";
869
+ emit();
870
+ }
871
+ );
872
+ };
873
+ return {
874
+ open: (placement, options = {}) => {
875
+ if (run) return run.promise;
876
+ const config = env.config();
877
+ const w = config?.paywalls?.find((x) => x.placement === placement);
878
+ const picked = w ? paywallPages(config, w, env.distinctId()) : null;
879
+ if (!w || !picked?.pages.length) return Promise.resolve({ outcome: "closed", index: 0 });
880
+ const own = new Map((options.plans ?? []).map((p) => [p.id, p]));
881
+ const plans = w.plans.map((p) => own.get(p.id) ?? p);
882
+ let resolve = () => {
883
+ };
884
+ const promise = new Promise((r) => resolve = r);
885
+ const plan = plans.some((p) => p.id === w.highlight) ? w.highlight : plans[0]?.id;
886
+ run = { w, variant: picked.variant, pages: picked.pages, plans, index: 0, plan, busy: false, seen: /* @__PURE__ */ new Set(), options, resolve, promise };
887
+ if (picked.test) env.track("$exposure", { experiment: picked.test, variant: picked.variant });
888
+ show();
889
+ return promise;
890
+ },
891
+ view: () => run ? {
892
+ paywall: run.w,
893
+ variant: run.variant,
894
+ page: run.pages[run.index],
895
+ index: run.index,
896
+ total: run.pages.length,
897
+ last: run.index === run.pages.length - 1,
898
+ plans: run.plans,
899
+ plan: run.plan,
900
+ busy: run.busy,
901
+ error: run.error
902
+ } : null,
903
+ subscribe: (l) => {
904
+ listeners.add(l);
905
+ return () => listeners.delete(l);
906
+ },
907
+ next: () => {
908
+ if (!run || run.busy) return;
909
+ if (run.pages[run.index].kind === "plans" || run.index === run.pages.length - 1) return buy();
910
+ run.index++;
911
+ run.error = void 0;
912
+ show();
913
+ },
914
+ back: () => {
915
+ if (!run || run.busy || !run.index) return;
916
+ run.index--;
917
+ run.error = void 0;
918
+ emit();
919
+ },
920
+ pick: (id) => {
921
+ if (!run || run.busy || !run.plans.some((p) => p.id === id)) return;
922
+ run.plan = id;
923
+ run.error = void 0;
924
+ emit();
925
+ },
926
+ close: () => {
927
+ if (!run || run.busy) return;
928
+ env.track(PAYWALL_EVENT, { ...base(), index: run.index, action: "closed" });
929
+ end(result("closed"));
930
+ }
931
+ };
932
+ };
933
+
808
934
  // src/rage.ts
809
935
  var RageDetector = class {
810
936
  constructor(windowMs = 1e3, radius = 30, count = 3) {
@@ -1072,6 +1198,7 @@ var createClient = (options, deps) => {
1072
1198
  const errorQueue = [];
1073
1199
  const errorsSeen = /* @__PURE__ */ new Set();
1074
1200
  let errorsSent = 0;
1201
+ const paywall = createPaywallHost({ config: () => config, distinctId: () => distinctId(), track: (event, props) => record(event, props) });
1075
1202
  const flows = createFlows({
1076
1203
  track: (event, props) => record(event, props),
1077
1204
  personId: () => consent !== "off" ? identified : void 0,
@@ -1632,6 +1759,8 @@ var createClient = (options, deps) => {
1632
1759
  },
1633
1760
  openCancelFlow: (o) => flows.openCancelFlow(o),
1634
1761
  startOnboarding: (id) => flows.startOnboarding(id),
1762
+ openPaywall: (placement, o) => paywall.open(placement, o),
1763
+ paywall,
1635
1764
  registerForPush,
1636
1765
  unregisterForPush,
1637
1766
  flows,
@@ -1697,19 +1826,21 @@ var RN = require("react-native");
1697
1826
  var LIGHT = { bg: "#ffffff", fg: "#1c1d20", muted: "#5d6068", soft: "#f4f5f7", line: "#e4e5e8" };
1698
1827
  var DARK = { bg: "#17181b", fg: "#f1f2f4", muted: "#a9acb3", soft: "#232428", line: "#2e2f35" };
1699
1828
  var months = (n) => `${n} ${n === 1 ? "month" : "months"}`;
1700
- var MRRocketFlows = ({ flows, appearance = {} }) => {
1829
+ var MRRocketFlows = ({ flows, paywall, appearance = {} }) => {
1701
1830
  const scheme = RN.useColorScheme?.();
1702
1831
  const dark = appearance.theme === "dark" || (appearance.theme ?? "auto") === "auto" && scheme === "dark";
1703
1832
  const p = { ...dark ? DARK : LIGHT, accent: appearance.accent ?? "#3f76ff", danger: "#d92d20" };
1704
1833
  const [, setTick] = (0, import_react.useState)(0);
1705
1834
  (0, import_react.useEffect)(() => flows.subscribe(() => setTick((t) => t + 1)), [flows]);
1835
+ (0, import_react.useEffect)(() => paywall?.subscribe(() => setTick((t) => t + 1)), [paywall]);
1836
+ const pw = paywall?.view() ?? null;
1706
1837
  const v = flows.view();
1707
1838
  const [comment, setComment] = (0, import_react.useState)("");
1708
1839
  const screen = v?.type === "cancel" ? v.screen : null;
1709
1840
  (0, import_react.useEffect)(() => {
1710
1841
  if (!screen) setComment("");
1711
1842
  }, [screen]);
1712
- if (!v) return null;
1843
+ if (!v && !pw) return null;
1713
1844
  const text2 = (children, style = {}) => (0, import_react.createElement)(RN.Text, { style: { color: p.fg, fontSize: 15, ...style } }, children);
1714
1845
  const muted = (children, style = {}) => text2(children, { color: p.muted, fontSize: 13, ...style });
1715
1846
  const title = (children) => (0, import_react.createElement)(RN.Text, { accessibilityRole: "header", style: { color: p.fg, fontSize: 18, fontWeight: "700" } }, children);
@@ -1778,6 +1909,71 @@ var MRRocketFlows = ({ flows, appearance = {} }) => {
1778
1909
  accessibilityLabel: placeholder,
1779
1910
  style: { minHeight: 72, borderWidth: 1, borderColor: p.line, borderRadius: 10, padding: 10, color: p.fg, textAlignVertical: "top" }
1780
1911
  });
1912
+ const paywallSheet = (w) => {
1913
+ const pg = w.page;
1914
+ const host = paywall;
1915
+ return sheet(
1916
+ [
1917
+ (0, import_react.createElement)(
1918
+ RN.View,
1919
+ { key: "top", style: { flexDirection: "row", alignItems: "center", gap: 4 } },
1920
+ ...w.total > 1 ? Array.from(
1921
+ { length: w.total },
1922
+ (_, i) => (0, import_react.createElement)(RN.View, { key: i, style: { width: 16, height: 4, borderRadius: 4, backgroundColor: i <= w.index ? p.accent : p.line } })
1923
+ ) : [],
1924
+ close("Close", host.close)
1925
+ ),
1926
+ title(pg.title),
1927
+ pg.body ? muted(pg.body, { fontSize: 15 }) : null,
1928
+ ...pg.kind === "benefits" ? (pg.items ?? []).map(
1929
+ (item, i) => (0, import_react.createElement)(
1930
+ RN.View,
1931
+ { key: `b${i}`, style: { flexDirection: "row", gap: 10, alignItems: "flex-start" } },
1932
+ text2("\u2713", { color: p.accent, fontWeight: "700" }),
1933
+ text2(item, { flex: 1 })
1934
+ )
1935
+ ) : [],
1936
+ ...pg.kind === "proof" ? (pg.quotes ?? []).map(
1937
+ (q, i) => (0, import_react.createElement)(RN.View, { key: `q${i}`, style: { backgroundColor: p.soft, borderRadius: 12, padding: 12, gap: 6 } }, text2(q.text), q.by ? muted(q.by) : null)
1938
+ ) : [],
1939
+ pg.kind === "plans" ? (0, import_react.createElement)(
1940
+ RN.View,
1941
+ { key: "plans", accessibilityRole: "radiogroup", style: { gap: 6 } },
1942
+ ...w.plans.map(
1943
+ (plan) => (0, import_react.createElement)(
1944
+ RN.Pressable,
1945
+ {
1946
+ key: plan.id,
1947
+ onPress: () => host.pick(plan.id),
1948
+ accessibilityRole: "radio",
1949
+ accessibilityLabel: `${plan.name}, ${plan.price}`,
1950
+ accessibilityState: { checked: w.plan === plan.id },
1951
+ style: {
1952
+ minHeight: 52,
1953
+ paddingHorizontal: 12,
1954
+ borderRadius: 10,
1955
+ borderWidth: 1,
1956
+ borderColor: w.plan === plan.id ? p.accent : p.line,
1957
+ backgroundColor: w.plan === plan.id ? p.soft : p.bg,
1958
+ flexDirection: "row",
1959
+ alignItems: "center",
1960
+ gap: 10
1961
+ }
1962
+ },
1963
+ (0, import_react.createElement)(RN.View, { style: { flex: 1 } }, text2(plan.name, { fontWeight: "600" }), plan.note ? muted(plan.note) : null),
1964
+ text2(plan.price, { fontWeight: "700" })
1965
+ )
1966
+ )
1967
+ ) : null,
1968
+ w.error ? text2(w.error, { color: p.danger, fontSize: 13 }) : null,
1969
+ btn(w.busy ? "One moment\u2026" : pg.button || "Continue", "primary", host.next, w.busy || pg.kind === "plans" && !w.plan),
1970
+ w.index ? btn("Back", "quiet", host.back, w.busy) : null
1971
+ ],
1972
+ host.close
1973
+ );
1974
+ };
1975
+ if (pw) return paywallSheet(pw);
1976
+ if (!v) return null;
1781
1977
  if (v.type === "cancel") {
1782
1978
  const top = (0, import_react.createElement)(
1783
1979
  RN.View,
@@ -3067,7 +3263,7 @@ var MRRocketProvider = ({ client: given, children, consent, flows = true, appear
3067
3263
  Ctx.Provider,
3068
3264
  { value: client },
3069
3265
  heatmaps ? (0, import_react3.createElement)(RN4.View, { style: { flex: 1 }, onTouchStartCapture: (e) => touch(e, false), onTouchEndCapture: (e) => touch(e, true) }, children) : children,
3070
- flows ? (0, import_react3.createElement)(MRRocketFlows, { flows: client.flows, appearance }) : null,
3266
+ flows ? (0, import_react3.createElement)(MRRocketFlows, { flows: client.flows, paywall: client.paywall, appearance }) : null,
3071
3267
  surveys ? (0, import_react3.createElement)(MRRocketSurveys, { client, appearance }) : null,
3072
3268
  feedback ? (0, import_react3.createElement)(MRRocketFeedback, { client, appearance, options: feedback }) : null,
3073
3269
  messenger ? (0, import_react3.createElement)(MRRocketMessenger, { client, appearance, options: messenger }) : null
@@ -3104,6 +3300,10 @@ var usePush = () => {
3104
3300
  const client = useClient();
3105
3301
  return { register: client.registerForPush, unregister: client.unregisterForPush };
3106
3302
  };
3303
+ var usePaywall = () => {
3304
+ const client = useClient();
3305
+ return { open: client.openPaywall };
3306
+ };
3107
3307
  var useOnboarding = () => {
3108
3308
  const client = useClient();
3109
3309
  return { start: client.startOnboarding };