@hyperyai/sdk 1.0.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/LICENSE +13 -0
- package/README.md +391 -0
- package/dist/components/AuthButton.d.ts +51 -0
- package/dist/components/AuthButton.js +60 -0
- package/dist/components/AuthModal.d.ts +20 -0
- package/dist/components/AuthModal.js +62 -0
- package/dist/components/BuyButton.d.ts +47 -0
- package/dist/components/BuyButton.js +74 -0
- package/dist/components/ErrorBoundary.d.ts +13 -0
- package/dist/components/ErrorBoundary.js +24 -0
- package/dist/components/HyperyModals.d.ts +30 -0
- package/dist/components/HyperyModals.js +30 -0
- package/dist/components/InsufficientCreditsAlert.d.ts +11 -0
- package/dist/components/InsufficientCreditsAlert.js +12 -0
- package/dist/components/ModernAuthForm.d.ts +52 -0
- package/dist/components/ModernAuthForm.js +83 -0
- package/dist/components/RestrictionModal.d.ts +43 -0
- package/dist/components/RestrictionModal.js +167 -0
- package/dist/components/SignIn.d.ts +26 -0
- package/dist/components/SignIn.js +47 -0
- package/dist/components/SignInForm.d.ts +41 -0
- package/dist/components/SignInForm.js +86 -0
- package/dist/components/SignUp.d.ts +26 -0
- package/dist/components/SignUp.js +52 -0
- package/dist/components/SpendingLimitAlert.d.ts +12 -0
- package/dist/components/SpendingLimitAlert.js +14 -0
- package/dist/components/UserButton.d.ts +26 -0
- package/dist/components/UserButton.js +35 -0
- package/dist/components/UserProfile.d.ts +22 -0
- package/dist/components/UserProfile.js +26 -0
- package/dist/components/WorkspaceSwitcher.d.ts +29 -0
- package/dist/components/WorkspaceSwitcher.js +66 -0
- package/dist/components/control.d.ts +64 -0
- package/dist/components/control.js +89 -0
- package/dist/components/ui/dialog.d.ts +19 -0
- package/dist/components/ui/dialog.js +53 -0
- package/dist/hooks/index.d.ts +22 -0
- package/dist/hooks/index.js +23 -0
- package/dist/hooks/useBuyerWallet.d.ts +37 -0
- package/dist/hooks/useBuyerWallet.js +66 -0
- package/dist/hooks/useCheckout.d.ts +27 -0
- package/dist/hooks/useCheckout.js +246 -0
- package/dist/hooks/useError.d.ts +19 -0
- package/dist/hooks/useError.js +36 -0
- package/dist/hooks/useMarketplace.d.ts +50 -0
- package/dist/hooks/useMarketplace.js +69 -0
- package/dist/hooks/useMemberships.d.ts +71 -0
- package/dist/hooks/useMemberships.js +138 -0
- package/dist/hooks/useWallet.d.ts +62 -0
- package/dist/hooks/useWallet.js +123 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +52 -0
- package/dist/lib/context.d.ts +50 -0
- package/dist/lib/context.js +409 -0
- package/dist/lib/oauth.d.ts +49 -0
- package/dist/lib/oauth.js +149 -0
- package/dist/lib/parse-error.d.ts +45 -0
- package/dist/lib/parse-error.js +185 -0
- package/dist/lib/parse-stream.d.ts +43 -0
- package/dist/lib/parse-stream.js +89 -0
- package/dist/lib/popup.d.ts +42 -0
- package/dist/lib/popup.js +80 -0
- package/dist/lib/storage.d.ts +43 -0
- package/dist/lib/storage.js +125 -0
- package/dist/lib/utils.d.ts +8 -0
- package/dist/lib/utils.js +11 -0
- package/dist/types/index.d.ts +191 -0
- package/dist/types/index.js +4 -0
- package/package.json +78 -0
- package/src/components/AuthButton.tsx +123 -0
- package/src/components/AuthModal.tsx +240 -0
- package/src/components/BuyButton.tsx +150 -0
- package/src/components/ErrorBoundary.tsx +97 -0
- package/src/components/HyperyModals.tsx +83 -0
- package/src/components/InsufficientCreditsAlert.tsx +73 -0
- package/src/components/ModernAuthForm.tsx +322 -0
- package/src/components/RestrictionModal.tsx +373 -0
- package/src/components/SignIn.tsx +84 -0
- package/src/components/SignInForm.tsx +256 -0
- package/src/components/SignUp.tsx +86 -0
- package/src/components/SpendingLimitAlert.tsx +91 -0
- package/src/components/UserButton.tsx +106 -0
- package/src/components/UserProfile.tsx +106 -0
- package/src/components/WorkspaceSwitcher.tsx +199 -0
- package/src/components/control.tsx +133 -0
- package/src/components/ui/dialog.tsx +151 -0
- package/src/hooks/index.ts +65 -0
- package/src/hooks/useBuyerWallet.ts +117 -0
- package/src/hooks/useCheckout.ts +312 -0
- package/src/hooks/useError.ts +60 -0
- package/src/hooks/useMarketplace.ts +129 -0
- package/src/hooks/useMemberships.ts +203 -0
- package/src/hooks/useWallet.ts +170 -0
- package/src/index.ts +147 -0
- package/src/lib/context.tsx +478 -0
- package/src/lib/oauth.ts +215 -0
- package/src/lib/parse-error.ts +224 -0
- package/src/lib/parse-stream.ts +121 -0
- package/src/lib/popup.ts +98 -0
- package/src/lib/storage.ts +140 -0
- package/src/lib/utils.ts +14 -0
- package/src/types/index.ts +216 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BuyButton — "buy with Hypery" marketplace checkout button.
|
|
3
|
+
*
|
|
4
|
+
* On click it runs the full auth+charge flow via {@link useCheckout}: it logs
|
|
5
|
+
* the buyer in if needed, charges, and adds a card if none is on file — each
|
|
6
|
+
* step as a popup or a redirect per `config.interactionMode` (`auto` by default).
|
|
7
|
+
* So an unauthenticated visitor can click Buy and be walked through login → card
|
|
8
|
+
* → charge without leaving the page (popup mode). Never throws.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
"use client";
|
|
12
|
+
|
|
13
|
+
import type React from "react";
|
|
14
|
+
import { useState } from "react";
|
|
15
|
+
import { type BuySuccess } from "../hooks/useMarketplace";
|
|
16
|
+
import { useCheckout } from "../hooks/useCheckout";
|
|
17
|
+
import type { BrandingConfig, ParsedError } from "../types";
|
|
18
|
+
|
|
19
|
+
export interface BuyButtonProps {
|
|
20
|
+
/** Seller app the purchase is credited to. */
|
|
21
|
+
appId: string;
|
|
22
|
+
/** Amount to charge the buyer, in cents. */
|
|
23
|
+
amountCents: number;
|
|
24
|
+
/** Optional human-readable description of the purchase. */
|
|
25
|
+
description?: string;
|
|
26
|
+
/** Button label. Defaults to a price-formatted "Buy · $X.XX". */
|
|
27
|
+
label?: React.ReactNode;
|
|
28
|
+
/**
|
|
29
|
+
* Require an explicit per-transaction confirmation before charging (the card
|
|
30
|
+
* is charged off-session, so this gives the buyer a clear "yes"). Defaults to
|
|
31
|
+
* true: the first click arms the button ("Confirm · $X.XX" + Cancel), the
|
|
32
|
+
* second click charges. Set false to charge on a single click.
|
|
33
|
+
*/
|
|
34
|
+
requireConfirmation?: boolean;
|
|
35
|
+
/** Called with the successful checkout result. */
|
|
36
|
+
onSuccess?: (result: BuySuccess) => void;
|
|
37
|
+
/** Called with a classified error for failures other than needs-a-card. */
|
|
38
|
+
onError?: (error: ParsedError) => void;
|
|
39
|
+
/** Custom className appended to the button. */
|
|
40
|
+
className?: string;
|
|
41
|
+
/** Optional branding (primaryColor is used as the button color). */
|
|
42
|
+
branding?: BrandingConfig;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function formatUsd(amountCents: number): string {
|
|
46
|
+
return `$${(amountCents / 100).toFixed(2)}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Drop-in checkout button for the Hypery marketplace.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```tsx
|
|
54
|
+
* <BuyButton appId="app_123" amountCents={499} description="Pro upgrade"
|
|
55
|
+
* onSuccess={(r) => console.log('paid', r.paymentIntentId)} />
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export function BuyButton({
|
|
59
|
+
appId,
|
|
60
|
+
amountCents,
|
|
61
|
+
description,
|
|
62
|
+
label,
|
|
63
|
+
onSuccess,
|
|
64
|
+
onError,
|
|
65
|
+
className = "",
|
|
66
|
+
branding,
|
|
67
|
+
requireConfirmation = true,
|
|
68
|
+
}: BuyButtonProps) {
|
|
69
|
+
const { checkout, isRunning } = useCheckout();
|
|
70
|
+
const [armed, setArmed] = useState(false);
|
|
71
|
+
|
|
72
|
+
const busy = isRunning;
|
|
73
|
+
|
|
74
|
+
const charge = async () => {
|
|
75
|
+
setArmed(false);
|
|
76
|
+
// Runs login → charge → add-card → retry as needed (popup or redirect).
|
|
77
|
+
const result = await checkout({ kind: "purchase", appId, amountCents, description });
|
|
78
|
+
|
|
79
|
+
if (result.status === "success") {
|
|
80
|
+
const b = result.data || {};
|
|
81
|
+
onSuccess?.({
|
|
82
|
+
ok: true,
|
|
83
|
+
paymentIntentId: b.paymentIntentId,
|
|
84
|
+
status: b.status,
|
|
85
|
+
amountCents: b.amountCents ?? amountCents,
|
|
86
|
+
applicationFeeCents: b.applicationFeeCents ?? 0,
|
|
87
|
+
} as BuySuccess);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// 'redirecting' (navigating away, resumes on return) and 'cancelled' (user
|
|
91
|
+
// closed a popup) are not errors — stay quiet.
|
|
92
|
+
if (result.status === "error" && result.error) {
|
|
93
|
+
onError?.(result.error);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const handleClick = () => {
|
|
98
|
+
if (requireConfirmation && !armed) {
|
|
99
|
+
setArmed(true);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
void charge();
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const primaryColor = branding?.primaryColor;
|
|
106
|
+
const style = primaryColor ? { backgroundColor: primaryColor } : undefined;
|
|
107
|
+
|
|
108
|
+
const idleLabel = label ?? `Buy · ${formatUsd(amountCents)}`;
|
|
109
|
+
const buttonLabel = busy
|
|
110
|
+
? "Processing…"
|
|
111
|
+
: armed
|
|
112
|
+
? `Confirm · ${formatUsd(amountCents)}`
|
|
113
|
+
: idleLabel;
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<span className="inline-flex items-center gap-2">
|
|
117
|
+
<button
|
|
118
|
+
type="button"
|
|
119
|
+
onClick={handleClick}
|
|
120
|
+
disabled={busy}
|
|
121
|
+
style={style}
|
|
122
|
+
className={`
|
|
123
|
+
inline-flex items-center justify-center gap-2
|
|
124
|
+
px-6 py-2.5 text-base rounded-lg font-medium transition-all
|
|
125
|
+
text-white shadow-md hover:shadow-lg
|
|
126
|
+
${primaryColor ? "" : "bg-purple-600 hover:bg-purple-700"}
|
|
127
|
+
disabled:opacity-60 disabled:cursor-not-allowed
|
|
128
|
+
${className}
|
|
129
|
+
`}
|
|
130
|
+
>
|
|
131
|
+
{busy && (
|
|
132
|
+
<span
|
|
133
|
+
className="inline-block h-4 w-4 animate-spin rounded-full border-2 border-white/40 border-t-white"
|
|
134
|
+
aria-hidden="true"
|
|
135
|
+
/>
|
|
136
|
+
)}
|
|
137
|
+
{buttonLabel}
|
|
138
|
+
</button>
|
|
139
|
+
{armed && !busy && (
|
|
140
|
+
<button
|
|
141
|
+
type="button"
|
|
142
|
+
onClick={() => setArmed(false)}
|
|
143
|
+
className="px-3 py-2.5 text-sm rounded-lg font-medium text-gray-600 hover:text-gray-900 hover:bg-gray-100 transition-colors"
|
|
144
|
+
>
|
|
145
|
+
Cancel
|
|
146
|
+
</button>
|
|
147
|
+
)}
|
|
148
|
+
</span>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { parseError } from '../lib/parse-error';
|
|
4
|
+
import { SpendingLimitAlert } from './SpendingLimitAlert';
|
|
5
|
+
import { InsufficientCreditsAlert } from './InsufficientCreditsAlert';
|
|
6
|
+
|
|
7
|
+
export interface ErrorBoundaryProps {
|
|
8
|
+
error: any;
|
|
9
|
+
onRetry?: () => void;
|
|
10
|
+
onUpgradeLimits?: () => void;
|
|
11
|
+
onAddCredits?: () => void;
|
|
12
|
+
className?: string;
|
|
13
|
+
children?: React.ReactNode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Universal error boundary component
|
|
18
|
+
* Automatically renders the appropriate error UI based on error type
|
|
19
|
+
*/
|
|
20
|
+
export function ErrorBoundary({
|
|
21
|
+
error,
|
|
22
|
+
onRetry,
|
|
23
|
+
onUpgradeLimits,
|
|
24
|
+
onAddCredits,
|
|
25
|
+
className = '',
|
|
26
|
+
children,
|
|
27
|
+
}: ErrorBoundaryProps) {
|
|
28
|
+
if (!error) return <>{children}</>;
|
|
29
|
+
|
|
30
|
+
const parsed = parseError(error);
|
|
31
|
+
|
|
32
|
+
// Render spending limit alert
|
|
33
|
+
if (parsed.isSpendingLimit) {
|
|
34
|
+
return (
|
|
35
|
+
<SpendingLimitAlert
|
|
36
|
+
error={parsed}
|
|
37
|
+
onRetry={onRetry}
|
|
38
|
+
onUpgradeLimits={onUpgradeLimits}
|
|
39
|
+
className={className}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Render insufficient credits alert
|
|
45
|
+
if (parsed.isInsufficientCredits) {
|
|
46
|
+
return (
|
|
47
|
+
<InsufficientCreditsAlert
|
|
48
|
+
error={parsed}
|
|
49
|
+
onAddCredits={onAddCredits}
|
|
50
|
+
className={className}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Render generic error
|
|
56
|
+
return (
|
|
57
|
+
<div
|
|
58
|
+
className={`rounded-lg border border-gray-200 bg-gray-50 p-4 ${className}`}
|
|
59
|
+
role="alert"
|
|
60
|
+
>
|
|
61
|
+
<div className="flex items-start gap-3">
|
|
62
|
+
<div className="flex-shrink-0">
|
|
63
|
+
<svg
|
|
64
|
+
className="h-5 w-5 text-gray-400"
|
|
65
|
+
viewBox="0 0 20 20"
|
|
66
|
+
fill="currentColor"
|
|
67
|
+
aria-hidden="true"
|
|
68
|
+
>
|
|
69
|
+
<path
|
|
70
|
+
fillRule="evenodd"
|
|
71
|
+
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z"
|
|
72
|
+
clipRule="evenodd"
|
|
73
|
+
/>
|
|
74
|
+
</svg>
|
|
75
|
+
</div>
|
|
76
|
+
<div className="flex-1">
|
|
77
|
+
<h3 className="text-sm font-medium text-gray-800">Error</h3>
|
|
78
|
+
<div className="mt-2 text-sm text-gray-700">
|
|
79
|
+
<p>{parsed.message}</p>
|
|
80
|
+
</div>
|
|
81
|
+
{onRetry && (
|
|
82
|
+
<div className="mt-4">
|
|
83
|
+
<button
|
|
84
|
+
type="button"
|
|
85
|
+
onClick={onRetry}
|
|
86
|
+
className="text-sm font-medium text-gray-800 hover:text-gray-900"
|
|
87
|
+
>
|
|
88
|
+
Try again
|
|
89
|
+
</button>
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { useHyperyAuth } from "../lib/context";
|
|
5
|
+
import { AuthModal } from "./AuthModal";
|
|
6
|
+
import { type RestrictionError, RestrictionModal } from "./RestrictionModal";
|
|
7
|
+
|
|
8
|
+
export interface HyperyModalsProps {
|
|
9
|
+
/**
|
|
10
|
+
* Branding + auth-form options forwarded to the auto-opened `AuthModal`.
|
|
11
|
+
*/
|
|
12
|
+
branding?: {
|
|
13
|
+
logo?: string;
|
|
14
|
+
appName?: string;
|
|
15
|
+
primaryColor?: string;
|
|
16
|
+
};
|
|
17
|
+
showSocial?: boolean;
|
|
18
|
+
showEmailPassword?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Called when the funds modal's retry button is pressed (e.g. re-run the
|
|
21
|
+
* request that was blocked). The restriction is cleared automatically.
|
|
22
|
+
*/
|
|
23
|
+
onRetry?: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Turnkey modal host. Mount this ONCE anywhere inside `<HyperyProvider>` and the
|
|
28
|
+
* billing/auth modals open automatically off the context state that
|
|
29
|
+
* `authenticatedFetch` sets — no manual `setError`/`isOpen` wiring:
|
|
30
|
+
*
|
|
31
|
+
* - a 402/429 billing restriction → `<RestrictionModal>` (add funds / add card)
|
|
32
|
+
* - a 401 that survives a silent-refresh retry → `<AuthModal>` (re-auth)
|
|
33
|
+
*
|
|
34
|
+
* All wiring (clientId, gatewayUrl, getAccessToken, open/close) is read from
|
|
35
|
+
* context, so this component takes no required props.
|
|
36
|
+
*/
|
|
37
|
+
export function HyperyModals({
|
|
38
|
+
branding,
|
|
39
|
+
showSocial,
|
|
40
|
+
showEmailPassword,
|
|
41
|
+
onRetry,
|
|
42
|
+
}: HyperyModalsProps = {}) {
|
|
43
|
+
const {
|
|
44
|
+
clientId,
|
|
45
|
+
gatewayUrl,
|
|
46
|
+
getAccessToken,
|
|
47
|
+
restriction,
|
|
48
|
+
clearRestriction,
|
|
49
|
+
authRequired,
|
|
50
|
+
clearAuthRequired,
|
|
51
|
+
} = useHyperyAuth();
|
|
52
|
+
|
|
53
|
+
// ParsedError → RestrictionModal's RestrictionError (data carries the billing
|
|
54
|
+
// fields: limit/current/available/resetsAt/...).
|
|
55
|
+
const restrictionError: RestrictionError | null = restriction
|
|
56
|
+
? {
|
|
57
|
+
code: restriction.code,
|
|
58
|
+
message: restriction.message,
|
|
59
|
+
type: restriction.type,
|
|
60
|
+
...(restriction.data as Record<string, unknown>),
|
|
61
|
+
}
|
|
62
|
+
: null;
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<>
|
|
66
|
+
<RestrictionModal
|
|
67
|
+
error={restrictionError}
|
|
68
|
+
clientId={clientId}
|
|
69
|
+
gatewayUrl={gatewayUrl}
|
|
70
|
+
getAccessToken={getAccessToken}
|
|
71
|
+
onClose={clearRestriction}
|
|
72
|
+
onRetry={onRetry}
|
|
73
|
+
/>
|
|
74
|
+
<AuthModal
|
|
75
|
+
isOpen={authRequired}
|
|
76
|
+
onClose={clearAuthRequired}
|
|
77
|
+
branding={branding}
|
|
78
|
+
showSocial={showSocial}
|
|
79
|
+
showEmailPassword={showEmailPassword}
|
|
80
|
+
/>
|
|
81
|
+
</>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ParsedError } from '../types';
|
|
4
|
+
|
|
5
|
+
export interface InsufficientCreditsAlertProps {
|
|
6
|
+
error: ParsedError;
|
|
7
|
+
onAddCredits?: () => void;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Alert component for insufficient credits errors
|
|
13
|
+
* Displays a user-friendly message with option to add credits
|
|
14
|
+
*/
|
|
15
|
+
export function InsufficientCreditsAlert({
|
|
16
|
+
error,
|
|
17
|
+
onAddCredits,
|
|
18
|
+
className = '',
|
|
19
|
+
}: InsufficientCreditsAlertProps) {
|
|
20
|
+
if (!error.isInsufficientCredits) return null;
|
|
21
|
+
|
|
22
|
+
const data = error.data as any;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
className={`rounded-lg border border-red-200 bg-red-50 p-4 ${className}`}
|
|
27
|
+
role="alert"
|
|
28
|
+
>
|
|
29
|
+
<div className="flex items-start gap-3">
|
|
30
|
+
<div className="flex-shrink-0">
|
|
31
|
+
<svg
|
|
32
|
+
className="h-5 w-5 text-red-400"
|
|
33
|
+
viewBox="0 0 20 20"
|
|
34
|
+
fill="currentColor"
|
|
35
|
+
aria-hidden="true"
|
|
36
|
+
>
|
|
37
|
+
<path
|
|
38
|
+
fillRule="evenodd"
|
|
39
|
+
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z"
|
|
40
|
+
clipRule="evenodd"
|
|
41
|
+
/>
|
|
42
|
+
</svg>
|
|
43
|
+
</div>
|
|
44
|
+
<div className="flex-1">
|
|
45
|
+
<h3 className="text-sm font-medium text-red-800">
|
|
46
|
+
Insufficient Credits
|
|
47
|
+
</h3>
|
|
48
|
+
<div className="mt-2 text-sm text-red-700">
|
|
49
|
+
<p>{error.message}</p>
|
|
50
|
+
{data.available !== undefined && data.required !== undefined && (
|
|
51
|
+
<p className="mt-1">
|
|
52
|
+
You have <strong>{data.available}</strong> credits, but need{' '}
|
|
53
|
+
<strong>{data.required}</strong> credits.
|
|
54
|
+
</p>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
{onAddCredits && (
|
|
58
|
+
<div className="mt-4">
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
onClick={onAddCredits}
|
|
62
|
+
className="text-sm font-medium text-red-800 hover:text-red-900 underline"
|
|
63
|
+
>
|
|
64
|
+
Add credits →
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|