@neko-os/rc-subscription 0.1.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 (82) hide show
  1. package/README.md +142 -0
  2. package/dist/config.js +5 -0
  3. package/dist/containers/SubscriptionHandler.js +75 -0
  4. package/dist/containers/SubscriptionRequired.js +11 -0
  5. package/dist/containers/SubscriptionRequiredCTA.js +37 -0
  6. package/dist/containers/paywall/Paywall.js +130 -0
  7. package/dist/containers/paywall/PaywallFeatures.js +42 -0
  8. package/dist/containers/paywall/PaywallFooter.js +16 -0
  9. package/dist/containers/paywall/PaywallHero.js +33 -0
  10. package/dist/containers/paywall/PaywallPlanCard.js +71 -0
  11. package/dist/containers/paywall/PaywallReturnIcon.js +8 -0
  12. package/dist/containers/paywall/_request/usePaywallActions.js +39 -0
  13. package/dist/index.js +6 -0
  14. package/dist/locales/cs.js +30 -0
  15. package/dist/locales/da.js +30 -0
  16. package/dist/locales/de.js +30 -0
  17. package/dist/locales/el.js +30 -0
  18. package/dist/locales/en.js +30 -0
  19. package/dist/locales/es.js +30 -0
  20. package/dist/locales/fi.js +30 -0
  21. package/dist/locales/fr.js +30 -0
  22. package/dist/locales/hi.js +30 -0
  23. package/dist/locales/hu.js +30 -0
  24. package/dist/locales/id.js +30 -0
  25. package/dist/locales/index.js +63 -0
  26. package/dist/locales/it.js +30 -0
  27. package/dist/locales/ja.js +30 -0
  28. package/dist/locales/ko.js +30 -0
  29. package/dist/locales/nl.js +30 -0
  30. package/dist/locales/no.js +30 -0
  31. package/dist/locales/pl.js +30 -0
  32. package/dist/locales/pt.js +30 -0
  33. package/dist/locales/ro.js +30 -0
  34. package/dist/locales/ru.js +30 -0
  35. package/dist/locales/sv.js +30 -0
  36. package/dist/locales/th.js +30 -0
  37. package/dist/locales/tr.js +30 -0
  38. package/dist/locales/uk.js +30 -0
  39. package/dist/locales/vi.js +30 -0
  40. package/dist/locales/zh.js +30 -0
  41. package/dist/views/active/ActiveSubscriptionView.js +51 -0
  42. package/package.json +53 -0
  43. package/src/config.js +5 -0
  44. package/src/containers/SubscriptionHandler.js +75 -0
  45. package/src/containers/SubscriptionRequired.js +11 -0
  46. package/src/containers/SubscriptionRequiredCTA.js +37 -0
  47. package/src/containers/paywall/Paywall.js +130 -0
  48. package/src/containers/paywall/PaywallFeatures.js +42 -0
  49. package/src/containers/paywall/PaywallFooter.js +16 -0
  50. package/src/containers/paywall/PaywallHero.js +33 -0
  51. package/src/containers/paywall/PaywallPlanCard.js +71 -0
  52. package/src/containers/paywall/PaywallReturnIcon.js +8 -0
  53. package/src/containers/paywall/_request/usePaywallActions.js +39 -0
  54. package/src/index.js +6 -0
  55. package/src/locales/cs.js +30 -0
  56. package/src/locales/da.js +30 -0
  57. package/src/locales/de.js +30 -0
  58. package/src/locales/el.js +30 -0
  59. package/src/locales/en.js +30 -0
  60. package/src/locales/es.js +30 -0
  61. package/src/locales/fi.js +30 -0
  62. package/src/locales/fr.js +30 -0
  63. package/src/locales/hi.js +30 -0
  64. package/src/locales/hu.js +30 -0
  65. package/src/locales/id.js +30 -0
  66. package/src/locales/index.js +63 -0
  67. package/src/locales/it.js +30 -0
  68. package/src/locales/ja.js +30 -0
  69. package/src/locales/ko.js +30 -0
  70. package/src/locales/nl.js +30 -0
  71. package/src/locales/no.js +30 -0
  72. package/src/locales/pl.js +30 -0
  73. package/src/locales/pt.js +30 -0
  74. package/src/locales/ro.js +30 -0
  75. package/src/locales/ru.js +30 -0
  76. package/src/locales/sv.js +30 -0
  77. package/src/locales/th.js +30 -0
  78. package/src/locales/tr.js +30 -0
  79. package/src/locales/uk.js +30 -0
  80. package/src/locales/vi.js +30 -0
  81. package/src/locales/zh.js +30 -0
  82. package/src/views/active/ActiveSubscriptionView.js +51 -0
