payment-kit 1.27.2 → 1.29.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/__blocklet__.js +37 -0
- package/api/ocap-1.30-subpath-shims.d.ts +35 -0
- package/api/src/crons/index.ts +32 -0
- package/api/src/crons/metering-subscription-detection.ts +12 -14
- package/api/src/crons/overdue-detection.ts +51 -74
- package/api/src/crons/retry-pending-events.ts +58 -0
- package/api/src/integrations/app-store/apple-root-certs.ts +26 -0
- package/api/src/integrations/app-store/client.ts +369 -0
- package/api/src/integrations/app-store/handlers/index.ts +46 -0
- package/api/src/integrations/app-store/handlers/subscription.ts +635 -0
- package/api/src/integrations/app-store/node-apple-receipt-verify.d.ts +17 -0
- package/api/src/integrations/app-store/notification-routing.ts +18 -0
- package/api/src/integrations/app-store/signed-data-verifier.ts +150 -0
- package/api/src/integrations/arcblock/nft.ts +6 -2
- package/api/src/integrations/arcblock/stake.ts +3 -2
- package/api/src/integrations/arcblock/token.ts +4 -4
- package/api/src/integrations/blocklet/notification.ts +1 -1
- package/api/src/integrations/ethereum/tx.ts +29 -0
- package/api/src/integrations/google-play/client.ts +276 -0
- package/api/src/integrations/google-play/handlers/index.ts +69 -0
- package/api/src/integrations/google-play/handlers/subscription.ts +565 -0
- package/api/src/integrations/google-play/handlers/voided.ts +106 -0
- package/api/src/integrations/google-play/setup.ts +43 -0
- package/api/src/integrations/google-play/verify.ts +251 -0
- package/api/src/integrations/iap-reconcile.ts +415 -0
- package/api/src/integrations/stripe/handlers/invoice.ts +70 -53
- package/api/src/integrations/stripe/handlers/payment-intent.ts +8 -1
- package/api/src/integrations/stripe/resource.ts +8 -0
- package/api/src/libs/audit.ts +70 -24
- package/api/src/libs/auth.ts +49 -2
- package/api/src/libs/chain-error.ts +31 -0
- package/api/src/libs/entitlement.ts +399 -0
- package/api/src/libs/env.ts +2 -0
- package/api/src/libs/error.ts +15 -0
- package/api/src/libs/event.ts +42 -1
- package/api/src/libs/invoice.ts +69 -34
- package/api/src/libs/notification/template/customer-auto-recharge-daily-limit-exceeded.ts +1 -3
- package/api/src/libs/notification/template/customer-auto-recharge-failed.ts +1 -3
- package/api/src/libs/notification/template/customer-credit-grant-granted.ts +1 -3
- package/api/src/libs/notification/template/customer-credit-insufficient.ts +1 -3
- package/api/src/libs/notification/template/customer-credit-low-balance.ts +1 -3
- package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -3
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -3
- package/api/src/libs/notification/template/one-time-payment-refund-succeeded.ts +1 -3
- package/api/src/libs/notification/template/one-time-payment-succeeded.ts +1 -3
- package/api/src/libs/notification/template/subscription-renew-failed.ts +1 -3
- package/api/src/libs/notification/template/subscription-slippage-exceeded.ts +1 -3
- package/api/src/libs/notification/template/subscription-slippage-warning.ts +1 -3
- package/api/src/libs/notification/template/subscription-succeeded.ts +1 -1
- package/api/src/libs/pagination.ts +14 -9
- package/api/src/libs/payment.ts +25 -10
- package/api/src/libs/security.ts +51 -0
- package/api/src/libs/session.ts +1 -1
- package/api/src/libs/subscription.ts +13 -1
- package/api/src/libs/timing.ts +35 -0
- package/api/src/libs/util.ts +29 -15
- package/api/src/libs/wallet-migration.ts +72 -53
- package/api/src/queues/auto-recharge.ts +1 -1
- package/api/src/queues/credit-consume.ts +94 -12
- package/api/src/queues/credit-grant.ts +4 -0
- package/api/src/queues/event.ts +39 -21
- package/api/src/queues/invoice.ts +1 -0
- package/api/src/queues/payment.ts +83 -15
- package/api/src/queues/refund.ts +84 -71
- package/api/src/queues/subscription.ts +1 -0
- package/api/src/queues/webhook.ts +12 -2
- package/api/src/routes/checkout-sessions.ts +82 -43
- package/api/src/routes/connect/change-payment.ts +2 -0
- package/api/src/routes/connect/change-plan.ts +2 -0
- package/api/src/routes/connect/pay.ts +12 -3
- package/api/src/routes/connect/setup.ts +3 -1
- package/api/src/routes/connect/shared.ts +52 -39
- package/api/src/routes/connect/subscribe.ts +4 -1
- package/api/src/routes/credit-grants.ts +25 -17
- package/api/src/routes/donations.ts +2 -2
- package/api/src/routes/entitlements.ts +105 -0
- package/api/src/routes/events.ts +2 -2
- package/api/src/routes/index.ts +12 -2
- package/api/src/routes/integrations/app-store.ts +267 -0
- package/api/src/routes/integrations/google-play.ts +324 -0
- package/api/src/routes/meter-events.ts +16 -6
- package/api/src/routes/payment-links.ts +1 -1
- package/api/src/routes/payment-methods.ts +131 -1
- package/api/src/routes/settings.ts +1 -1
- package/api/src/routes/tax-rates.ts +1 -1
- package/api/src/store/migrations/20260526-iap-foundation.ts +105 -0
- package/api/src/store/models/customer.ts +37 -1
- package/api/src/store/models/entitlement-grant.ts +118 -0
- package/api/src/store/models/entitlement-product.ts +48 -0
- package/api/src/store/models/entitlement.ts +86 -0
- package/api/src/store/models/index.ts +9 -0
- package/api/src/store/models/invoice.ts +20 -0
- package/api/src/store/models/payment-method.ts +66 -1
- package/api/src/store/models/price.ts +23 -14
- package/api/src/store/models/refund.ts +10 -0
- package/api/src/store/models/subscription.ts +14 -0
- package/api/src/store/models/types.ts +32 -0
- package/api/tests/integrations/app-store/client.spec.ts +335 -0
- package/api/tests/integrations/app-store/handlers.spec.ts +480 -0
- package/api/tests/integrations/app-store/notifications.spec.ts +381 -0
- package/api/tests/integrations/app-store/signed-data-verifier.spec.ts +72 -0
- package/api/tests/integrations/app-store/webhook-routing.spec.ts +27 -0
- package/api/tests/integrations/google-play/handlers.spec.ts +341 -0
- package/api/tests/integrations/google-play/verify.spec.ts +215 -0
- package/api/tests/integrations/iap-reconcile.spec.ts +237 -0
- package/api/tests/libs/entitlement.spec.ts +347 -0
- package/api/tests/libs/wallet-migration.spec.ts +4 -4
- package/api/tests/queues/credit-consume-batch.spec.ts +5 -2
- package/api/tests/queues/credit-consume.spec.ts +8 -4
- package/api/tests/routes/credit-grants.spec.ts +1 -0
- package/blocklet.yml +1 -1
- package/cloudflare/MIGRATION-CHALLENGES.md +676 -0
- package/cloudflare/MIGRATION-RUNBOOK.md +777 -0
- package/cloudflare/README.md +499 -0
- package/cloudflare/STAGING-MIGRATION-GUIDE.md +602 -0
- package/cloudflare/build.ts +151 -0
- package/cloudflare/did-connect-auth.ts +527 -0
- package/cloudflare/docs/2026-04-22-sdk-1.30.9-upgrade-retro.md +324 -0
- package/cloudflare/docs/2026-04-24-queue-ops-followup.md +218 -0
- package/cloudflare/docs/cf-queues-ops-alert-analysis.md +663 -0
- package/cloudflare/docs/cf-workers-local-dev-and-fixes.md +284 -0
- package/cloudflare/docs/cleanup-tasks-2026-05.md +62 -0
- package/cloudflare/docs/payment-kit-platform-analysis-2026-04-20.md +354 -0
- package/cloudflare/frontend-shims/buffer-polyfill.ts +9 -0
- package/cloudflare/frontend-shims/js-sdk.ts +43 -0
- package/cloudflare/frontend-shims/mime-types.ts +46 -0
- package/cloudflare/frontend-shims/session.ts +24 -0
- package/cloudflare/frontend-shims/vite-plugin-noop.ts +6 -0
- package/cloudflare/index.html +40 -0
- package/cloudflare/migrate-to-d1.js +252 -0
- package/cloudflare/migrations/0001_initial_schema.sql +82 -0
- package/cloudflare/migrations/0002_indexes.sql +75 -0
- package/cloudflare/migrations/0003_locks_and_constraints.sql +18 -0
- package/cloudflare/migrations/0004_iap_foundation.sql +72 -0
- package/cloudflare/migrations/0005_iap_tenant_backfill.sql +112 -0
- package/cloudflare/run-build.js +391 -0
- package/cloudflare/scripts/test-decrypt.js +102 -0
- package/cloudflare/shims/arcblock-ws.ts +20 -0
- package/cloudflare/shims/axios-http-adapter.ts +4 -0
- package/cloudflare/shims/axios-lite.ts +117 -0
- package/cloudflare/shims/blocklet-sdk/auth-service.ts +33 -0
- package/cloudflare/shims/blocklet-sdk/cdn.ts +3 -0
- package/cloudflare/shims/blocklet-sdk/component-api.ts +35 -0
- package/cloudflare/shims/blocklet-sdk/component.ts +18 -0
- package/cloudflare/shims/blocklet-sdk/config.ts +8 -0
- package/cloudflare/shims/blocklet-sdk/did.ts +14 -0
- package/cloudflare/shims/blocklet-sdk/env.ts +12 -0
- package/cloudflare/shims/blocklet-sdk/eventbus.ts +3 -0
- package/cloudflare/shims/blocklet-sdk/fallback.ts +3 -0
- package/cloudflare/shims/blocklet-sdk/index.ts +11 -0
- package/cloudflare/shims/blocklet-sdk/logger.ts +11 -0
- package/cloudflare/shims/blocklet-sdk/middlewares.ts +15 -0
- package/cloudflare/shims/blocklet-sdk/notification.ts +11 -0
- package/cloudflare/shims/blocklet-sdk/security.ts +53 -0
- package/cloudflare/shims/blocklet-sdk/session.ts +8 -0
- package/cloudflare/shims/blocklet-sdk/verify-session.ts +44 -0
- package/cloudflare/shims/blocklet-sdk/verify-sign.ts +38 -0
- package/cloudflare/shims/blocklet-sdk/wallet-authenticator.ts +3 -0
- package/cloudflare/shims/blocklet-sdk/wallet-handler.ts +6 -0
- package/cloudflare/shims/blocklet-sdk/wallet.ts +103 -0
- package/cloudflare/shims/cookie-parser.ts +3 -0
- package/cloudflare/shims/cors.ts +21 -0
- package/cloudflare/shims/cron.ts +189 -0
- package/cloudflare/shims/crypto-js-warn.ts +7 -0
- package/cloudflare/shims/did-space-js.ts +17 -0
- package/cloudflare/shims/did-space.ts +11 -0
- package/cloudflare/shims/error.ts +18 -0
- package/cloudflare/shims/express-compat/index.ts +80 -0
- package/cloudflare/shims/express-compat/types.ts +41 -0
- package/cloudflare/shims/fastq.ts +105 -0
- package/cloudflare/shims/lock.ts +115 -0
- package/cloudflare/shims/mime-types.ts +56 -0
- package/cloudflare/shims/nedb-storage.ts +9 -0
- package/cloudflare/shims/node-child-process.ts +9 -0
- package/cloudflare/shims/node-fs.ts +20 -0
- package/cloudflare/shims/node-http.ts +13 -0
- package/cloudflare/shims/node-https.ts +4 -0
- package/cloudflare/shims/node-misc.ts +15 -0
- package/cloudflare/shims/node-net.ts +8 -0
- package/cloudflare/shims/node-os.ts +14 -0
- package/cloudflare/shims/node-tty.ts +8 -0
- package/cloudflare/shims/node-zlib.ts +17 -0
- package/cloudflare/shims/noop.ts +26 -0
- package/cloudflare/shims/payment-vendor.ts +14 -0
- package/cloudflare/shims/querystring.ts +12 -0
- package/cloudflare/shims/queue.ts +611 -0
- package/cloudflare/shims/rolldown-runtime.ts +43 -0
- package/cloudflare/shims/sequelize-d1/datatypes.ts +24 -0
- package/cloudflare/shims/sequelize-d1/helpers.ts +46 -0
- package/cloudflare/shims/sequelize-d1/index.ts +34 -0
- package/cloudflare/shims/sequelize-d1/model.ts +1176 -0
- package/cloudflare/shims/sequelize-d1/operators.ts +306 -0
- package/cloudflare/shims/sequelize-d1/retry.ts +85 -0
- package/cloudflare/shims/sequelize-d1/sequelize-class.ts +119 -0
- package/cloudflare/shims/sequelize-d1/timing.ts +81 -0
- package/cloudflare/shims/sequelize-d1/types.ts +35 -0
- package/cloudflare/shims/stripe-cf.ts +29 -0
- package/cloudflare/shims/ws-lite.ts +103 -0
- package/cloudflare/shims/xss.ts +3 -0
- package/cloudflare/tests/shims/cron.spec.ts +210 -0
- package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +87 -0
- package/cloudflare/tests/shims/queue-scheduled.spec.ts +186 -0
- package/cloudflare/vite.config.ts +162 -0
- package/cloudflare/worker.ts +1608 -0
- package/cloudflare/wrangler.json +63 -0
- package/cloudflare/wrangler.jsonc +75 -0
- package/cloudflare/wrangler.staging.json +67 -0
- package/cloudflare/wrangler.toml +28 -0
- package/jest.config.js +4 -12
- package/package.json +30 -22
- package/scripts/seed-google-play.ts +79 -0
- package/src/app.tsx +62 -4
- package/src/components/customer/link.tsx +9 -13
- package/src/components/customer/notification-preference.tsx +3 -2
- package/src/components/filter-toolbar.tsx +4 -0
- package/src/components/invoice/list.tsx +9 -1
- package/src/components/invoice-pdf/utils.ts +2 -1
- package/src/components/layout/admin.tsx +39 -5
- package/src/components/layout/user-cf.tsx +77 -0
- package/src/components/payment-intent/actions.tsx +23 -3
- package/src/components/payment-method/app-store.tsx +103 -0
- package/src/components/payment-method/form.tsx +7 -1
- package/src/components/payment-method/google-play.tsx +85 -0
- package/src/components/safe-did-address.tsx +75 -0
- package/src/components/subscription/list.tsx +20 -0
- package/src/libs/patch-user-card.ts +25 -0
- package/src/libs/util.ts +5 -7
- package/src/locales/en.tsx +63 -0
- package/src/locales/zh.tsx +63 -0
- package/src/pages/admin/billing/meter-events/index.tsx +4 -0
- package/src/pages/admin/billing/subscriptions/detail.tsx +80 -0
- package/src/pages/admin/customers/customers/detail.tsx +8 -2
- package/src/pages/admin/customers/customers/index.tsx +2 -2
- package/src/pages/admin/overview.tsx +3 -1
- package/src/pages/admin/settings/payment-methods/create.tsx +12 -0
- package/src/pages/admin/settings/payment-methods/index.tsx +1 -1
- package/src/pages/customer/subscription/detail.tsx +4 -4
- package/tsconfig.api.json +1 -6
- package/tsconfig.json +3 -4
- package/tsconfig.types.json +2 -1
- package/vite.config.ts +6 -1
|
@@ -1,13 +1,36 @@
|
|
|
1
1
|
/* eslint-disable react-hooks/exhaustive-deps */
|
|
2
|
-
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
2
|
import { PaymentProvider } from '@blocklet/payment-react';
|
|
4
3
|
import Dashboard from '@blocklet/ui-react/lib/Dashboard';
|
|
5
4
|
import { styled } from '@mui/system';
|
|
6
5
|
import { useEffect } from 'react';
|
|
7
6
|
|
|
8
|
-
import {
|
|
7
|
+
import { Box, CircularProgress } from '@mui/material';
|
|
9
8
|
import { useSessionContext } from '../../contexts/session';
|
|
10
9
|
|
|
10
|
+
function LayoutLoading() {
|
|
11
|
+
return (
|
|
12
|
+
<Box
|
|
13
|
+
sx={{
|
|
14
|
+
minHeight: '100vh',
|
|
15
|
+
display: 'flex',
|
|
16
|
+
alignItems: 'center',
|
|
17
|
+
justifyContent: 'center',
|
|
18
|
+
}}>
|
|
19
|
+
<CircularProgress />
|
|
20
|
+
</Box>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// window.blocklet is bootstrapped synchronously in public/index.html (CF) but
|
|
25
|
+
// the full payload is overlaid from /__blocklet__.js. Don't render children
|
|
26
|
+
// (which may fire API requests on mount) until the critical fields are in
|
|
27
|
+
// place — otherwise requests go out against a half-initialized config and
|
|
28
|
+
// can cascade into auth failures / unintended redirects.
|
|
29
|
+
function isBlockletReady() {
|
|
30
|
+
const b = (window as any).blocklet;
|
|
31
|
+
return !!(b && b.appPid && b.prefix);
|
|
32
|
+
}
|
|
33
|
+
|
|
11
34
|
const Root = styled(Dashboard)<{ padding: string }>`
|
|
12
35
|
width: 100%;
|
|
13
36
|
background-color: ${({ theme }) => theme.palette.background.default};
|
|
@@ -62,12 +85,11 @@ const Root = styled(Dashboard)<{ padding: string }>`
|
|
|
62
85
|
`;
|
|
63
86
|
|
|
64
87
|
export default function Layout(props: any) {
|
|
65
|
-
const { t } = useLocaleContext();
|
|
66
88
|
const { session, connectApi, events } = useSessionContext();
|
|
67
89
|
|
|
68
90
|
useEffect(() => {
|
|
69
91
|
events.once('logout', () => {
|
|
70
|
-
window.location.href = '/';
|
|
92
|
+
window.location.href = window?.blocklet?.prefix || '/';
|
|
71
93
|
});
|
|
72
94
|
}, []);
|
|
73
95
|
|
|
@@ -78,6 +100,14 @@ export default function Layout(props: any) {
|
|
|
78
100
|
}
|
|
79
101
|
}, [session.initialized]);
|
|
80
102
|
|
|
103
|
+
// Show a loading state — not a "Redirecting..." text and not a blank page —
|
|
104
|
+
// while either window.blocklet or the session is still resolving. Admin
|
|
105
|
+
// pages fire authenticated API requests on mount, so children must not
|
|
106
|
+
// render until both are fully in place.
|
|
107
|
+
if (!isBlockletReady() || !session.initialized) {
|
|
108
|
+
return <LayoutLoading />;
|
|
109
|
+
}
|
|
110
|
+
|
|
81
111
|
if (session.user) {
|
|
82
112
|
return (
|
|
83
113
|
<PaymentProvider session={session} connect={connectApi}>
|
|
@@ -86,5 +116,9 @@ export default function Layout(props: any) {
|
|
|
86
116
|
);
|
|
87
117
|
}
|
|
88
118
|
|
|
89
|
-
|
|
119
|
+
// Session initialized but not logged in: the effect above triggers a
|
|
120
|
+
// redirect-based login. Keep showing the loading state (instead of a
|
|
121
|
+
// "Redirecting..." text) so the user isn't confused when the actual
|
|
122
|
+
// navigation hasn't happened yet.
|
|
123
|
+
return <LayoutLoading />;
|
|
90
124
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/* eslint-disable react-hooks/exhaustive-deps */
|
|
2
|
+
import { PaymentProvider } from '@blocklet/payment-react';
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
import { Header } from '@blocklet/ui-react';
|
|
5
|
+
import { Box, CircularProgress } from '@mui/material';
|
|
6
|
+
import { useSessionContext } from '../../contexts/session';
|
|
7
|
+
|
|
8
|
+
function LayoutLoading() {
|
|
9
|
+
return (
|
|
10
|
+
<Box
|
|
11
|
+
sx={{
|
|
12
|
+
minHeight: '100vh',
|
|
13
|
+
display: 'flex',
|
|
14
|
+
alignItems: 'center',
|
|
15
|
+
justifyContent: 'center',
|
|
16
|
+
}}>
|
|
17
|
+
<CircularProgress />
|
|
18
|
+
</Box>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// window.blocklet is bootstrapped synchronously in public/index.html but the
|
|
23
|
+
// full payload is overlaid from /__blocklet__.js. Treat the layout as "not
|
|
24
|
+
// ready" until the overlay has populated the fields the downstream components
|
|
25
|
+
// (Header, PaymentProvider) rely on, so we never render children against a
|
|
26
|
+
// half-initialized blocklet config.
|
|
27
|
+
function isBlockletReady() {
|
|
28
|
+
const b = (window as any).blocklet;
|
|
29
|
+
return !!(b && b.appPid && b.prefix);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* CF Workers layout for customer pages.
|
|
34
|
+
* Uses Dashboard component for consistent header + SessionUser dropdown.
|
|
35
|
+
* Adds "My Billing" to the user dropdown via sessionManagerProps.menu.
|
|
36
|
+
*/
|
|
37
|
+
export default function UserLayoutCF(props: any) {
|
|
38
|
+
const { session, connectApi } = useSessionContext();
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (session.initialized && !session.user) {
|
|
42
|
+
session.login(() => {});
|
|
43
|
+
}
|
|
44
|
+
}, [session.initialized]);
|
|
45
|
+
|
|
46
|
+
// Show a loading state (not a blank page, not a redirect) while either
|
|
47
|
+
// window.blocklet or the session is still being resolved. Any child that
|
|
48
|
+
// fires API calls on mount (useRequest / useEffect) must not run until
|
|
49
|
+
// both are in place — otherwise the request goes out without the right
|
|
50
|
+
// auth cookies/headers and may cascade into a redirect.
|
|
51
|
+
if (!isBlockletReady() || !session.initialized) {
|
|
52
|
+
return <LayoutLoading />;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Session initialized but not logged in: the effect above will trigger the
|
|
56
|
+
// DID Connect login modal. Keep showing the loading state instead of a
|
|
57
|
+
// blank screen so the user doesn't perceive it as "bounced to home".
|
|
58
|
+
if (!session.user) {
|
|
59
|
+
return <LayoutLoading />;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<PaymentProvider session={session} connect={connectApi}>
|
|
64
|
+
<Header
|
|
65
|
+
meta={undefined}
|
|
66
|
+
addons={undefined}
|
|
67
|
+
sessionManagerProps={undefined}
|
|
68
|
+
homeLink={undefined}
|
|
69
|
+
theme={undefined}
|
|
70
|
+
hideNavMenu={undefined}
|
|
71
|
+
maxWidth={false}
|
|
72
|
+
sx={{ borderBottom: '1px solid', borderColor: 'divider' }}
|
|
73
|
+
/>
|
|
74
|
+
<Box sx={{ flex: 1, maxWidth: 1200, mx: 'auto', px: 3, py: 3, width: '100%' }}>{props.children}</Box>
|
|
75
|
+
</PaymentProvider>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
2
|
import Toast from '@arcblock/ux/lib/Toast';
|
|
3
|
-
import { ConfirmDialog, api, formatAmountPrecisionLimit,
|
|
3
|
+
import { ConfirmDialog, api, formatAmountPrecisionLimit, formatError } from '@blocklet/payment-react';
|
|
4
4
|
import type { TPaymentIntentExpanded } from '@blocklet/payment-types';
|
|
5
|
+
import { fromUnitToToken } from '@ocap/util';
|
|
5
6
|
import { useRequest, useSetState } from 'ahooks';
|
|
6
7
|
import { useNavigate } from 'react-router-dom';
|
|
7
8
|
import type { LiteralUnion } from 'type-fest';
|
|
@@ -21,6 +22,25 @@ import { useState } from 'react';
|
|
|
21
22
|
import Actions from '../actions';
|
|
22
23
|
import ClickBoundary from '../click-boundary';
|
|
23
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Format the refundable amount the server returned (raw BN string) into a human-readable
|
|
27
|
+
* token string by **flooring** to `precision` decimal places — never rounding up.
|
|
28
|
+
*
|
|
29
|
+
* `formatBNStr` rounds (via toLocaleString / toFixed), which can produce a value whose
|
|
30
|
+
* `fromTokenToUnit` round-trip exceeds the original raw amount by sub-cent dust and gets
|
|
31
|
+
* rejected by the server with "refund amount exceeds the available amount". Flooring keeps
|
|
32
|
+
* the round-trip ≤ original. The trailing-zero trim mirrors `formatBNStr` so the input
|
|
33
|
+
* field looks identical for amounts that don't get truncated.
|
|
34
|
+
*/
|
|
35
|
+
function formatRefundableAmount(raw: string | undefined, decimals: number, precision: number = 6): string {
|
|
36
|
+
if (!raw) return '0';
|
|
37
|
+
const tokenAmount = fromUnitToToken(raw, decimals);
|
|
38
|
+
const [whole = '0', frac = ''] = tokenAmount.split('.');
|
|
39
|
+
if (precision <= 0) return whole;
|
|
40
|
+
const truncated = frac.substring(0, precision).replace(/0+$/, '');
|
|
41
|
+
return truncated ? `${whole}.${truncated}` : whole;
|
|
42
|
+
}
|
|
43
|
+
|
|
24
44
|
type Props = {
|
|
25
45
|
data: TPaymentIntentExpanded;
|
|
26
46
|
variant?: LiteralUnion<'compact' | 'normal', string>;
|
|
@@ -185,7 +205,7 @@ export function PaymentIntentActionsInner({ data, variant = 'compact', onChange
|
|
|
185
205
|
},
|
|
186
206
|
{
|
|
187
207
|
onSuccess: (res: any) => {
|
|
188
|
-
const amount =
|
|
208
|
+
const amount = formatRefundableAmount(res?.amount, data.paymentCurrency.decimal);
|
|
189
209
|
setRefundMaxAmount(amount);
|
|
190
210
|
},
|
|
191
211
|
manual: true,
|
|
@@ -218,7 +238,7 @@ export function PaymentIntentActionsInner({ data, variant = 'compact', onChange
|
|
|
218
238
|
handler: () => {
|
|
219
239
|
runRefundAmountAsync().then((res) => {
|
|
220
240
|
reset();
|
|
221
|
-
const curAmount =
|
|
241
|
+
const curAmount = formatRefundableAmount(res?.amount, data.paymentCurrency.decimal);
|
|
222
242
|
if (Number(curAmount) <= 0) {
|
|
223
243
|
Toast.info(t('admin.paymentIntent.refundForm.empty'));
|
|
224
244
|
return;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
|
+
import { FormInput } from '@blocklet/payment-react';
|
|
3
|
+
import { Stack, ToggleButton, ToggleButtonGroup, Typography } from '@mui/material';
|
|
4
|
+
import { Controller, useFormContext } from 'react-hook-form';
|
|
5
|
+
|
|
6
|
+
export default function AppStoreMethodForm({ checkDisabled }: { checkDisabled: (key: string) => boolean }) {
|
|
7
|
+
const { t } = useLocaleContext();
|
|
8
|
+
const { control } = useFormContext();
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<>
|
|
12
|
+
<FormInput
|
|
13
|
+
name="name"
|
|
14
|
+
type="text"
|
|
15
|
+
rules={{ required: true }}
|
|
16
|
+
label={t('admin.paymentMethod.name.label')}
|
|
17
|
+
placeholder={t('admin.paymentMethod.name.tip')}
|
|
18
|
+
disabled={checkDisabled('name')}
|
|
19
|
+
inputProps={{ maxLength: 32 }}
|
|
20
|
+
/>
|
|
21
|
+
<FormInput
|
|
22
|
+
name="description"
|
|
23
|
+
type="text"
|
|
24
|
+
rules={{ required: true }}
|
|
25
|
+
label={t('admin.paymentMethod.description.label')}
|
|
26
|
+
placeholder={t('admin.paymentMethod.description.tip')}
|
|
27
|
+
inputProps={{ maxLength: 255 }}
|
|
28
|
+
/>
|
|
29
|
+
<FormInput
|
|
30
|
+
name="settings.app_store.bundle_id"
|
|
31
|
+
type="text"
|
|
32
|
+
rules={{ required: true }}
|
|
33
|
+
label={t('admin.paymentMethod.app_store.bundle_id.label')}
|
|
34
|
+
placeholder={t('admin.paymentMethod.app_store.bundle_id.tip')}
|
|
35
|
+
disabled={checkDisabled('settings.app_store.bundle_id')}
|
|
36
|
+
/>
|
|
37
|
+
|
|
38
|
+
<Stack spacing={0.5}>
|
|
39
|
+
<Typography variant="body2">{t('admin.paymentMethod.app_store.environment.label')}</Typography>
|
|
40
|
+
<Controller
|
|
41
|
+
name="settings.app_store.environment"
|
|
42
|
+
control={control}
|
|
43
|
+
rules={{ required: true }}
|
|
44
|
+
render={({ field }) => (
|
|
45
|
+
<ToggleButtonGroup
|
|
46
|
+
{...field}
|
|
47
|
+
exclusive
|
|
48
|
+
disabled={checkDisabled('settings.app_store.environment')}
|
|
49
|
+
onChange={(_, value: string | null) => {
|
|
50
|
+
if (value !== null) field.onChange(value);
|
|
51
|
+
}}>
|
|
52
|
+
<ToggleButton value="production">
|
|
53
|
+
{t('admin.paymentMethod.app_store.environment.production')}
|
|
54
|
+
</ToggleButton>
|
|
55
|
+
<ToggleButton value="sandbox">{t('admin.paymentMethod.app_store.environment.sandbox')}</ToggleButton>
|
|
56
|
+
</ToggleButtonGroup>
|
|
57
|
+
)}
|
|
58
|
+
/>
|
|
59
|
+
<Typography variant="caption" color="text.secondary">
|
|
60
|
+
{t('admin.paymentMethod.app_store.environment.tip')}
|
|
61
|
+
</Typography>
|
|
62
|
+
</Stack>
|
|
63
|
+
|
|
64
|
+
<FormInput
|
|
65
|
+
name="settings.app_store.shared_secret"
|
|
66
|
+
type="password"
|
|
67
|
+
label={t('admin.paymentMethod.app_store.shared_secret.label')}
|
|
68
|
+
placeholder={t('admin.paymentMethod.app_store.shared_secret.tip')}
|
|
69
|
+
disabled={checkDisabled('settings.app_store.shared_secret')}
|
|
70
|
+
/>
|
|
71
|
+
|
|
72
|
+
<Typography variant="subtitle2" sx={{ mt: 2 }}>
|
|
73
|
+
{t('admin.paymentMethod.app_store.serverApi.heading')}
|
|
74
|
+
</Typography>
|
|
75
|
+
<Typography variant="caption" color="text.secondary" sx={{ mt: -1, display: 'block' }}>
|
|
76
|
+
{t('admin.paymentMethod.app_store.serverApi.tip')}
|
|
77
|
+
</Typography>
|
|
78
|
+
<FormInput
|
|
79
|
+
name="settings.app_store.issuer_id"
|
|
80
|
+
type="text"
|
|
81
|
+
label={t('admin.paymentMethod.app_store.issuer_id.label')}
|
|
82
|
+
placeholder={t('admin.paymentMethod.app_store.issuer_id.tip')}
|
|
83
|
+
disabled={checkDisabled('settings.app_store.issuer_id')}
|
|
84
|
+
/>
|
|
85
|
+
<FormInput
|
|
86
|
+
name="settings.app_store.key_id"
|
|
87
|
+
type="text"
|
|
88
|
+
label={t('admin.paymentMethod.app_store.key_id.label')}
|
|
89
|
+
placeholder={t('admin.paymentMethod.app_store.key_id.tip')}
|
|
90
|
+
disabled={checkDisabled('settings.app_store.key_id')}
|
|
91
|
+
/>
|
|
92
|
+
<FormInput
|
|
93
|
+
name="settings.app_store.private_key_pem"
|
|
94
|
+
type="text"
|
|
95
|
+
multiline
|
|
96
|
+
rows={5}
|
|
97
|
+
label={t('admin.paymentMethod.app_store.private_key_pem.label')}
|
|
98
|
+
placeholder={t('admin.paymentMethod.app_store.private_key_pem.tip')}
|
|
99
|
+
disabled={checkDisabled('settings.app_store.private_key_pem')}
|
|
100
|
+
/>
|
|
101
|
+
</>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -3,11 +3,13 @@ import { Stack, ToggleButton, ToggleButtonGroup, Typography } from '@mui/materia
|
|
|
3
3
|
import { styled } from '@mui/system';
|
|
4
4
|
import { Controller, useFormContext, useWatch } from 'react-hook-form';
|
|
5
5
|
|
|
6
|
+
import AppStoreMethodForm from './app-store';
|
|
6
7
|
import ArcBlockMethodForm from './arcblock';
|
|
8
|
+
import BaseMethodForm from './base';
|
|
7
9
|
import BitcoinMethodForm from './bitcoin';
|
|
8
10
|
import EthereumMethodForm from './ethereum';
|
|
11
|
+
import GooglePlayMethodForm from './google-play';
|
|
9
12
|
import StripeMethodForm from './stripe';
|
|
10
|
-
import BaseMethodForm from './base';
|
|
11
13
|
|
|
12
14
|
export default function PaymentMethodForm({
|
|
13
15
|
action = 'create',
|
|
@@ -47,6 +49,8 @@ export default function PaymentMethodForm({
|
|
|
47
49
|
<ToggleButton value="stripe">Stripe</ToggleButton>
|
|
48
50
|
<ToggleButton value="ethereum">Ethereum</ToggleButton>
|
|
49
51
|
<ToggleButton value="base">Base</ToggleButton>
|
|
52
|
+
<ToggleButton value="google_play">Google Play</ToggleButton>
|
|
53
|
+
<ToggleButton value="app_store">App Store</ToggleButton>
|
|
50
54
|
<ToggleButton value="bitcoin" disabled>
|
|
51
55
|
Bitcoin
|
|
52
56
|
</ToggleButton>
|
|
@@ -60,6 +64,8 @@ export default function PaymentMethodForm({
|
|
|
60
64
|
{type === 'arcblock' && <ArcBlockMethodForm checkDisabled={checkDisabled} />}
|
|
61
65
|
{type === 'ethereum' && <EthereumMethodForm checkDisabled={checkDisabled} />}
|
|
62
66
|
{type === 'base' && <BaseMethodForm checkDisabled={checkDisabled} />}
|
|
67
|
+
{type === 'google_play' && <GooglePlayMethodForm checkDisabled={checkDisabled} />}
|
|
68
|
+
{type === 'app_store' && <AppStoreMethodForm checkDisabled={checkDisabled} />}
|
|
63
69
|
{type === 'bitcoin' && <BitcoinMethodForm checkDisabled={checkDisabled} />}
|
|
64
70
|
</Root>
|
|
65
71
|
);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
|
+
import { FormInput } from '@blocklet/payment-react';
|
|
3
|
+
import { useFormContext, useWatch } from 'react-hook-form';
|
|
4
|
+
import { Alert, Typography } from '@mui/material';
|
|
5
|
+
|
|
6
|
+
export default function GooglePlayMethodForm({ checkDisabled }: { checkDisabled: (key: string) => boolean }) {
|
|
7
|
+
const { t } = useLocaleContext();
|
|
8
|
+
const { control } = useFormContext();
|
|
9
|
+
const serviceAccountJson = useWatch({ control, name: 'settings.google_play.service_account_json' }) as
|
|
10
|
+
| string
|
|
11
|
+
| undefined;
|
|
12
|
+
|
|
13
|
+
// Quick client-side JSON sanity check — the server re-validates and decrypts.
|
|
14
|
+
let parseError: string | null = null;
|
|
15
|
+
let clientEmail: string | null = null;
|
|
16
|
+
if (serviceAccountJson) {
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(serviceAccountJson);
|
|
19
|
+
if (!parsed.client_email || !parsed.private_key) {
|
|
20
|
+
parseError = t('admin.paymentMethod.google_play.service_account_json.missingFields');
|
|
21
|
+
} else {
|
|
22
|
+
clientEmail = parsed.client_email;
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
parseError = t('admin.paymentMethod.google_play.service_account_json.invalidJson');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<>
|
|
31
|
+
<FormInput
|
|
32
|
+
name="name"
|
|
33
|
+
type="text"
|
|
34
|
+
rules={{ required: true }}
|
|
35
|
+
label={t('admin.paymentMethod.name.label')}
|
|
36
|
+
placeholder={t('admin.paymentMethod.name.tip')}
|
|
37
|
+
disabled={checkDisabled('name')}
|
|
38
|
+
inputProps={{ maxLength: 32 }}
|
|
39
|
+
/>
|
|
40
|
+
<FormInput
|
|
41
|
+
name="description"
|
|
42
|
+
type="text"
|
|
43
|
+
rules={{ required: true }}
|
|
44
|
+
label={t('admin.paymentMethod.description.label')}
|
|
45
|
+
placeholder={t('admin.paymentMethod.description.tip')}
|
|
46
|
+
inputProps={{ maxLength: 255 }}
|
|
47
|
+
/>
|
|
48
|
+
<FormInput
|
|
49
|
+
name="settings.google_play.package_name"
|
|
50
|
+
type="text"
|
|
51
|
+
rules={{ required: true }}
|
|
52
|
+
label={t('admin.paymentMethod.google_play.package_name.label')}
|
|
53
|
+
placeholder={t('admin.paymentMethod.google_play.package_name.tip')}
|
|
54
|
+
disabled={checkDisabled('settings.google_play.package_name')}
|
|
55
|
+
/>
|
|
56
|
+
<FormInput
|
|
57
|
+
name="settings.google_play.service_account_json"
|
|
58
|
+
type="text"
|
|
59
|
+
multiline
|
|
60
|
+
rows={6}
|
|
61
|
+
rules={{ required: true }}
|
|
62
|
+
label={t('admin.paymentMethod.google_play.service_account_json.label')}
|
|
63
|
+
placeholder={t('admin.paymentMethod.google_play.service_account_json.tip')}
|
|
64
|
+
disabled={checkDisabled('settings.google_play.service_account_json')}
|
|
65
|
+
/>
|
|
66
|
+
{parseError && (
|
|
67
|
+
<Alert severity="error" sx={{ mt: -1 }}>
|
|
68
|
+
{parseError}
|
|
69
|
+
</Alert>
|
|
70
|
+
)}
|
|
71
|
+
{clientEmail && (
|
|
72
|
+
<Typography variant="caption" color="text.secondary" sx={{ mt: -1, display: 'block' }}>
|
|
73
|
+
{t('admin.paymentMethod.google_play.service_account_json.detectedClient')}: {clientEmail}
|
|
74
|
+
</Typography>
|
|
75
|
+
)}
|
|
76
|
+
<FormInput
|
|
77
|
+
name="settings.google_play.pubsub_topic_name"
|
|
78
|
+
type="text"
|
|
79
|
+
label={t('admin.paymentMethod.google_play.pubsub_topic_name.label')}
|
|
80
|
+
placeholder={t('admin.paymentMethod.google_play.pubsub_topic_name.tip')}
|
|
81
|
+
disabled={checkDisabled('settings.google_play.pubsub_topic_name')}
|
|
82
|
+
/>
|
|
83
|
+
</>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import DidAddress from '@arcblock/ux/lib/DID';
|
|
2
|
+
import { Typography } from '@mui/material';
|
|
3
|
+
import { Component, type ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Matches DIDs that @arcblock/did-motif can parse:
|
|
7
|
+
* z-prefixed base58 DID (z1..., z2..., zNK...) — standard ArcBlock format
|
|
8
|
+
* did:method:identifier — W3C DID format
|
|
9
|
+
*/
|
|
10
|
+
function isValidDid(did: unknown): did is string {
|
|
11
|
+
return typeof did === 'string' && /^(z[0-9a-zA-Z]{20,}|did:[a-z]+:[A-Za-z0-9]+)/.test(did);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ErrorBoundaryProps {
|
|
15
|
+
fallback: ReactNode;
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Inline error boundary — catches DIDParsingError (or any render error) from DidAddress
|
|
21
|
+
* and shows a fallback instead of crashing the whole page.
|
|
22
|
+
*/
|
|
23
|
+
class DidErrorBoundary extends Component<ErrorBoundaryProps, { hasError: boolean }> {
|
|
24
|
+
// eslint-disable-next-line react/state-in-constructor
|
|
25
|
+
override state = { hasError: false };
|
|
26
|
+
|
|
27
|
+
static getDerivedStateFromError() {
|
|
28
|
+
return { hasError: true };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
override componentDidCatch() {
|
|
32
|
+
// Swallow — DIDParsingError is non-fatal, we just show a fallback
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
override render() {
|
|
36
|
+
if (this.state.hasError) return this.props.fallback;
|
|
37
|
+
return this.props.children;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SafeDidAddressProps {
|
|
42
|
+
did?: string | null;
|
|
43
|
+
[key: string]: any;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* SafeDidAddress — drop-in replacement for `@arcblock/ux/lib/DID` that:
|
|
48
|
+
* 1. Shows an em-dash placeholder for empty/null DIDs
|
|
49
|
+
* 2. Pre-validates the DID format before rendering
|
|
50
|
+
* 3. Catches any render errors from did-motif via an error boundary
|
|
51
|
+
*
|
|
52
|
+
* Use this anywhere the backing data may have invalid/missing DIDs (e.g. migrated
|
|
53
|
+
* customer records with empty did fields).
|
|
54
|
+
*/
|
|
55
|
+
// eslint-disable-next-line react/require-default-props
|
|
56
|
+
export default function SafeDidAddress({ did, ...rest }: SafeDidAddressProps) {
|
|
57
|
+
if (!did || !isValidDid(did)) {
|
|
58
|
+
return (
|
|
59
|
+
<Typography variant="body2" color="text.secondary" component="span">
|
|
60
|
+
{did || '—'}
|
|
61
|
+
</Typography>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<DidErrorBoundary
|
|
67
|
+
fallback={
|
|
68
|
+
<Typography variant="body2" color="text.secondary" component="span">
|
|
69
|
+
{did}
|
|
70
|
+
</Typography>
|
|
71
|
+
}>
|
|
72
|
+
<DidAddress did={did} {...rest} />
|
|
73
|
+
</DidErrorBoundary>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -152,6 +152,26 @@ export default function SubscriptionList({
|
|
|
152
152
|
},
|
|
153
153
|
},
|
|
154
154
|
},
|
|
155
|
+
{
|
|
156
|
+
label: t('admin.subscription.channel'),
|
|
157
|
+
name: 'channel',
|
|
158
|
+
options: {
|
|
159
|
+
filter: true,
|
|
160
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
161
|
+
const item = data.list[index] as TSubscriptionExpanded;
|
|
162
|
+
const channel = (item as any).channel || (item as any).paymentMethod?.type;
|
|
163
|
+
if (!channel) return null;
|
|
164
|
+
let color: 'primary' | 'secondary' | 'default' = 'default';
|
|
165
|
+
if (channel === 'google_play' || channel === 'app_store') color = 'primary';
|
|
166
|
+
else if (channel === 'stripe') color = 'secondary';
|
|
167
|
+
return (
|
|
168
|
+
<Link to={`/admin/billing/${item.id}`}>
|
|
169
|
+
<Status label={channel} color={color as any} />
|
|
170
|
+
</Link>
|
|
171
|
+
);
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
155
175
|
{
|
|
156
176
|
label: t('common.createdAt'),
|
|
157
177
|
name: 'created_at',
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// UserCard (@arcblock/ux) calls sdk.user.getUserPublicInfo per mount without
|
|
2
|
+
// in-flight dedup. Pages that render many rows with the same DID fire N
|
|
3
|
+
// concurrent requests before the first response warms sessionStorage.
|
|
4
|
+
// Patch the bundled SDK singleton so concurrent calls with the same args
|
|
5
|
+
// share one Promise.
|
|
6
|
+
// Deep import into ux's nested js-sdk copy (intentional; that bundle exposes
|
|
7
|
+
// the SDK singleton UserCard actually calls into — patching any other copy
|
|
8
|
+
// of @blocklet/js-sdk doesn't affect UserCard).
|
|
9
|
+
// eslint-disable-next-line import/extensions
|
|
10
|
+
import { getBlockletSDK } from '@arcblock/ux/lib/node_modules/@blocklet/js-sdk/dist/index.js';
|
|
11
|
+
|
|
12
|
+
type PublicInfoArgs = { did?: string; name?: string };
|
|
13
|
+
|
|
14
|
+
const sdk = getBlockletSDK() as { user: { getUserPublicInfo: (args: PublicInfoArgs) => Promise<unknown> } };
|
|
15
|
+
const original = sdk.user.getUserPublicInfo.bind(sdk.user);
|
|
16
|
+
const inflight = new Map<string, Promise<unknown>>();
|
|
17
|
+
|
|
18
|
+
sdk.user.getUserPublicInfo = (args: PublicInfoArgs) => {
|
|
19
|
+
const key = `${args?.did || ''}|${args?.name || ''}`;
|
|
20
|
+
const pending = inflight.get(key);
|
|
21
|
+
if (pending) return pending;
|
|
22
|
+
const promise = original(args).finally(() => inflight.delete(key));
|
|
23
|
+
inflight.set(key, promise);
|
|
24
|
+
return promise;
|
|
25
|
+
};
|
package/src/libs/util.ts
CHANGED
|
@@ -14,8 +14,6 @@ import type {
|
|
|
14
14
|
TProductExpanded,
|
|
15
15
|
TSubscriptionExpanded,
|
|
16
16
|
} from '@blocklet/payment-types';
|
|
17
|
-
import { Hasher } from '@ocap/mcrypto';
|
|
18
|
-
import { hexToNumber } from '@ocap/util';
|
|
19
17
|
import { isEmpty, isObject } from 'lodash';
|
|
20
18
|
import cloneDeep from 'lodash/cloneDeep';
|
|
21
19
|
import isEqual from 'lodash/isEqual';
|
|
@@ -249,11 +247,11 @@ export function getCategoricalColors(specifier: string = '4e79a7f28e2ce1575976b7
|
|
|
249
247
|
|
|
250
248
|
export function stringToColor(str: string) {
|
|
251
249
|
const colors = getCategoricalColors();
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const index = Math.abs(
|
|
250
|
+
let hash = 0;
|
|
251
|
+
for (let i = 0; i < str.length; i += 1) {
|
|
252
|
+
hash = (hash * 31 + str.charCodeAt(i)) | 0;
|
|
253
|
+
}
|
|
254
|
+
const index = Math.abs(hash) % colors.length;
|
|
257
255
|
return colors[index];
|
|
258
256
|
}
|
|
259
257
|
|
package/src/locales/en.tsx
CHANGED
|
@@ -1310,6 +1310,55 @@ export default flat({
|
|
|
1310
1310
|
tip: 'Number of blocks required since transaction execution',
|
|
1311
1311
|
},
|
|
1312
1312
|
},
|
|
1313
|
+
google_play: {
|
|
1314
|
+
package_name: {
|
|
1315
|
+
label: 'Package Name',
|
|
1316
|
+
tip: 'e.g. com.example.app, configured in Play Console',
|
|
1317
|
+
},
|
|
1318
|
+
service_account_json: {
|
|
1319
|
+
label: 'Service Account JSON',
|
|
1320
|
+
tip: 'Paste the full service account credentials JSON downloaded from Google Cloud Console',
|
|
1321
|
+
invalidJson: 'Not valid JSON',
|
|
1322
|
+
missingFields: 'JSON is missing client_email or private_key',
|
|
1323
|
+
detectedClient: 'Detected client',
|
|
1324
|
+
},
|
|
1325
|
+
pubsub_topic_name: {
|
|
1326
|
+
label: 'Pub/Sub Topic (optional)',
|
|
1327
|
+
tip: 'projects/<project-id>/topics/<topic-name>, for receiving RTDN',
|
|
1328
|
+
},
|
|
1329
|
+
},
|
|
1330
|
+
app_store: {
|
|
1331
|
+
bundle_id: {
|
|
1332
|
+
label: 'Bundle ID',
|
|
1333
|
+
tip: 'e.g. com.example.app, configured in App Store Connect',
|
|
1334
|
+
},
|
|
1335
|
+
environment: {
|
|
1336
|
+
label: 'Environment',
|
|
1337
|
+
tip: 'StoreKit 2 JWS environment must match this setting',
|
|
1338
|
+
production: 'Production',
|
|
1339
|
+
sandbox: 'Sandbox',
|
|
1340
|
+
},
|
|
1341
|
+
shared_secret: {
|
|
1342
|
+
label: 'Shared Secret (for StoreKit 1)',
|
|
1343
|
+
tip: 'App-Specific Shared Secret for legacy receipt verification. Not needed for StoreKit 2 JWS. Find it at App Store Connect → App Information → App-Specific Shared Secret',
|
|
1344
|
+
},
|
|
1345
|
+
serverApi: {
|
|
1346
|
+
heading: 'Server API Credentials (optional)',
|
|
1347
|
+
tip: 'Not required for StoreKit 2 JWS verification. Only needed when querying App Store Server API for subscription status. Provide all three or none.',
|
|
1348
|
+
},
|
|
1349
|
+
issuer_id: {
|
|
1350
|
+
label: 'Issuer ID',
|
|
1351
|
+
tip: 'App Store Connect API Issuer ID',
|
|
1352
|
+
},
|
|
1353
|
+
key_id: {
|
|
1354
|
+
label: 'Key ID',
|
|
1355
|
+
tip: 'App Store Connect API Key ID',
|
|
1356
|
+
},
|
|
1357
|
+
private_key_pem: {
|
|
1358
|
+
label: 'Private Key (.p8 contents)',
|
|
1359
|
+
tip: 'Paste the contents of the .p8 file downloaded from App Store Connect',
|
|
1360
|
+
},
|
|
1361
|
+
},
|
|
1313
1362
|
},
|
|
1314
1363
|
paymentCurrency: {
|
|
1315
1364
|
name: 'Payment Currency',
|
|
@@ -1484,7 +1533,19 @@ export default flat({
|
|
|
1484
1533
|
attention: 'Past due subscriptions',
|
|
1485
1534
|
product: 'Product',
|
|
1486
1535
|
collectionMethod: 'Billing',
|
|
1536
|
+
channel: 'Channel',
|
|
1487
1537
|
currentPeriod: 'Current Period',
|
|
1538
|
+
iap: {
|
|
1539
|
+
googlePlayTitle: 'Google Play Purchase',
|
|
1540
|
+
appStoreTitle: 'App Store Purchase',
|
|
1541
|
+
purchaseToken: 'Purchase Token',
|
|
1542
|
+
orderId: 'Order ID',
|
|
1543
|
+
productId: 'Product ID',
|
|
1544
|
+
originalTransactionId: 'Original Transaction ID',
|
|
1545
|
+
transactionId: 'Transaction ID',
|
|
1546
|
+
expiryTime: 'Expires At',
|
|
1547
|
+
environment: 'Environment',
|
|
1548
|
+
},
|
|
1488
1549
|
trialingPeriod: 'Trial Period',
|
|
1489
1550
|
trialEnd: 'Trial ends {prefix} {date}',
|
|
1490
1551
|
willEnd: 'Will end {prefix} {date}',
|
|
@@ -1605,6 +1666,8 @@ export default flat({
|
|
|
1605
1666
|
email: 'Email',
|
|
1606
1667
|
phone: 'Phone',
|
|
1607
1668
|
invoicePrefix: 'Invoice Prefix',
|
|
1669
|
+
googlePlayUuid: 'Google Play UUID',
|
|
1670
|
+
appStoreUuid: 'App Store UUID',
|
|
1608
1671
|
balance: 'Balance ({currency})',
|
|
1609
1672
|
summary: {
|
|
1610
1673
|
refund: 'Refunds',
|