@neko-os/rc-subscription 0.2.0 → 0.3.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.
- package/README.md +32 -0
- package/dist/config.js +5 -1
- package/dist/containers/SubscriptionHandler.js +52 -2
- package/dist/containers/SubscriptionRequiredCTA.js +3 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/config.js +4 -0
- package/src/containers/SubscriptionHandler.js +52 -2
- package/src/containers/SubscriptionRequiredCTA.js +3 -2
- package/src/index.js +1 -1
package/README.md
CHANGED
|
@@ -84,6 +84,7 @@ import { ActiveSubscriptionView } from 'neko-rc-subscription'
|
|
|
84
84
|
|--------|-------------|
|
|
85
85
|
| `useSubscription()` | Full subscription context: `{ isSubscribed, isLoading, customerInfo, offerings, paywallConfig, refresh }` |
|
|
86
86
|
| `useIsSubscribed()` | Shorthand boolean — `true` when active entitlement exists. |
|
|
87
|
+
| `useSubscribedAction(fn?)` | Returns an action wrapper that runs `fn` when subscribed, otherwise opens the paywall. See [Gating an action](#gating-an-action). |
|
|
87
88
|
|
|
88
89
|
### Functions
|
|
89
90
|
|
|
@@ -123,6 +124,37 @@ import { ActiveSubscriptionView } from 'neko-rc-subscription'
|
|
|
123
124
|
| `size` | `string` | `'xs'` | Button size. |
|
|
124
125
|
| `buttonProps` | `object` | — | Extra props forwarded to the unlock Button. |
|
|
125
126
|
|
|
127
|
+
## Gating an action
|
|
128
|
+
|
|
129
|
+
`useSubscribedAction` wraps any callback so it only fires for subscribed users — otherwise the paywall opens (navigates to `subscription/active`). Use it for one-off actions that don't warrant a full gate component (`SubscriptionRequired` / `SubscriptionRequiredCTA`).
|
|
130
|
+
|
|
131
|
+
Two equivalent call styles:
|
|
132
|
+
|
|
133
|
+
```jsx
|
|
134
|
+
import { useSubscribedAction } from 'neko-rc-subscription'
|
|
135
|
+
|
|
136
|
+
// Bind the fn up front
|
|
137
|
+
function ExportButton() {
|
|
138
|
+
const exportData = useSubscribedAction(() => doExport())
|
|
139
|
+
return <Button label="Export" onPress={exportData} />
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Bind nothing — pass the fn at call time (handy when the callback varies)
|
|
143
|
+
function Row({ item }) {
|
|
144
|
+
const run = useSubscribedAction()
|
|
145
|
+
return <Button label="Pin" onPress={() => run(() => pin(item))} />
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
While subscription state is still loading (`isLoading`), the action is a no-op — it neither runs the fn nor opens the paywall, so an already-subscribed user is never bounced to the paywall during init.
|
|
150
|
+
|
|
151
|
+
Arguments are forwarded to the wrapped fn, and its return value is passed through (or `undefined` when the paywall is shown instead):
|
|
152
|
+
|
|
153
|
+
```jsx
|
|
154
|
+
const save = useSubscribedAction(saveReport)
|
|
155
|
+
save(reportId) // -> saveReport(reportId) when subscribed
|
|
156
|
+
```
|
|
157
|
+
|
|
126
158
|
## Package Support
|
|
127
159
|
|
|
128
160
|
The paywall auto-renders available packages from the current RevenueCat offering. Supported package types:
|
package/dist/config.js
CHANGED
|
@@ -4,4 +4,8 @@ export var RC_API_KEY =
|
|
|
4
4
|
Platform.OS === 'ios' ? process.env.EXPO_PUBLIC_RC_API_KEY_IOS : process.env.EXPO_PUBLIC_RC_API_KEY_ANDROID;
|
|
5
5
|
export var RC_ENTITLEMENT_ID = process.env.EXPO_PUBLIC_RC_ENTITLEMENT_ID || 'premium';
|
|
6
6
|
export var PRIVACY_POLICY_URL = process.env.EXPO_PUBLIC_PRIVACY_POLICY_URL;
|
|
7
|
-
export var TERMS_OF_USE_URL = process.env.EXPO_PUBLIC_TERMS_OF_USE_URL;
|
|
7
|
+
export var TERMS_OF_USE_URL = process.env.EXPO_PUBLIC_TERMS_OF_USE_URL;
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export var SUBSCRIPTION_ROUTE = 'subscription/active';
|
|
@@ -1,7 +1,8 @@
|
|
|
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
|
|
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 _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 _toConsumableArray(r) {return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread();}function _nonIterableSpread() {throw new TypeError("Invalid attempt to spread 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 _iterableToArray(r) {if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r);}function _arrayWithoutHoles(r) {if (Array.isArray(r)) return _arrayLikeToArray(r);}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 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, useCallback, useContext, useEffect, useState } from 'react';
|
|
2
2
|
import Purchases from 'react-native-purchases';
|
|
3
|
+
import { useNavigation } from '@react-navigation/native';
|
|
3
4
|
|
|
4
|
-
import { RC_API_KEY, RC_ENTITLEMENT_ID } from "../config";import { jsx as _jsx } from "react/jsx-runtime";
|
|
5
|
+
import { RC_API_KEY, RC_ENTITLEMENT_ID, SUBSCRIPTION_ROUTE } from "../config";import { jsx as _jsx } from "react/jsx-runtime";
|
|
5
6
|
|
|
6
7
|
var SubscriptionContext = createContext({
|
|
7
8
|
isSubscribed: false,
|
|
@@ -20,6 +21,55 @@ export function useIsSubscribed() {
|
|
|
20
21
|
return useContext(SubscriptionContext).isSubscribed;
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
export function useSubscribedAction(boundFn) {
|
|
40
|
+
var _useSubscription = useSubscription(),isSubscribed = _useSubscription.isSubscribed,isLoading = _useSubscription.isLoading;
|
|
41
|
+
var _useNavigation = useNavigation(),navigate = _useNavigation.navigate;
|
|
42
|
+
|
|
43
|
+
return useCallback(
|
|
44
|
+
function () {for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {args[_key] = arguments[_key];}
|
|
45
|
+
var fn = boundFn || args[0];
|
|
46
|
+
var fnArgs = boundFn ? args : args.slice(1);
|
|
47
|
+
|
|
48
|
+
if (typeof fn !== 'function') {
|
|
49
|
+
if (__DEV__) {
|
|
50
|
+
console.warn(
|
|
51
|
+
'[neko-rc-subscription] useSubscribedAction: no function to run — ' +
|
|
52
|
+
'pass it to the hook `useSubscribedAction(fn)` or to the action `action(fn)`.'
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
if (isLoading) return undefined;
|
|
61
|
+
|
|
62
|
+
if (!isSubscribed) {
|
|
63
|
+
navigate(SUBSCRIPTION_ROUTE);
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return fn.apply(void 0, _toConsumableArray(fnArgs));
|
|
68
|
+
},
|
|
69
|
+
[isSubscribed, isLoading, navigate, boundFn]
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
23
73
|
export default function SubscriptionHandler(_ref) {var children = _ref.children,paywallConfig = _ref.paywallConfig;
|
|
24
74
|
var _useState = useState(false),_useState2 = _slicedToArray(_useState, 2),isSubscribed = _useState2[0],setIsSubscribed = _useState2[1];
|
|
25
75
|
var _useState3 = useState(true),_useState4 = _slicedToArray(_useState3, 2),isLoading = _useState4[0],setIsLoading = _useState4[1];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
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
2
|
import { useNavigation } from '@react-navigation/native';
|
|
3
3
|
|
|
4
|
+
import { SUBSCRIPTION_ROUTE } from "../config";
|
|
4
5
|
import { useIsSubscribed } from "./SubscriptionHandler";import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
5
6
|
|
|
6
7
|
export default function SubscriptionRequiredCTA(_ref)
|
|
@@ -20,7 +21,7 @@ export default function SubscriptionRequiredCTA(_ref)
|
|
|
20
21
|
return (
|
|
21
22
|
_jsxs(View, Object.assign({ relative: true, hiddenOverflow: true }, props, { children: [
|
|
22
23
|
children,
|
|
23
|
-
_jsx(BlurView, { absoluteFill: true, center: true, zIndex: 10, intensity: 18, onPress: function onPress() {return navigate(
|
|
24
|
+
_jsx(BlurView, { absoluteFill: true, center: true, zIndex: 10, intensity: 18, onPress: function onPress() {return navigate(SUBSCRIPTION_ROUTE);}, children:
|
|
24
25
|
!hideButton &&
|
|
25
26
|
_jsx(Button, Object.assign({
|
|
26
27
|
label: t('cta.unlock'),
|
|
@@ -28,7 +29,7 @@ export default function SubscriptionRequiredCTA(_ref)
|
|
|
28
29
|
yellow: true,
|
|
29
30
|
size: size || 'xs' },
|
|
30
31
|
buttonProps, {
|
|
31
|
-
onPress: function onPress() {return navigate(
|
|
32
|
+
onPress: function onPress() {return navigate(SUBSCRIPTION_ROUTE);} })
|
|
32
33
|
) }
|
|
33
34
|
|
|
34
35
|
)] })
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { registerSubscriptionLocales } from "./locales/index";
|
|
2
|
-
export { default as SubscriptionHandler, useIsSubscribed, useSubscription } from "./containers/SubscriptionHandler";
|
|
2
|
+
export { default as SubscriptionHandler, useIsSubscribed, useSubscribedAction, useSubscription } from "./containers/SubscriptionHandler";
|
|
3
3
|
export { default as SubscriptionRequired } from "./containers/SubscriptionRequired";
|
|
4
4
|
export { default as SubscriptionRequiredCTA } from "./containers/SubscriptionRequiredCTA";
|
|
5
5
|
export { default as Paywall } from "./containers/paywall/Paywall";
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -5,3 +5,7 @@ export const RC_API_KEY =
|
|
|
5
5
|
export const RC_ENTITLEMENT_ID = process.env.EXPO_PUBLIC_RC_ENTITLEMENT_ID || 'premium'
|
|
6
6
|
export const PRIVACY_POLICY_URL = process.env.EXPO_PUBLIC_PRIVACY_POLICY_URL
|
|
7
7
|
export const TERMS_OF_USE_URL = process.env.EXPO_PUBLIC_TERMS_OF_USE_URL
|
|
8
|
+
|
|
9
|
+
// Route that renders ActiveSubscriptionView, which shows the Paywall when the
|
|
10
|
+
// user is not subscribed. Used to imperatively open the paywall.
|
|
11
|
+
export const SUBSCRIPTION_ROUTE = 'subscription/active'
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import React, { createContext, useContext, useEffect, useState } from 'react'
|
|
1
|
+
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
|
2
2
|
import Purchases from 'react-native-purchases'
|
|
3
|
+
import { useNavigation } from '@react-navigation/native'
|
|
3
4
|
|
|
4
|
-
import { RC_API_KEY, RC_ENTITLEMENT_ID } from '../config'
|
|
5
|
+
import { RC_API_KEY, RC_ENTITLEMENT_ID, SUBSCRIPTION_ROUTE } from '../config'
|
|
5
6
|
|
|
6
7
|
const SubscriptionContext = createContext({
|
|
7
8
|
isSubscribed: false,
|
|
@@ -20,6 +21,55 @@ export function useIsSubscribed() {
|
|
|
20
21
|
return useContext(SubscriptionContext).isSubscribed
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Returns an action wrapper that only runs when the user is subscribed,
|
|
26
|
+
* otherwise opens the paywall (navigates to `subscription/active`).
|
|
27
|
+
*
|
|
28
|
+
* Two equivalent call styles:
|
|
29
|
+
*
|
|
30
|
+
* const action = useSubscribedAction(fn) // bind the fn up front
|
|
31
|
+
* action(...args) // -> fn(...args) if subscribed
|
|
32
|
+
*
|
|
33
|
+
* const action = useSubscribedAction() // bind nothing
|
|
34
|
+
* action(fn, ...args) // -> fn(...args) if subscribed
|
|
35
|
+
*
|
|
36
|
+
* Returns the result of `fn` when subscribed, or `undefined` when the paywall
|
|
37
|
+
* is shown instead.
|
|
38
|
+
*/
|
|
39
|
+
export function useSubscribedAction(boundFn) {
|
|
40
|
+
const { isSubscribed, isLoading } = useSubscription()
|
|
41
|
+
const { navigate } = useNavigation()
|
|
42
|
+
|
|
43
|
+
return useCallback(
|
|
44
|
+
(...args) => {
|
|
45
|
+
const fn = boundFn || args[0]
|
|
46
|
+
const fnArgs = boundFn ? args : args.slice(1)
|
|
47
|
+
|
|
48
|
+
if (typeof fn !== 'function') {
|
|
49
|
+
if (__DEV__) {
|
|
50
|
+
console.warn(
|
|
51
|
+
'[neko-rc-subscription] useSubscribedAction: no function to run — ' +
|
|
52
|
+
'pass it to the hook `useSubscribedAction(fn)` or to the action `action(fn)`.',
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
return undefined
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Subscription state not resolved yet: no-op rather than wrongly bouncing
|
|
59
|
+
// an already-subscribed user to the paywall.
|
|
60
|
+
if (isLoading) return undefined
|
|
61
|
+
|
|
62
|
+
if (!isSubscribed) {
|
|
63
|
+
navigate(SUBSCRIPTION_ROUTE)
|
|
64
|
+
return undefined
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return fn(...fnArgs)
|
|
68
|
+
},
|
|
69
|
+
[isSubscribed, isLoading, navigate, boundFn],
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
23
73
|
export default function SubscriptionHandler({ children, paywallConfig }) {
|
|
24
74
|
const [isSubscribed, setIsSubscribed] = useState(false)
|
|
25
75
|
const [isLoading, setIsLoading] = useState(true)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BlurView, Button, View, useTranslation } from '@neko-os/ui'
|
|
2
2
|
import { useNavigation } from '@react-navigation/native'
|
|
3
3
|
|
|
4
|
+
import { SUBSCRIPTION_ROUTE } from '../config'
|
|
4
5
|
import { useIsSubscribed } from './SubscriptionHandler'
|
|
5
6
|
|
|
6
7
|
export default function SubscriptionRequiredCTA({
|
|
@@ -20,7 +21,7 @@ export default function SubscriptionRequiredCTA({
|
|
|
20
21
|
return (
|
|
21
22
|
<View relative hiddenOverflow {...props}>
|
|
22
23
|
{children}
|
|
23
|
-
<BlurView absoluteFill center zIndex={10} intensity={18} onPress={() => navigate(
|
|
24
|
+
<BlurView absoluteFill center zIndex={10} intensity={18} onPress={() => navigate(SUBSCRIPTION_ROUTE)}>
|
|
24
25
|
{!hideButton && (
|
|
25
26
|
<Button
|
|
26
27
|
label={t('cta.unlock')}
|
|
@@ -28,7 +29,7 @@ export default function SubscriptionRequiredCTA({
|
|
|
28
29
|
yellow
|
|
29
30
|
size={size || 'xs'}
|
|
30
31
|
{...buttonProps}
|
|
31
|
-
onPress={() => navigate(
|
|
32
|
+
onPress={() => navigate(SUBSCRIPTION_ROUTE)}
|
|
32
33
|
/>
|
|
33
34
|
)}
|
|
34
35
|
</BlurView>
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { registerSubscriptionLocales } from './locales/index'
|
|
2
|
-
export { default as SubscriptionHandler, useIsSubscribed, useSubscription } from './containers/SubscriptionHandler'
|
|
2
|
+
export { default as SubscriptionHandler, useIsSubscribed, useSubscribedAction, useSubscription } from './containers/SubscriptionHandler'
|
|
3
3
|
export { default as SubscriptionRequired } from './containers/SubscriptionRequired'
|
|
4
4
|
export { default as SubscriptionRequiredCTA } from './containers/SubscriptionRequiredCTA'
|
|
5
5
|
export { default as Paywall } from './containers/paywall/Paywall'
|