package/README.md ADDED
@@ -0,0 +1,142 @@
1
+ # neko-rc-subscription
2
+
3
+ Drop-in RevenueCat subscription module for React Native (Expo) apps built with `@neko-os/ui`. Provides a full paywall, subscription gating, and subscription management — with i18n support for 26 languages.
4
+
5
+ ## Peer Dependencies
6
+
7
+ - `react-native-purchases` (RevenueCat SDK)
8
+ - `@neko-os/ui`
9
+ - `@react-navigation/native`
10
+ - `react-native-reanimated`
11
+ - `react-native-safe-area-context`
12
+ - `i18next`
13
+ - `dayjs`
14
+
15
+ ## Configuration
16
+
17
+ Environment variables (or defaults):
18
+
19
+ | Variable | Default | Description |
20
+ |----------|---------|-------------|
21
+ | `EXPO_PUBLIC_RC_API_KEY_IOS` | — | RevenueCat API key for iOS |
22
+ | `EXPO_PUBLIC_RC_API_KEY_ANDROID` | — | RevenueCat API key for Android |
23
+ | `EXPO_PUBLIC_RC_ENTITLEMENT_ID` | `premium` | RevenueCat entitlement identifier |
24
+
25
+ Platform-specific API key selected automatically via `Platform.OS`.
26
+
27
+ ## Setup
28
+
29
+ ### 1. Register locales
30
+
31
+ Call once at app boot, after i18n is initialized:
32
+
33
+ ```jsx
34
+ import { registerSubscriptionLocales } from 'neko-rc-subscription'
35
+
36
+ registerSubscriptionLocales(i18n)
37
+ ```
38
+
39
+ ### 2. Wrap your app
40
+
41
+ ```jsx
42
+ import { SubscriptionHandler } from 'neko-rc-subscription'
43
+
44
+ <SubscriptionHandler
45
+ paywallConfig={{
46
+ title: 'Unlock Full Access',
47
+ subtitle: 'Track your habits without limits',
48
+ image: require('./assets/paywall-hero.png'),
49
+ features: [
50
+ { label: 'paywall.unlimitedGoals', icon: 'flag-fill', free: false },
51
+ { label: 'paywall.analytics', icon: 'bar-chart-fill', free: false },
52
+ { label: 'paywall.basicTracking', icon: 'check-line', free: true },
53
+ ],
54
+ }}
55
+ >
56
+ <App />
57
+ </SubscriptionHandler>
58
+ ```
59
+
60
+ ### 3. Add routes
61
+
62
+ ```jsx
63
+ import { ActiveSubscriptionView } from 'neko-rc-subscription'
64
+
65
+ // Stack navigator
66
+ <Stack.Screen name="subscription/active" component={ActiveSubscriptionView} />
67
+ ```
68
+
69
+ ## Exports
70
+
71
+ ### Components
72
+
73
+ | Export | Description |
74
+ |--------|-------------|
75
+ | `SubscriptionHandler` | Context provider. Configures RevenueCat, fetches offerings and customer info, provides subscription state to the tree. |
76
+ | `SubscriptionRequired` | Gate component. Shows children if subscribed, otherwise renders the Paywall. |
77
+ | `SubscriptionRequiredCTA` | Soft gate. Renders children with a blurred overlay and "Unlock" button when not subscribed. |
78
+ | `Paywall` | Full paywall screen with hero image, feature comparison, plan selection, purchase and restore. |
79
+ | `ActiveSubscriptionView` | Subscription management screen showing plan details and expiry. |
80
+
81
+ ### Hooks
82
+
83
+ | Export | Description |
84
+ |--------|-------------|
85
+ | `useSubscription()` | Full subscription context: `{ isSubscribed, isLoading, customerInfo, offerings, paywallConfig, refresh }` |
86
+ | `useIsSubscribed()` | Shorthand boolean — `true` when active entitlement exists. |
87
+
88
+ ### Functions
89
+
90
+ | Export | Description |
91
+ |--------|-------------|
92
+ | `registerSubscriptionLocales(i18n)` | Registers the `subscription` namespace into an existing i18next instance for all 26 supported languages. |
93
+
94
+ ## SubscriptionHandler Props
95
+
96
+ | Prop | Type | Description |
97
+ |------|------|-------------|
98
+ | `paywallConfig` | `object` | Optional paywall customization (see below). |
99
+
100
+ ### paywallConfig
101
+
102
+ | Key | Type | Description |
103
+ |-----|------|-------------|
104
+ | `title` | `string` | Hero title. Falls back to `t('paywall.title')`. |
105
+ | `subtitle` | `string` | Hero subtitle. Falls back to `t('paywall.subtitle')`. |
106
+ | `image` | `ImageSource` | Hero image (parallax). Omit for text-only hero. |
107
+ | `features` | `Feature[]` | Feature comparison rows. Each: `{ label, icon?, free? }`. `label` is a i18n key. `free` marks whether available on the free plan. |
108
+
109
+ ## SubscriptionRequired Props
110
+
111
+ | Prop | Type | Default | Description |
112
+ |------|------|---------|-------------|
113
+ | `disabled` | `bool` | `false` | Bypass the gate (always show children). |
114
+ | `modal` | `bool` | — | Paywall rendered as modal (close icon instead of back arrow). |
115
+ | `showReturn` | `bool` | — | Show return icon on the paywall. |
116
+ | `footerPaddingB` | `number` | — | Override footer bottom padding. |
117
+
118
+ ## SubscriptionRequiredCTA Props
119
+
120
+ | Prop | Type | Default | Description |
121
+ |------|------|---------|-------------|
122
+ | `disabled` | `bool` | `false` | Bypass the gate. |
123
+ | `size` | `string` | `'xs'` | Button size. |
124
+ | `buttonProps` | `object` | — | Extra props forwarded to the unlock Button. |
125
+
126
+ ## Package Support
127
+
128
+ The paywall auto-renders available packages from the current RevenueCat offering. Supported package types:
129
+
130
+ - **Annual** — shows per-month price and save percentage vs monthly
131
+ - **Monthly** — base reference price
132
+ - **Lifetime** — one-time purchase
133
+
134
+ Free trial detection is automatic from `introPrice` metadata.
135
+
136
+ ## i18n
137
+
138
+ Namespace: `subscription`
139
+
140
+ 26 languages: cs, da, de, el, en, es, fi, fr, hi, hu, id, it, ja, ko, nl, no, pl, pt, ro, ru, sv, th, tr, uk, vi, zh.
141
+
142
+ Translation keys are organized under `settings`, `paywall`, `cta`, and `active` groups. See `locales/en.js` for the full key reference.
package/dist/config.js ADDED
@@ -0,0 +1,5 @@
1
+ import { Platform } from "react-native-web";
2
+
3
+ export var RC_API_KEY =
4
+ Platform.OS === 'ios' ? process.env.EXPO_PUBLIC_RC_API_KEY_IOS : process.env.EXPO_PUBLIC_RC_API_KEY_ANDROID;
5
+ export var RC_ENTITLEMENT_ID = process.env.EXPO_PUBLIC_RC_ENTITLEMENT_ID || 'premium';
@@ -0,0 +1,75 @@
1
+ var _jsxFileName = "/Users/christianstorch/Apps/nekoapps/libs/neko-rc-subscription/src/containers/SubscriptionHandler.js";function _slicedToArray(r, e) {return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();}function _nonIterableRest() {throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");}function _unsupportedIterableToArray(r, a) {if (r) {if ("string" == typeof r) return _arrayLikeToArray(r, a);var t = {}.toString.call(r).slice(8, -1);return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;}}function _arrayLikeToArray(r, a) {(null == a || a > r.length) && (a = r.length);for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];return n;}function _iterableToArrayLimit(r, l) {var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];if (null != t) {var e,n,i,u,a = [],f = !0,o = !1;try {if (i = (t = t.call(r)).next, 0 === l) {if (Object(t) !== t) return;f = !1;} else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0);} catch (r) {o = !0, n = r;} finally {try {if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;} finally {if (o) throw n;}}return a;}}function _arrayWithHoles(r) {if (Array.isArray(r)) return r;}function asyncGeneratorStep(n, t, e, r, o, a, c) {try {var i = n[a](c),u = i.value;} catch (n) {return void e(n);}i.done ? t(u) : Promise.resolve(u).then(r, o);}function _asyncToGenerator(n) {return function () {var t = this,e = arguments;return new Promise(function (r, o) {var a = n.apply(t, e);function _next(n) {asyncGeneratorStep(a, r, o, _next, _throw, "next", n);}function _throw(n) {asyncGeneratorStep(a, r, o, _next, _throw, "throw", n);}_next(void 0);});};}import React, { createContext, useContext, useEffect, useState } from 'react';
2
+ import Purchases from 'react-native-purchases';
3
+
4
+ import { RC_API_KEY, RC_ENTITLEMENT_ID } from "../config";import { jsx as _jsx } from "react/jsx-runtime";
5
+
6
+ var SubscriptionContext = createContext({
7
+ isSubscribed: false,
8
+ isLoading: true,
9
+ customerInfo: null,
10
+ offerings: null,
11
+ paywallConfig: null,
12
+ refresh: function () {var _refresh = _asyncToGenerator(function* () {});function refresh() {return _refresh.apply(this, arguments);}return refresh;}()
13
+ });
14
+
15
+ export function useSubscription() {
16
+ return useContext(SubscriptionContext);
17
+ }
18
+
19
+ export function useIsSubscribed() {
20
+ return useContext(SubscriptionContext).isSubscribed;
21
+ }
22
+
23
+ export default function SubscriptionHandler(_ref) {var children = _ref.children,paywallConfig = _ref.paywallConfig;
24
+ var _useState = useState(false),_useState2 = _slicedToArray(_useState, 2),isSubscribed = _useState2[0],setIsSubscribed = _useState2[1];
25
+ var _useState3 = useState(true),_useState4 = _slicedToArray(_useState3, 2),isLoading = _useState4[0],setIsLoading = _useState4[1];
26
+ var _useState5 = useState(null),_useState6 = _slicedToArray(_useState5, 2),customerInfo = _useState6[0],setCustomerInfo = _useState6[1];
27
+ var _useState7 = useState(null),_useState8 = _slicedToArray(_useState7, 2),offerings = _useState8[0],setOfferings = _useState8[1];function
28
+
29
+ refresh() {return _refresh2.apply(this, arguments);}function _refresh2() {_refresh2 = _asyncToGenerator(function* () {
30
+ try {var _info$entitlements2, _info$entitlements2$a;
31
+ var info = yield Purchases.getCustomerInfo();
32
+ setCustomerInfo(info);
33
+ setIsSubscribed(!!(info != null && (_info$entitlements2 = info.entitlements) != null && (_info$entitlements2$a = _info$entitlements2.active) != null && _info$entitlements2$a[RC_ENTITLEMENT_ID]));
34
+ } catch (_) {}
35
+ });return _refresh2.apply(this, arguments);}
36
+
37
+ useEffect(function () {
38
+ if (!RC_API_KEY) {
39
+ setIsLoading(false);
40
+ return;
41
+ }
42
+
43
+ function handleCustomerInfo(info) {var _info$entitlements, _info$entitlements$ac;
44
+ setCustomerInfo(info);
45
+ setIsSubscribed(!!(info != null && (_info$entitlements = info.entitlements) != null && (_info$entitlements$ac = _info$entitlements.active) != null && _info$entitlements$ac[RC_ENTITLEMENT_ID]));
46
+ }
47
+
48
+ Purchases.configure({ apiKey: RC_API_KEY });
49
+
50
+ _asyncToGenerator(function* () {
51
+ try {
52
+ var _yield$Promise$all = yield Promise.all([
53
+ Purchases.getCustomerInfo(),
54
+ Purchases.getOfferings()]
55
+ ),_yield$Promise$all2 = _slicedToArray(_yield$Promise$all, 2),info = _yield$Promise$all2[0],offeringsResult = _yield$Promise$all2[1];
56
+ handleCustomerInfo(info);
57
+ setOfferings(offeringsResult);
58
+ } catch (_) {
59
+ } finally {
60
+ setIsLoading(false);
61
+ }
62
+ })();
63
+
64
+ Purchases.addCustomerInfoUpdateListener(handleCustomerInfo);
65
+ return function () {return Purchases.removeCustomerInfoUpdateListener(handleCustomerInfo);};
66
+ }, []);
67
+
68
+ return (
69
+ _jsx(SubscriptionContext.Provider, {
70
+ value: { isSubscribed: isSubscribed, isLoading: isLoading, customerInfo: customerInfo, offerings: offerings, paywallConfig: paywallConfig, refresh: refresh }, children:
71
+
72
+ children }
73
+ ));
74
+
75
+ }
@@ -0,0 +1,11 @@
1
+ var _jsxFileName = "/Users/christianstorch/Apps/nekoapps/libs/neko-rc-subscription/src/containers/SubscriptionRequired.js";import { Loading } from '@neko-os/ui';
2
+
3
+ import Paywall from "./paywall/Paywall";
4
+ import { useSubscription } from "./SubscriptionHandler";import { jsx as _jsx } from "react/jsx-runtime";
5
+
6
+ export default function SubscriptionRequired(_ref) {var children = _ref.children,_ref$disabled = _ref.disabled,disabled = _ref$disabled === void 0 ? false : _ref$disabled,modal = _ref.modal,showReturn = _ref.showReturn,footerPaddingB = _ref.footerPaddingB;
7
+ var _useSubscription = useSubscription(),isSubscribed = _useSubscription.isSubscribed,isLoading = _useSubscription.isLoading;
8
+ if (disabled || isSubscribed) return children;
9
+ if (isLoading) return _jsx(Loading, {});
10
+ return _jsx(Paywall, { modal: modal, showReturn: showReturn, footerPaddingB: footerPaddingB });
11
+ }
@@ -0,0 +1,37 @@
1
+ var _jsxFileName = "/Users/christianstorch/Apps/nekoapps/libs/neko-rc-subscription/src/containers/SubscriptionRequiredCTA.js";var _excluded = ["children", "disabled", "hideButton", "size", "buttonProps"];function _objectWithoutProperties(e, t) {if (null == e) return {};var o,r,i = _objectWithoutPropertiesLoose(e, t);if (Object.getOwnPropertySymbols) {var n = Object.getOwnPropertySymbols(e);for (r = 0; r < n.length; r++) o = n[r], -1 === t.indexOf(o) && {}.propertyIsEnumerable.call(e, o) && (i[o] = e[o]);}return i;}function _objectWithoutPropertiesLoose(r, e) {if (null == r) return {};var t = {};for (var n in r) if ({}.hasOwnProperty.call(r, n)) {if (-1 !== e.indexOf(n)) continue;t[n] = r[n];}return t;}import { BlurView, Button, View, useTranslation } from '@neko-os/ui';
2
+ import { useNavigation } from '@react-navigation/native';
3
+
4
+ import { useIsSubscribed } from "./SubscriptionHandler";import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
5
+
6
+ export default function SubscriptionRequiredCTA(_ref)
7
+
8
+
9
+
10
+
11
+
12
+
13
+ {var children = _ref.children,_ref$disabled = _ref.disabled,disabled = _ref$disabled === void 0 ? false : _ref$disabled,_ref$hideButton = _ref.hideButton,hideButton = _ref$hideButton === void 0 ? false : _ref$hideButton,size = _ref.size,buttonProps = _ref.buttonProps,props = _objectWithoutProperties(_ref, _excluded);
14
+ var _useTranslation = useTranslation('subscription'),t = _useTranslation.t;
15
+ var _useNavigation = useNavigation(),navigate = _useNavigation.navigate;
16
+ var isSubscribed = useIsSubscribed();
17
+
18
+ if (disabled || isSubscribed) return children;
19
+
20
+ return (
21
+ _jsxs(View, Object.assign({ relative: true, hiddenOverflow: true }, props, { children: [
22
+ children,
23
+ _jsx(BlurView, { absoluteFill: true, center: true, zIndex: 10, intensity: 18, onPress: function onPress() {return navigate('subscription/active');}, children:
24
+ !hideButton &&
25
+ _jsx(Button, Object.assign({
26
+ label: t('cta.unlock'),
27
+ icon: "trophy-fill",
28
+ yellow: true,
29
+ size: size || 'xs' },
30
+ buttonProps, {
31
+ onPress: function onPress() {return navigate('subscription/active');} })
32
+ ) }
33
+
34
+ )] })
35
+ ));
36
+
37
+ }
@@ -0,0 +1,130 @@
1
+ var _jsxFileName = "/Users/christianstorch/Apps/nekoapps/libs/neko-rc-subscription/src/containers/paywall/Paywall.js";function _slicedToArray(r, e) {return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();}function _nonIterableRest() {throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");}function _unsupportedIterableToArray(r, a) {if (r) {if ("string" == typeof r) return _arrayLikeToArray(r, a);var t = {}.toString.call(r).slice(8, -1);return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;}}function _arrayLikeToArray(r, a) {(null == a || a > r.length) && (a = r.length);for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];return n;}function _iterableToArrayLimit(r, l) {var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];if (null != t) {var e,n,i,u,a = [],f = !0,o = !1;try {if (i = (t = t.call(r)).next, 0 === l) {if (Object(t) !== t) return;f = !1;} else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0);} catch (r) {o = !0, n = r;} finally {try {if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;} finally {if (o) throw n;}}return a;}}function _arrayWithHoles(r) {if (Array.isArray(r)) return r;}import {
2
+ AnimatedTopBar,
3
+ BlurView,
4
+ ReanimatedScrollHandler,
5
+ ScrollView,
6
+ View,
7
+ useReanimatedScroll,
8
+ useTranslation } from
9
+ '@neko-os/ui';
10
+ import { Platform } from "react-native-web";
11
+ import { useState } from 'react';
12
+ import Animated from 'react-native-reanimated';
13
+
14
+ import { useSubscription } from "../SubscriptionHandler";
15
+ import PaywallFeatures from "./PaywallFeatures";
16
+ import PaywallFooter from "./PaywallFooter";
17
+ import PaywallHero from "./PaywallHero";
18
+ import PaywallPlanCard from "./PaywallPlanCard";
19
+ import PaywallReturnIcon from "./PaywallReturnIcon";
20
+ import usePaywallActions from "./_request/usePaywallActions";import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
21
+
22
+ var AnimatedScrollView = Animated.createAnimatedComponent(ScrollView);
23
+
24
+ function getAvailablePackages(offerings) {
25
+ if (!(offerings != null && offerings.current)) return [];
26
+ var _offerings$current = offerings.current,annual = _offerings$current.annual,monthly = _offerings$current.monthly,lifetime = _offerings$current.lifetime;
27
+ return [annual, monthly, lifetime].filter(Boolean);
28
+ }
29
+
30
+ function getTrialDays(pkg) {var _pkg$product, _intro$periodUnit;
31
+ var intro = pkg == null ? void 0 : (_pkg$product = pkg.product) == null ? void 0 : _pkg$product.introPrice;
32
+ if (!intro || intro.price > 0) return null;
33
+ var units = intro.periodNumberOfUnits || 0;
34
+ var unit = (_intro$periodUnit = intro.periodUnit) == null ? void 0 : _intro$periodUnit.toUpperCase();
35
+ if (unit === 'DAY') return units;
36
+ if (unit === 'WEEK') return units * 7;
37
+ if (unit === 'MONTH') return units * 30;
38
+ return units;
39
+ }
40
+
41
+ function PaywallScrollContent(_ref)
42
+
43
+
44
+
45
+
46
+
47
+
48
+
49
+
50
+
51
+
52
+ {var _this = this;var image = _ref.image,title = _ref.title,subtitle = _ref.subtitle,features = _ref.features,packages = _ref.packages,selectedPkg = _ref.selectedPkg,onSelect = _ref.onSelect,monthlyPrice = _ref.monthlyPrice,showReturnIcon = _ref.showReturnIcon,modal = _ref.modal;
53
+ var _useReanimatedScroll = useReanimatedScroll(),scrollHandler = _useReanimatedScroll.scrollHandler;
54
+
55
+ return (
56
+ _jsxs(AnimatedScrollView, { onScroll: scrollHandler, children: [
57
+ _jsx(PaywallHero, { image: image, title: title, subtitle: subtitle, showReturnIcon: showReturnIcon, modal: modal }),
58
+ _jsx(PaywallFeatures, { features: features }),
59
+
60
+ _jsx(View, { gap: "xs", paddingH: "md", paddingT: "sm", children:
61
+ packages.map(function (pkg) {return (
62
+ _jsx(PaywallPlanCard, {
63
+
64
+ pkg: pkg,
65
+ selected: (selectedPkg == null ? void 0 : selectedPkg.identifier) === pkg.identifier,
66
+ onSelect: onSelect,
67
+ monthlyPrice: monthlyPrice }, pkg.identifier
68
+ ));}
69
+ ) }
70
+ ),
71
+ _jsx(View, { height: 200 })] }
72
+ ));
73
+
74
+ }
75
+
76
+ export default function Paywall(_ref2) {var _packages$find, _packages$find$produc;var modal = _ref2.modal,showReturn = _ref2.showReturn,footerPaddingB = _ref2.footerPaddingB;
77
+ var _useTranslation = useTranslation('subscription'),t = _useTranslation.t;
78
+ var _useSubscription = useSubscription(),offerings = _useSubscription.offerings,paywallConfig = _useSubscription.paywallConfig;
79
+ var _usePaywallActions = usePaywallActions(),handlePurchase = _usePaywallActions.handlePurchase,handleRestore = _usePaywallActions.handleRestore,loading = _usePaywallActions.loading;
80
+
81
+ var packages = getAvailablePackages(offerings);
82
+ var _useState = useState(function () {return packages[0] || null;}),_useState2 = _slicedToArray(_useState, 2),selectedPkg = _useState2[0],setSelectedPkg = _useState2[1];
83
+
84
+ var monthlyPrice = (_packages$find = packages.find(function (p) {return p.packageType === 'MONTHLY';})) == null ? void 0 : (_packages$find$produc = _packages$find.product) == null ? void 0 : _packages$find$produc.price;
85
+ var trialDays = getTrialDays(selectedPkg);
86
+
87
+ var title = (paywallConfig == null ? void 0 : paywallConfig.title) || t('paywall.title');
88
+ var subtitle = (paywallConfig == null ? void 0 : paywallConfig.subtitle) || t('paywall.subtitle');
89
+ var image = paywallConfig == null ? void 0 : paywallConfig.image;
90
+ var features = paywallConfig == null ? void 0 : paywallConfig.features;
91
+ var showReturnIcon = modal || showReturn;
92
+
93
+ return (
94
+ _jsxs(View, { flex: true, bg: "mainBG", children: [
95
+ _jsxs(ReanimatedScrollHandler, { children: [
96
+ _jsx(AnimatedTopBar, {
97
+ WrapperView: BlurView,
98
+ title: title,
99
+ subtitle: subtitle,
100
+ slide: true,
101
+ shadow: true,
102
+ showAfter: 230,
103
+ useSafeArea: !(modal && Platform.OS === 'ios'),
104
+ left: showReturnIcon && _jsx(PaywallReturnIcon, { modal: modal }) }
105
+ ),
106
+
107
+ _jsx(PaywallScrollContent, {
108
+ image: image,
109
+ title: title,
110
+ subtitle: subtitle,
111
+ features: features,
112
+ packages: packages,
113
+ selectedPkg: selectedPkg,
114
+ onSelect: setSelectedPkg,
115
+ monthlyPrice: monthlyPrice,
116
+ showReturnIcon: showReturnIcon,
117
+ modal: modal }
118
+ )] }
119
+ ),
120
+
121
+ _jsx(PaywallFooter, {
122
+ onPurchase: function onPurchase() {return handlePurchase(selectedPkg);},
123
+ onRestore: handleRestore,
124
+ loading: loading,
125
+ trialDays: trialDays,
126
+ footerPaddingB: footerPaddingB }
127
+ )] }
128
+ ));
129
+
130
+ }
@@ -0,0 +1,42 @@
1
+ var _jsxFileName = "/Users/christianstorch/Apps/nekoapps/libs/neko-rc-subscription/src/containers/paywall/PaywallFeatures.js";import { Icon, Text, View, useTranslation } from '@neko-os/ui';import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+
3
+ export default function PaywallFeatures(_ref) {var _this = this;var features = _ref.features;
4
+ var _useTranslation = useTranslation('subscription'),t = _useTranslation.t;
5
+
6
+ if (!(features != null && features.length)) return null;
7
+
8
+ return (
9
+ _jsxs(View, { paddingH: "xl", paddingB: "md", children: [
10
+ _jsxs(View, { row: true, paddingB: "xs", children: [
11
+ _jsx(View, { flex: true }),
12
+ _jsx(Text, { xs: true, text3: true, center: true, width: 40, children:
13
+ t('paywall.free') }
14
+ ),
15
+ _jsx(Text, { xs: true, strong: true, center: true, width: 40, color: "primary", children:
16
+ t('paywall.pro') }
17
+ )] }
18
+ ),
19
+
20
+ features.map(function (feature, i) {return (
21
+ _jsxs(View, { row: true, centerV: true, paddingV: "sm", children: [
22
+ _jsxs(View, { flex: true, row: true, gap: "sm", centerV: true, children: [
23
+ _jsx(Icon, { name: feature.icon || 'check-line', size: "sm" }),
24
+ _jsx(Text, { sm: true, flex: true, children:
25
+ t(feature.label) }
26
+ )] }
27
+ ),
28
+ _jsx(View, { width: 40, center: true, children:
29
+ feature.free ?
30
+ _jsx(Icon, { name: "checkbox-circle-fill", primary: true, size: "sm" }) :
31
+
32
+ _jsx(Icon, { name: "close-circle-fill", color: "text4_op40", size: "sm" }) }
33
+
34
+ ),
35
+ _jsx(View, { width: 40, center: true, children:
36
+ _jsx(Icon, { name: "checkbox-circle-fill", primary: true, size: "sm" }) }
37
+ )] }, i
38
+ ));}
39
+ )] }
40
+ ));
41
+
42
+ }
@@ -0,0 +1,16 @@
1
+ var _jsxFileName = "/Users/christianstorch/Apps/nekoapps/libs/neko-rc-subscription/src/containers/paywall/PaywallFooter.js";import { Button, Link, View, useTranslation } from '@neko-os/ui';
2
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+
4
+ export default function PaywallFooter(_ref) {var onPurchase = _ref.onPurchase,onRestore = _ref.onRestore,loading = _ref.loading,trialDays = _ref.trialDays,footerPaddingB = _ref.footerPaddingB;
5
+ var _useTranslation = useTranslation('subscription'),t = _useTranslation.t;
6
+ var _useSafeAreaInsets = useSafeAreaInsets(),bottom = _useSafeAreaInsets.bottom;
7
+
8
+ var ctaLabel = trialDays ? t('paywall.ctaTrial') : t('paywall.cta');
9
+
10
+ return (
11
+ _jsxs(View, { gap: "sm", padding: "md", paddingB: footerPaddingB || Math.max(bottom / 2, 16), borderT: true, shadow: !footerPaddingB, children: [
12
+ _jsx(Button, { label: ctaLabel, onPress: onPurchase, disabled: loading, loading: loading }),
13
+ _jsx(Link, { label: t('paywall.restore'), onPress: onRestore, disabled: loading, xs: true, text3: true, center: true, underline: true })] }
14
+ ));
15
+
16
+ }
@@ -0,0 +1,33 @@
1
+ var _jsxFileName = "/Users/christianstorch/Apps/nekoapps/libs/neko-rc-subscription/src/containers/paywall/PaywallHero.js";import { Image, ParallaxHeader, SafeAreaView, Text, View } from '@neko-os/ui';
2
+
3
+ import PaywallReturnIcon from "./PaywallReturnIcon";import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
4
+
5
+ export default function PaywallHero(_ref) {var image = _ref.image,title = _ref.title,subtitle = _ref.subtitle,showReturnIcon = _ref.showReturnIcon,modal = _ref.modal;
6
+ var IMAGE_HEIGHT = modal ? 180 : 280;
7
+ return (
8
+ _jsxs(_Fragment, { children: [
9
+ image &&
10
+ _jsx(ParallaxHeader, { height: IMAGE_HEIGHT, disableResistence: true, children:
11
+ _jsx(Image, { source: image, height: IMAGE_HEIGHT, fullW: true, br: 0 }) }
12
+ ),
13
+
14
+
15
+ showReturnIcon &&
16
+ _jsx(SafeAreaView, { absolute: true, top: "sm", left: "md", zIndex: 100, children:
17
+ _jsx(PaywallReturnIcon, { modal: modal }) }
18
+ ),
19
+
20
+
21
+ _jsxs(View, { brT: "xxxl", bg: "mainBG", marginT: -25, paddingH: "md", gap: "xs", paddingV: "xl", children: [
22
+ _jsx(Text, { h2: true, strong: true, center: true, children:
23
+ title }
24
+ ),
25
+ subtitle &&
26
+ _jsx(Text, { sm: true, text3: true, center: true, children:
27
+ subtitle }
28
+ )] }
29
+
30
+ )] }
31
+ ));
32
+
33
+ }
@@ -0,0 +1,71 @@
1
+ var _jsxFileName = "/Users/christianstorch/Apps/nekoapps/libs/neko-rc-subscription/src/containers/paywall/PaywallPlanCard.js";import { Card, Link, Radio, Tag, Text, View, useTranslation } from '@neko-os/ui';import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+
3
+ var TYPE_LABELS = {
4
+ MONTHLY: 'monthly',
5
+ ANNUAL: 'yearly',
6
+ LIFETIME: 'lifetime'
7
+ };
8
+
9
+ function getTrialDays(introPrice) {var _introPrice$periodUni;
10
+ if (!introPrice || introPrice.price > 0) return null;
11
+ var units = introPrice.periodNumberOfUnits || 0;
12
+ var unit = (_introPrice$periodUni = introPrice.periodUnit) == null ? void 0 : _introPrice$periodUni.toUpperCase();
13
+ if (unit === 'DAY') return units;
14
+ if (unit === 'WEEK') return units * 7;
15
+ if (unit === 'MONTH') return units * 30;
16
+ return units;
17
+ }
18
+
19
+ function getSavePercent(monthlyPrice, annualPrice) {
20
+ if (!monthlyPrice || !annualPrice) return null;
21
+ var yearlyFromMonthly = monthlyPrice * 12;
22
+ var percent = Math.round((yearlyFromMonthly - annualPrice) / yearlyFromMonthly * 100);
23
+ return percent > 0 ? percent : null;
24
+ }
25
+
26
+ export default function PaywallPlanCard(_ref) {var pkg = _ref.pkg,selected = _ref.selected,onSelect = _ref.onSelect,monthlyPrice = _ref.monthlyPrice;
27
+ var _useTranslation = useTranslation('subscription'),t = _useTranslation.t;
28
+ var product = pkg.product;
29
+ var typeKey = TYPE_LABELS[pkg.packageType] || pkg.packageType;
30
+ var label = t('paywall.' + typeKey, { defaultValue: typeKey });
31
+
32
+ var trialDays = getTrialDays(product.introPrice);
33
+ var savePercent = pkg.packageType === 'ANNUAL' ? getSavePercent(monthlyPrice, product.price) : null;
34
+
35
+ return (
36
+ _jsx(Link, { onPress: function onPress() {return onSelect(pkg);}, children:
37
+ _jsxs(Card, {
38
+ row: true,
39
+ centerV: true,
40
+ padding: "md",
41
+ gap: "md",
42
+ bg: selected ? 'primary_op10' : 'overlayBG',
43
+ borderColor: selected ? 'primary' : 'divider',
44
+ border: true,
45
+ minH: "xxl", children: [
46
+
47
+ _jsx(Radio, { xs: true, value: selected }),
48
+
49
+ _jsxs(View, { flex: true, gap: "xxs", children: [
50
+ _jsxs(View, { row: true, centerV: true, gap: "xs", children: [
51
+ _jsx(Text, { strong: true, children: label }),
52
+ savePercent && _jsx(Tag, { primary: true, label: t('paywall.save', { percent: savePercent }), fill: true }),
53
+ trialDays && _jsx(Tag, { green: true, label: t('paywall.freeTrial', { count: trialDays }) })] }
54
+ ),
55
+ pkg.packageType === 'ANNUAL' && product.pricePerMonthString &&
56
+ _jsx(Text, { xs: true, text3: true, children:
57
+ t('paywall.perMonth', { price: product.pricePerMonthString }) }
58
+ ),
59
+
60
+ product.introPrice && product.introPrice.price > 0 &&
61
+ _jsxs(Text, { xs: true, text3: true, children: [
62
+ product.introPrice.priceString, " intro"] }
63
+ )] }
64
+
65
+ ),
66
+
67
+ _jsx(Text, { strong: true, children: product.priceString })] }
68
+ ) }
69
+ ));
70
+
71
+ }
@@ -0,0 +1,8 @@
1
+ var _jsxFileName = "/Users/christianstorch/Apps/nekoapps/libs/neko-rc-subscription/src/containers/paywall/PaywallReturnIcon.js";import { Button } from '@neko-os/ui';
2
+ import { useNavigation } from '@react-navigation/native';import { jsx as _jsx } from "react/jsx-runtime";
3
+
4
+ export default function PaywallReturnIcon(_ref) {var modal = _ref.modal;
5
+ var _useNavigation = useNavigation(),goBack = _useNavigation.goBack;
6
+
7
+ return _jsx(Button, { icon: modal ? 'close-line' : 'arrow-left-line', ratio: 1, xs: true, color: "mainBG", round: true, onPress: goBack });
8
+ }
@@ -0,0 +1,39 @@
1
+ function asyncGeneratorStep(n, t, e, r, o, a, c) {try {var i = n[a](c),u = i.value;} catch (n) {return void e(n);}i.done ? t(u) : Promise.resolve(u).then(r, o);}function _asyncToGenerator(n) {return function () {var t = this,e = arguments;return new Promise(function (r, o) {var a = n.apply(t, e);function _next(n) {asyncGeneratorStep(a, r, o, _next, _throw, "next", n);}function _throw(n) {asyncGeneratorStep(a, r, o, _next, _throw, "throw", n);}_next(void 0);});};}function _slicedToArray(r, e) {return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();}function _nonIterableRest() {throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");}function _unsupportedIterableToArray(r, a) {if (r) {if ("string" == typeof r) return _arrayLikeToArray(r, a);var t = {}.toString.call(r).slice(8, -1);return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;}}function _arrayLikeToArray(r, a) {(null == a || a > r.length) && (a = r.length);for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];return n;}function _iterableToArrayLimit(r, l) {var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];if (null != t) {var e,n,i,u,a = [],f = !0,o = !1;try {if (i = (t = t.call(r)).next, 0 === l) {if (Object(t) !== t) return;f = !1;} else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0);} catch (r) {o = !0, n = r;} finally {try {if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;} finally {if (o) throw n;}}return a;}}function _arrayWithHoles(r) {if (Array.isArray(r)) return r;}import { Alert } from "react-native-web";
2
+ import { useTranslation } from '@neko-os/ui';
3
+ import { useState } from 'react';
4
+ import Purchases from 'react-native-purchases';
5
+
6
+ import { useSubscription } from "../../SubscriptionHandler";
7
+
8
+ export default function usePaywallActions() {
9
+ var _useTranslation = useTranslation('subscription'),t = _useTranslation.t;
10
+ var _useSubscription = useSubscription(),refresh = _useSubscription.refresh;
11
+ var _useState = useState(false),_useState2 = _slicedToArray(_useState, 2),loading = _useState2[0],setLoading = _useState2[1];function
12
+
13
+ handlePurchase(_x) {return _handlePurchase.apply(this, arguments);}function _handlePurchase() {_handlePurchase = _asyncToGenerator(function* (pkg) {
14
+ if (!pkg || loading) return;
15
+ setLoading(true);
16
+ try {
17
+ yield Purchases.purchasePackage(pkg);
18
+ } catch (e) {
19
+ if (!e.userCancelled) Alert.alert(t('paywall.errorTitle', { defaultValue: 'Error' }), e.message);
20
+ } finally {
21
+ setLoading(false);
22
+ }
23
+ });return _handlePurchase.apply(this, arguments);}function
24
+
25
+ handleRestore() {return _handleRestore.apply(this, arguments);}function _handleRestore() {_handleRestore = _asyncToGenerator(function* () {
26
+ if (loading) return;
27
+ setLoading(true);
28
+ try {
29
+ yield Purchases.restorePurchases();
30
+ yield refresh();
31
+ } catch (e) {
32
+ Alert.alert(t('paywall.errorTitle', { defaultValue: 'Error' }), e.message);
33
+ } finally {
34
+ setLoading(false);
35
+ }
36
+ });return _handleRestore.apply(this, arguments);}
37
+
38
+ return { handlePurchase: handlePurchase, handleRestore: handleRestore, loading: loading };
39
+ }
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { registerSubscriptionLocales } from "./locales/index";
2
+ export { default as SubscriptionHandler, useIsSubscribed, useSubscription } from "./containers/SubscriptionHandler";
3
+ export { default as SubscriptionRequired } from "./containers/SubscriptionRequired";
4
+ export { default as SubscriptionRequiredCTA } from "./containers/SubscriptionRequiredCTA";
5
+ export { default as Paywall } from "./containers/paywall/Paywall";
6
+ export { default as ActiveSubscriptionView } from "./views/active/ActiveSubscriptionView";