@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,185 @@
|
|
|
1
|
+
const SPENDING_LIMIT = 'SPENDING_LIMIT_EXCEEDED';
|
|
2
|
+
const INSUFFICIENT_CREDITS = 'INSUFFICIENT_CREDITS';
|
|
3
|
+
const PAYMENT_METHOD_REQUIRED = 'PAYMENT_METHOD_REQUIRED';
|
|
4
|
+
const PAYMENT_DECLINED = 'PAYMENT_DECLINED';
|
|
5
|
+
const UNAUTHENTICATED = 'UNAUTHENTICATED';
|
|
6
|
+
const PERMISSION_DENIED = 'PERMISSION_DENIED';
|
|
7
|
+
const INSUFFICIENT_SCOPE = 'INSUFFICIENT_SCOPE';
|
|
8
|
+
const RATE_LIMITED = 'RATE_LIMITED';
|
|
9
|
+
/**
|
|
10
|
+
* Extract the inner error object from any of the shapes the gateway / a caller
|
|
11
|
+
* may hand us:
|
|
12
|
+
* - the raw server envelope: `{ error: { code, message, type, ... } }`
|
|
13
|
+
* - an already-unwrapped error object: `{ code, message, type, ... }`
|
|
14
|
+
* Returns undefined if neither shape carries a `code`.
|
|
15
|
+
*/
|
|
16
|
+
function unwrap(error) {
|
|
17
|
+
if (error && typeof error === 'object') {
|
|
18
|
+
if (error.error && typeof error.error === 'object' && 'code' in error.error) {
|
|
19
|
+
return error.error;
|
|
20
|
+
}
|
|
21
|
+
if (typeof error.code === 'string') {
|
|
22
|
+
return error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
/** Pull an HTTP status off a Response-like / thrown-error object, if present. */
|
|
28
|
+
function statusOf(error) {
|
|
29
|
+
const s = error?.status ?? error?.statusCode ?? error?.response?.status;
|
|
30
|
+
return typeof s === 'number' ? s : undefined;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Parse an error (a parsed JSON body, a thrown error, or a Response-like object)
|
|
34
|
+
* from the Hypery API into a normalized, classified shape. Tolerant of both the
|
|
35
|
+
* `{ error: { code } }` envelope and an already-unwrapped object, and falls back
|
|
36
|
+
* to the HTTP status when no recognized `code` is present.
|
|
37
|
+
*/
|
|
38
|
+
export function parseError(error) {
|
|
39
|
+
const errorData = unwrap(error);
|
|
40
|
+
const status = statusOf(error);
|
|
41
|
+
if (errorData) {
|
|
42
|
+
const code = errorData.code;
|
|
43
|
+
return {
|
|
44
|
+
code,
|
|
45
|
+
message: errorData.message,
|
|
46
|
+
type: errorData.type,
|
|
47
|
+
status,
|
|
48
|
+
isSpendingLimit: code === SPENDING_LIMIT,
|
|
49
|
+
isInsufficientCredits: code === INSUFFICIENT_CREDITS,
|
|
50
|
+
isPaymentMethodRequired: code === PAYMENT_METHOD_REQUIRED,
|
|
51
|
+
isPaymentDeclined: code === PAYMENT_DECLINED,
|
|
52
|
+
isAuth: code === UNAUTHENTICATED || errorData.type === 'authentication_error',
|
|
53
|
+
isPermissionDenied: code === PERMISSION_DENIED ||
|
|
54
|
+
code === INSUFFICIENT_SCOPE ||
|
|
55
|
+
errorData.type === 'permission_error' ||
|
|
56
|
+
errorData.type === 'authorization_error',
|
|
57
|
+
isRateLimit: code === RATE_LIMITED || errorData.type === 'rate_limit_error',
|
|
58
|
+
data: errorData,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// No recognized code — classify from HTTP status so a payment/auth modal can
|
|
62
|
+
// still open for an endpoint that returns a non-canonical body.
|
|
63
|
+
if (status === 402) {
|
|
64
|
+
return statusFallback(INSUFFICIENT_CREDITS, 'Insufficient credits.', status, {
|
|
65
|
+
isInsufficientCredits: true,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (status === 401) {
|
|
69
|
+
return statusFallback(UNAUTHENTICATED, 'Authentication required.', status, { isAuth: true });
|
|
70
|
+
}
|
|
71
|
+
if (status === 403) {
|
|
72
|
+
return statusFallback(PERMISSION_DENIED, 'You do not have permission to perform this action.', status, {
|
|
73
|
+
isPermissionDenied: true,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (status === 429) {
|
|
77
|
+
return statusFallback(RATE_LIMITED, 'Rate limit exceeded. Please retry shortly.', status, {
|
|
78
|
+
isRateLimit: true,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
// Handle generic errors
|
|
82
|
+
const message = error?.message || 'An unknown error occurred';
|
|
83
|
+
return {
|
|
84
|
+
code: 'UNKNOWN_ERROR',
|
|
85
|
+
message,
|
|
86
|
+
status,
|
|
87
|
+
isSpendingLimit: false,
|
|
88
|
+
isInsufficientCredits: false,
|
|
89
|
+
isPaymentMethodRequired: false,
|
|
90
|
+
isPaymentDeclined: false,
|
|
91
|
+
isAuth: false,
|
|
92
|
+
isPermissionDenied: false,
|
|
93
|
+
isRateLimit: false,
|
|
94
|
+
data: {
|
|
95
|
+
code: 'UNKNOWN_ERROR',
|
|
96
|
+
message,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function statusFallback(code, message, status, flags) {
|
|
101
|
+
return {
|
|
102
|
+
code,
|
|
103
|
+
message,
|
|
104
|
+
status,
|
|
105
|
+
isSpendingLimit: false,
|
|
106
|
+
isInsufficientCredits: false,
|
|
107
|
+
isPaymentMethodRequired: false,
|
|
108
|
+
isPaymentDeclined: false,
|
|
109
|
+
isAuth: false,
|
|
110
|
+
isPermissionDenied: false,
|
|
111
|
+
isRateLimit: false,
|
|
112
|
+
...flags,
|
|
113
|
+
data: { code, message },
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Check if an error is a spending limit error
|
|
118
|
+
*/
|
|
119
|
+
export function isSpendingLimitError(error) {
|
|
120
|
+
return parseError(error).isSpendingLimit;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Check if an error is an insufficient credits error
|
|
124
|
+
*/
|
|
125
|
+
export function isInsufficientCreditsError(error) {
|
|
126
|
+
return parseError(error).isInsufficientCredits;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Check if an error means the user must add a payment method (metered billing).
|
|
130
|
+
*/
|
|
131
|
+
export function isPaymentMethodRequiredError(error) {
|
|
132
|
+
return parseError(error).isPaymentMethodRequired;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Check if an error means the user's card was declined (metered billing).
|
|
136
|
+
*/
|
|
137
|
+
export function isPaymentDeclinedError(error) {
|
|
138
|
+
return parseError(error).isPaymentDeclined;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Check if an error is an authentication failure (re-auth / open the auth modal).
|
|
142
|
+
*/
|
|
143
|
+
export function isAuthError(error) {
|
|
144
|
+
return parseError(error).isAuth;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Check if an error is a scope/permission denial (403) — NOT an auth failure,
|
|
148
|
+
* so it should not trigger re-auth.
|
|
149
|
+
*/
|
|
150
|
+
export function isPermissionDeniedError(error) {
|
|
151
|
+
return parseError(error).isPermissionDenied;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Check if an error is a rate-limit denial (429 / RATE_LIMITED).
|
|
155
|
+
*/
|
|
156
|
+
export function isRateLimitError(error) {
|
|
157
|
+
return parseError(error).isRateLimit;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Any billing restriction the funds widget should surface a CTA for.
|
|
161
|
+
*/
|
|
162
|
+
export function isBillingRestriction(error) {
|
|
163
|
+
return (isSpendingLimitError(error) ||
|
|
164
|
+
isInsufficientCreditsError(error) ||
|
|
165
|
+
isPaymentMethodRequiredError(error) ||
|
|
166
|
+
isPaymentDeclinedError(error));
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Format time until reset
|
|
170
|
+
*/
|
|
171
|
+
export function formatTimeUntilReset(resetsAt) {
|
|
172
|
+
if (!resetsAt)
|
|
173
|
+
return '';
|
|
174
|
+
const reset = new Date(resetsAt);
|
|
175
|
+
const now = new Date();
|
|
176
|
+
const diff = reset.getTime() - now.getTime();
|
|
177
|
+
if (diff < 0)
|
|
178
|
+
return 'soon';
|
|
179
|
+
const hours = Math.floor(diff / (1000 * 60 * 60));
|
|
180
|
+
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
|
181
|
+
if (hours > 0) {
|
|
182
|
+
return `in ${hours}h ${minutes}m`;
|
|
183
|
+
}
|
|
184
|
+
return `in ${minutes}m`;
|
|
185
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ParsedError } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* Streaming (SSE) error classification for the gateway's chat/completions stream.
|
|
4
|
+
*
|
|
5
|
+
* A pre-stream denial arrives as a normal HTTP 402/429/403 with a JSON body —
|
|
6
|
+
* `parseError` already handles that. But a failure AFTER the stream has started
|
|
7
|
+
* (e.g. a billing/finalize error) is delivered as an SSE `event: error` frame,
|
|
8
|
+
* which a fetch-stream consumer must parse itself. These helpers surface that
|
|
9
|
+
* terminal error in the EXACT same classified shape as a non-streaming error,
|
|
10
|
+
* because the gateway now emits the canonical `{ error: { code, type, message } }`
|
|
11
|
+
* envelope inside the frame's `data:` (see lib/errors/api-error.ts).
|
|
12
|
+
*/
|
|
13
|
+
export interface SSEEvent {
|
|
14
|
+
/** The `event:` name, if the frame set one (e.g. `error`). */
|
|
15
|
+
event?: string;
|
|
16
|
+
/** The concatenated `data:` payload. */
|
|
17
|
+
data: string;
|
|
18
|
+
}
|
|
19
|
+
/** Parse a raw SSE frame block (the text between blank-line delimiters). */
|
|
20
|
+
export declare function parseSSEFrame(block: string): SSEEvent | null;
|
|
21
|
+
/**
|
|
22
|
+
* If a frame is a terminal error — either `event: error`, or a `data:` payload
|
|
23
|
+
* carrying the canonical `{ error: { code } }` envelope — return the classified
|
|
24
|
+
* ParsedError (identical to `parseError()` for a non-streaming failure).
|
|
25
|
+
* Returns null for a normal data frame or the `[DONE]` sentinel.
|
|
26
|
+
*/
|
|
27
|
+
export declare function parseSSEError(frame: SSEEvent | string): ParsedError | null;
|
|
28
|
+
export interface SSEStreamHandlers {
|
|
29
|
+
/** Called with each normal SSE `data:` payload (e.g. an OpenAI delta chunk). */
|
|
30
|
+
onData?: (data: string) => void;
|
|
31
|
+
/** Called ONCE with a classified error if the stream ends in an error frame. */
|
|
32
|
+
onError?: (error: ParsedError) => void;
|
|
33
|
+
/** Called when the stream completes (after `[DONE]` or the body closing). */
|
|
34
|
+
onDone?: () => void;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Consume a streaming Response body (`fetch(...).body`), splitting it into SSE
|
|
38
|
+
* frames. Normal data frames go to `onData`; a terminal `event: error` frame is
|
|
39
|
+
* classified and delivered to `onError` (NOT thrown), so a consumer using raw
|
|
40
|
+
* fetch-stream parsing gets the same `ParsedError` it would from a non-streaming
|
|
41
|
+
* call and can drive the same billing/auth modal. Resolves when the stream ends.
|
|
42
|
+
*/
|
|
43
|
+
export declare function consumeSSEStream(body: ReadableStream<Uint8Array> | null | undefined, handlers: SSEStreamHandlers): Promise<void>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { parseError } from "./parse-error";
|
|
2
|
+
/** Parse a raw SSE frame block (the text between blank-line delimiters). */
|
|
3
|
+
export function parseSSEFrame(block) {
|
|
4
|
+
let event;
|
|
5
|
+
const dataLines = [];
|
|
6
|
+
for (const line of block.split("\n")) {
|
|
7
|
+
if (line.startsWith("event:")) {
|
|
8
|
+
event = line.slice(6).trim();
|
|
9
|
+
}
|
|
10
|
+
else if (line.startsWith("data:")) {
|
|
11
|
+
dataLines.push(line.slice(5).replace(/^ /, ""));
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
if (!event && dataLines.length === 0)
|
|
15
|
+
return null;
|
|
16
|
+
return { event, data: dataLines.join("\n") };
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* If a frame is a terminal error — either `event: error`, or a `data:` payload
|
|
20
|
+
* carrying the canonical `{ error: { code } }` envelope — return the classified
|
|
21
|
+
* ParsedError (identical to `parseError()` for a non-streaming failure).
|
|
22
|
+
* Returns null for a normal data frame or the `[DONE]` sentinel.
|
|
23
|
+
*/
|
|
24
|
+
export function parseSSEError(frame) {
|
|
25
|
+
const f = typeof frame === "string" ? parseSSEFrame(frame) : frame;
|
|
26
|
+
if (!f)
|
|
27
|
+
return null;
|
|
28
|
+
if (f.data === "[DONE]")
|
|
29
|
+
return null;
|
|
30
|
+
const isErrorEvent = f.event === "error";
|
|
31
|
+
let payload;
|
|
32
|
+
try {
|
|
33
|
+
payload = JSON.parse(f.data);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Non-JSON data on an error frame — still surface it as a classified error.
|
|
37
|
+
return isErrorEvent ? parseError({ message: f.data }) : null;
|
|
38
|
+
}
|
|
39
|
+
const hasEnvelope = !!payload &&
|
|
40
|
+
typeof payload === "object" &&
|
|
41
|
+
typeof payload.error === "object" &&
|
|
42
|
+
payload.error !== null &&
|
|
43
|
+
"code" in payload.error;
|
|
44
|
+
return isErrorEvent || hasEnvelope ? parseError(payload) : null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Consume a streaming Response body (`fetch(...).body`), splitting it into SSE
|
|
48
|
+
* frames. Normal data frames go to `onData`; a terminal `event: error` frame is
|
|
49
|
+
* classified and delivered to `onError` (NOT thrown), so a consumer using raw
|
|
50
|
+
* fetch-stream parsing gets the same `ParsedError` it would from a non-streaming
|
|
51
|
+
* call and can drive the same billing/auth modal. Resolves when the stream ends.
|
|
52
|
+
*/
|
|
53
|
+
export async function consumeSSEStream(body, handlers) {
|
|
54
|
+
if (!body) {
|
|
55
|
+
handlers.onDone?.();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const reader = body.getReader();
|
|
59
|
+
const decoder = new TextDecoder();
|
|
60
|
+
let buffer = "";
|
|
61
|
+
try {
|
|
62
|
+
while (true) {
|
|
63
|
+
const { value, done } = await reader.read();
|
|
64
|
+
if (done)
|
|
65
|
+
break;
|
|
66
|
+
buffer += decoder.decode(value, { stream: true });
|
|
67
|
+
let idx;
|
|
68
|
+
while ((idx = buffer.indexOf("\n\n")) !== -1) {
|
|
69
|
+
const block = buffer.slice(0, idx);
|
|
70
|
+
buffer = buffer.slice(idx + 2);
|
|
71
|
+
const frame = parseSSEFrame(block);
|
|
72
|
+
if (!frame)
|
|
73
|
+
continue;
|
|
74
|
+
const err = parseSSEError(frame);
|
|
75
|
+
if (err) {
|
|
76
|
+
handlers.onError?.(err);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (frame.data === "[DONE]")
|
|
80
|
+
continue;
|
|
81
|
+
handlers.onData?.(frame.data);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
reader.releaseLock();
|
|
87
|
+
}
|
|
88
|
+
handlers.onDone?.();
|
|
89
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Popup helpers for the seamless (in-page) auth + charge flow.
|
|
3
|
+
*
|
|
4
|
+
* Both auth and Stripe card-entry can run in a popup so the whole
|
|
5
|
+
* press-buy → login → add-card → charge chain completes without leaving the
|
|
6
|
+
* developer's page (the PayPal / Firebase `signInWithPopup` model). Each helper
|
|
7
|
+
* resolves when the popup posts a completion message from the expected origin,
|
|
8
|
+
* or when the popup closes (treated as a cancel). If the browser blocks the
|
|
9
|
+
* popup, the helper returns `'blocked'` so the caller can fall back to redirect.
|
|
10
|
+
*/
|
|
11
|
+
import type { InteractionMode, ResolvedMode } from '../types';
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the effective mode. `auto` prefers popup on desktop but falls back to
|
|
14
|
+
* redirect on mobile / in-app webviews, where popups are unreliable (this mirrors
|
|
15
|
+
* Firebase's "popup on web, redirect on mobile" guidance).
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveInteractionMode(mode: InteractionMode | undefined): ResolvedMode;
|
|
18
|
+
interface PopupOptions {
|
|
19
|
+
url: string;
|
|
20
|
+
/** window.open target name; also read back in the callback to detect popup context. */
|
|
21
|
+
name: string;
|
|
22
|
+
/** Only completion messages from this origin are trusted. */
|
|
23
|
+
expectedOrigin: string;
|
|
24
|
+
/** Message discriminator to accept (e.g. 'hypery:auth', 'hypery:payment-method'). */
|
|
25
|
+
messageType: string;
|
|
26
|
+
width?: number;
|
|
27
|
+
height?: number;
|
|
28
|
+
}
|
|
29
|
+
export type PopupResult<T> = {
|
|
30
|
+
blocked: true;
|
|
31
|
+
} | {
|
|
32
|
+
blocked: false;
|
|
33
|
+
cancelled: boolean;
|
|
34
|
+
data?: T;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Open `url` in a popup and resolve when it posts `{ type: messageType, ... }`
|
|
38
|
+
* from `expectedOrigin`, or when it closes (cancelled). Returns `{ blocked: true }`
|
|
39
|
+
* if the browser blocked the popup.
|
|
40
|
+
*/
|
|
41
|
+
export declare function openPopup<T = any>(opts: PopupOptions): Promise<PopupResult<T>>;
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Popup helpers for the seamless (in-page) auth + charge flow.
|
|
3
|
+
*
|
|
4
|
+
* Both auth and Stripe card-entry can run in a popup so the whole
|
|
5
|
+
* press-buy → login → add-card → charge chain completes without leaving the
|
|
6
|
+
* developer's page (the PayPal / Firebase `signInWithPopup` model). Each helper
|
|
7
|
+
* resolves when the popup posts a completion message from the expected origin,
|
|
8
|
+
* or when the popup closes (treated as a cancel). If the browser blocks the
|
|
9
|
+
* popup, the helper returns `'blocked'` so the caller can fall back to redirect.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the effective mode. `auto` prefers popup on desktop but falls back to
|
|
13
|
+
* redirect on mobile / in-app webviews, where popups are unreliable (this mirrors
|
|
14
|
+
* Firebase's "popup on web, redirect on mobile" guidance).
|
|
15
|
+
*/
|
|
16
|
+
export function resolveInteractionMode(mode) {
|
|
17
|
+
const m = mode || 'auto';
|
|
18
|
+
if (m === 'popup' || m === 'redirect')
|
|
19
|
+
return m;
|
|
20
|
+
if (typeof navigator === 'undefined')
|
|
21
|
+
return 'redirect';
|
|
22
|
+
const ua = navigator.userAgent || '';
|
|
23
|
+
const isMobile = /Android|iPhone|iPad|iPod|Mobile|Silk|Kindle|BlackBerry|Opera Mini/i.test(ua);
|
|
24
|
+
return isMobile ? 'redirect' : 'popup';
|
|
25
|
+
}
|
|
26
|
+
function centeredFeatures(w, h) {
|
|
27
|
+
// Best-effort centering; harmless if the values are unavailable (SSR-guarded caller).
|
|
28
|
+
const dualLeft = typeof window !== 'undefined' ? (window.screenLeft ?? 0) : 0;
|
|
29
|
+
const dualTop = typeof window !== 'undefined' ? (window.screenTop ?? 0) : 0;
|
|
30
|
+
const width = typeof window !== 'undefined' ? window.innerWidth || w : w;
|
|
31
|
+
const height = typeof window !== 'undefined' ? window.innerHeight || h : h;
|
|
32
|
+
const left = dualLeft + Math.max(0, (width - w) / 2);
|
|
33
|
+
const top = dualTop + Math.max(0, (height - h) / 2);
|
|
34
|
+
return `width=${w},height=${h},left=${Math.round(left)},top=${Math.round(top)},resizable=yes,scrollbars=yes`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Open `url` in a popup and resolve when it posts `{ type: messageType, ... }`
|
|
38
|
+
* from `expectedOrigin`, or when it closes (cancelled). Returns `{ blocked: true }`
|
|
39
|
+
* if the browser blocked the popup.
|
|
40
|
+
*/
|
|
41
|
+
export function openPopup(opts) {
|
|
42
|
+
if (typeof window === 'undefined') {
|
|
43
|
+
return Promise.resolve({ blocked: false, cancelled: true });
|
|
44
|
+
}
|
|
45
|
+
const popup = window.open(opts.url, opts.name, centeredFeatures(opts.width || 480, opts.height || 720));
|
|
46
|
+
if (!popup) {
|
|
47
|
+
return Promise.resolve({ blocked: true });
|
|
48
|
+
}
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
let settled = false;
|
|
51
|
+
const finish = (result) => {
|
|
52
|
+
if (settled)
|
|
53
|
+
return;
|
|
54
|
+
settled = true;
|
|
55
|
+
window.removeEventListener('message', onMessage);
|
|
56
|
+
clearInterval(poll);
|
|
57
|
+
resolve(result);
|
|
58
|
+
};
|
|
59
|
+
const onMessage = (event) => {
|
|
60
|
+
if (event.origin !== opts.expectedOrigin)
|
|
61
|
+
return;
|
|
62
|
+
const data = event.data;
|
|
63
|
+
if (data && typeof data === 'object' && data.type === opts.messageType) {
|
|
64
|
+
try {
|
|
65
|
+
popup.close();
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
/* cross-origin close may throw; ignore */
|
|
69
|
+
}
|
|
70
|
+
finish({ blocked: false, cancelled: false, data: data });
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
window.addEventListener('message', onMessage);
|
|
74
|
+
// Fallback: if the popup is closed without a message, treat it as a cancel.
|
|
75
|
+
const poll = setInterval(() => {
|
|
76
|
+
if (popup.closed)
|
|
77
|
+
finish({ blocked: false, cancelled: true });
|
|
78
|
+
}, 500);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token storage utilities
|
|
3
|
+
*/
|
|
4
|
+
import type { AuthTokens, User } from '../types';
|
|
5
|
+
export declare class TokenStorage {
|
|
6
|
+
private storage;
|
|
7
|
+
private storageType;
|
|
8
|
+
constructor(storageType?: 'localStorage' | 'sessionStorage' | 'memory');
|
|
9
|
+
/**
|
|
10
|
+
* Save auth tokens
|
|
11
|
+
*/
|
|
12
|
+
saveTokens(tokens: AuthTokens): void;
|
|
13
|
+
/**
|
|
14
|
+
* Get auth tokens
|
|
15
|
+
*/
|
|
16
|
+
getTokens(): (AuthTokens & {
|
|
17
|
+
savedAt: number;
|
|
18
|
+
}) | null;
|
|
19
|
+
/**
|
|
20
|
+
* Check if access token is expired
|
|
21
|
+
*/
|
|
22
|
+
isTokenExpired(): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Clear auth tokens
|
|
25
|
+
*/
|
|
26
|
+
clearTokens(): void;
|
|
27
|
+
/**
|
|
28
|
+
* Save user info
|
|
29
|
+
*/
|
|
30
|
+
saveUser(user: User): void;
|
|
31
|
+
/**
|
|
32
|
+
* Get user info
|
|
33
|
+
*/
|
|
34
|
+
getUser(): User | null;
|
|
35
|
+
/**
|
|
36
|
+
* Clear user info
|
|
37
|
+
*/
|
|
38
|
+
clearUser(): void;
|
|
39
|
+
/**
|
|
40
|
+
* Clear all auth data
|
|
41
|
+
*/
|
|
42
|
+
clear(): void;
|
|
43
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token storage utilities
|
|
3
|
+
*/
|
|
4
|
+
const TOKEN_KEY = 'hypery_auth_tokens';
|
|
5
|
+
const USER_KEY = 'hypery_auth_user';
|
|
6
|
+
export class TokenStorage {
|
|
7
|
+
constructor(storageType = 'localStorage') {
|
|
8
|
+
this.storageType = storageType;
|
|
9
|
+
if (storageType === 'memory' || typeof window === 'undefined') {
|
|
10
|
+
// In-memory storage for SSR or memory mode
|
|
11
|
+
this.storage = new Map();
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
this.storage = storageType === 'localStorage' ? localStorage : sessionStorage;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Save auth tokens
|
|
19
|
+
*/
|
|
20
|
+
saveTokens(tokens) {
|
|
21
|
+
const data = JSON.stringify({
|
|
22
|
+
...tokens,
|
|
23
|
+
savedAt: Date.now(),
|
|
24
|
+
});
|
|
25
|
+
if (this.storage instanceof Map) {
|
|
26
|
+
this.storage.set(TOKEN_KEY, data);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
this.storage.setItem(TOKEN_KEY, data);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Get auth tokens
|
|
34
|
+
*/
|
|
35
|
+
getTokens() {
|
|
36
|
+
let data = null;
|
|
37
|
+
if (this.storage instanceof Map) {
|
|
38
|
+
data = this.storage.get(TOKEN_KEY) || null;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
data = this.storage.getItem(TOKEN_KEY);
|
|
42
|
+
}
|
|
43
|
+
if (!data)
|
|
44
|
+
return null;
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(data);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Check if access token is expired
|
|
54
|
+
*/
|
|
55
|
+
isTokenExpired() {
|
|
56
|
+
const tokens = this.getTokens();
|
|
57
|
+
if (!tokens)
|
|
58
|
+
return true;
|
|
59
|
+
const expiresAt = tokens.savedAt + tokens.expiresIn * 1000;
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
// Add 60s buffer to refresh before expiration
|
|
62
|
+
return now >= expiresAt - 60000;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Clear auth tokens
|
|
66
|
+
*/
|
|
67
|
+
clearTokens() {
|
|
68
|
+
if (this.storage instanceof Map) {
|
|
69
|
+
this.storage.delete(TOKEN_KEY);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
this.storage.removeItem(TOKEN_KEY);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Save user info
|
|
77
|
+
*/
|
|
78
|
+
saveUser(user) {
|
|
79
|
+
const data = JSON.stringify(user);
|
|
80
|
+
if (this.storage instanceof Map) {
|
|
81
|
+
this.storage.set(USER_KEY, data);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
this.storage.setItem(USER_KEY, data);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get user info
|
|
89
|
+
*/
|
|
90
|
+
getUser() {
|
|
91
|
+
let data = null;
|
|
92
|
+
if (this.storage instanceof Map) {
|
|
93
|
+
data = this.storage.get(USER_KEY) || null;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
data = this.storage.getItem(USER_KEY);
|
|
97
|
+
}
|
|
98
|
+
if (!data)
|
|
99
|
+
return null;
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(data);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Clear user info
|
|
109
|
+
*/
|
|
110
|
+
clearUser() {
|
|
111
|
+
if (this.storage instanceof Map) {
|
|
112
|
+
this.storage.delete(USER_KEY);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
this.storage.removeItem(USER_KEY);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Clear all auth data
|
|
120
|
+
*/
|
|
121
|
+
clear() {
|
|
122
|
+
this.clearTokens();
|
|
123
|
+
this.clearUser();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for @hyperyai/sdk
|
|
3
|
+
*/
|
|
4
|
+
import { clsx } from "clsx";
|
|
5
|
+
import { twMerge } from "tailwind-merge";
|
|
6
|
+
/**
|
|
7
|
+
* Merge Tailwind CSS classes with proper precedence
|
|
8
|
+
*/
|
|
9
|
+
export function cn(...inputs) {
|
|
10
|
+
return twMerge(clsx(inputs));
|
|
11
|
+
}
|