@hexclave/next 1.0.21 → 1.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +187 -7
  2. package/dist/components-page/oauth-callback.js +14 -19
  3. package/dist/components-page/oauth-callback.js.map +1 -1
  4. package/dist/components-page/oauth-callback.test.d.ts +1 -0
  5. package/dist/components-page/oauth-callback.test.js +90 -0
  6. package/dist/components-page/oauth-callback.test.js.map +1 -0
  7. package/dist/esm/components-page/oauth-callback.js +14 -19
  8. package/dist/esm/components-page/oauth-callback.js.map +1 -1
  9. package/dist/esm/components-page/oauth-callback.test.d.ts +1 -0
  10. package/dist/esm/components-page/oauth-callback.test.js +89 -0
  11. package/dist/esm/components-page/oauth-callback.test.js.map +1 -0
  12. package/dist/esm/generated/quetzal-translations.d.ts +2 -2
  13. package/dist/esm/lib/auth.d.ts.map +1 -1
  14. package/dist/esm/lib/auth.js +32 -11
  15. package/dist/esm/lib/auth.js.map +1 -1
  16. package/dist/esm/lib/auth.test.js +25 -10
  17. package/dist/esm/lib/auth.test.js.map +1 -1
  18. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +1 -0
  19. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
  20. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.js +1 -0
  21. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.js.map +1 -1
  22. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +54 -0
  23. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
  24. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +4 -1
  25. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
  26. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js +28 -4
  27. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
  28. package/dist/esm/lib/hexclave-app/apps/implementations/common.js +1 -1
  29. package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.d.ts +1 -0
  30. package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.d.ts.map +1 -1
  31. package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.js.map +1 -1
  32. package/dist/generated/quetzal-translations.d.ts +2 -2
  33. package/dist/lib/auth.d.ts.map +1 -1
  34. package/dist/lib/auth.js +31 -10
  35. package/dist/lib/auth.js.map +1 -1
  36. package/dist/lib/auth.test.js +23 -8
  37. package/dist/lib/auth.test.js.map +1 -1
  38. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +1 -0
  39. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
  40. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.js +1 -0
  41. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.js.map +1 -1
  42. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +54 -0
  43. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
  44. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +4 -1
  45. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
  46. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js +27 -3
  47. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
  48. package/dist/lib/hexclave-app/apps/implementations/common.js +1 -1
  49. package/dist/lib/hexclave-app/apps/interfaces/admin-app.d.ts +1 -0
  50. package/dist/lib/hexclave-app/apps/interfaces/admin-app.d.ts.map +1 -1
  51. package/dist/lib/hexclave-app/apps/interfaces/admin-app.js.map +1 -1
  52. package/package.json +4 -4
  53. package/src/components-page/oauth-callback.test.tsx +109 -0
  54. package/src/components-page/oauth-callback.tsx +14 -19
  55. package/src/lib/auth.test.ts +32 -10
  56. package/src/lib/auth.ts +41 -7
  57. package/src/lib/hexclave-app/apps/implementations/admin-app-impl.ts +2 -1
  58. package/src/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.ts +66 -0
  59. package/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +39 -3
  60. package/src/lib/hexclave-app/apps/interfaces/admin-app.ts +1 -0
package/README.md CHANGED
@@ -1,10 +1,190 @@
1
- # This package has been renamed.
1
+ # Hexclave
2
+ <div align="center">
2
3
 
3
- Stack Auth is now Hexclave! The new packages are:
4
+ <img src=".github/assets/hexclave-header.svg" alt="Hexclave" width="320"/>
4
5
 
5
- - @hexclave/next
6
- - @hexclave/react
7
- - @hexclave/js
8
- - @hexclave/cli
6
+ <br/>
9
7
 
10
- See the [migration guide](https://docs.hexclave.com/migration) for more information.
8
+ **The user infrastructure platform.**
9
+
10
+ Hexclave handles everything around your users: authentication, teams,
11
+ payments, emails, analytics, and much more. Start in minutes on the hosted
12
+ cloud. Your data is always yours to export and self-host.
13
+
14
+ [Website](https://hexclave.com) · [Docs](https://docs.hexclave.com) · [Dashboard](https://app.hexclave.com) · [Discord](https://discord.hexclave.com)
15
+
16
+ ![License](https://img.shields.io/badge/license-MIT%20%2F%20AGPLv3-blue)
17
+ ![SDKs](https://img.shields.io/badge/SDKs-Next.js%20%C2%B7%20React%20%C2%B7%20JS-black)
18
+ ![Deploy](https://img.shields.io/badge/deploy-Cloud%20or%20self--hosted-success)
19
+
20
+ </div>
21
+
22
+ ---
23
+
24
+ <div align="center">
25
+ <img src=".github/assets/comparison.png" alt="Where Hexclave fits in the infrastructure stack" width="900"/>
26
+ </div>
27
+
28
+ ## Get started
29
+
30
+ Setting up Hexclave is one prompt. Paste this into your coding agent of choice:
31
+
32
+ ```text
33
+ Read skill.hexclave.com and help me setup hexclave in this project
34
+ ```
35
+
36
+ ## For AI agents
37
+
38
+ If you're an AI coding agent, use your web fetch tool or curl the skill documentation for up-to-date integration instructions:
39
+
40
+ ```
41
+ curl -sSL "https://skill.hexclave.com?question=<your-question>&context=<your-context>"
42
+ ```
43
+
44
+ ## What's included
45
+
46
+ Hexclave ships as a catalog of apps you switch on as your product needs them.
47
+ Each one is built on the same user model, and new apps land regularly.
48
+
49
+ <table><tr>
50
+ <td width="50%" valign="middle">
51
+
52
+ ### <img src=".github/assets/logos/authentication.png" alt="" width="40" align="top"/> &nbsp; Authentication
53
+
54
+ Authentication that just works with passkeys, OAuth, and CLI auth. Drop in one component and ship the whole flow; auth methods toggle from the dashboard with no code changes needed.
55
+
56
+ </td>
57
+ <td width="50%" valign="middle" align="center">
58
+ <img src=".github/assets/app-shots/authentication.png" width="520" alt="Authentication"/>
59
+ </td>
60
+ </tr></table>
61
+
62
+ <table><tr>
63
+ <td width="50%" valign="middle">
64
+
65
+ ### <img src=".github/assets/logos/teams.png" alt="" width="40" align="top"/> &nbsp; Teams
66
+
67
+ Build for teams, not just users, with workspaces, email invites, and roles that actually gate the work. The workspace switcher remembers selection, invites auto sign up new users, and permissions hold up under audit.
68
+
69
+ </td>
70
+ <td width="50%" valign="middle" align="center">
71
+ <img src=".github/assets/app-shots/teams.png" width="520" alt="Teams"/>
72
+ </td>
73
+ </tr></table>
74
+
75
+ <table><tr>
76
+ <td width="50%" valign="middle">
77
+
78
+ ### <img src=".github/assets/logos/rbac.png" alt="" width="40" align="top"/> &nbsp; RBAC
79
+
80
+ Permissions, sorted: roles that nest and one permission check that works the same on server or client. Define them in the dashboard, check them anywhere in your code.
81
+
82
+ </td>
83
+ <td width="50%" valign="middle" align="center">
84
+ <img src=".github/assets/app-shots/rbac.png" width="520" alt="RBAC"/>
85
+ </td>
86
+ </tr></table>
87
+
88
+ <table><tr>
89
+ <td width="50%" valign="middle">
90
+
91
+ ### <img src=".github/assets/logos/api-keys.png" alt="" width="40" align="top"/> &nbsp; API Keys
92
+
93
+ API keys without the footguns: leaked keys get auto-revoked, work for users and teams, and show the full secret only once. We never keep the plaintext after creation.
94
+
95
+ </td>
96
+ <td width="50%" valign="middle" align="center">
97
+ <img src=".github/assets/app-shots/api-keys.png" width="520" alt="API Keys"/>
98
+ </td>
99
+ </tr></table>
100
+
101
+ <table><tr>
102
+ <td width="50%" valign="middle">
103
+
104
+ ### <img src=".github/assets/logos/payments.png" alt="" width="40" align="top"/> &nbsp; Payments
105
+
106
+ Payments without the plumbing for subscriptions, one-time charges, and usage metering with credits. Bill a person or a whole team with one model, no separate codepath.
107
+
108
+ </td>
109
+ <td width="50%" valign="middle" align="center">
110
+ <img src=".github/assets/app-shots/payments.png" width="520" alt="Payments"/>
111
+ </td>
112
+ </tr></table>
113
+
114
+ <table><tr>
115
+ <td width="50%" valign="middle">
116
+
117
+ ### <img src=".github/assets/logos/emails.png" alt="" width="40" align="top"/> &nbsp; Emails
118
+
119
+ Email that delivers and tells you so, handling transactional and marketing sends from one API. Edit templates with an AI editor, theme once, and track every open and click.
120
+
121
+ </td>
122
+ <td width="50%" valign="middle" align="center">
123
+ <img src=".github/assets/app-shots/emails.png" width="520" alt="Emails"/>
124
+ </td>
125
+ </tr></table>
126
+
127
+ <table><tr>
128
+ <td width="50%" valign="middle">
129
+
130
+ ### <img src=".github/assets/logos/analytics.png" alt="" width="40" align="top"/> &nbsp; Analytics
131
+
132
+ Know your users with no data stack required, with live active user counts and session replays out of the box. Ask in plain English to build dashboards or write SQL to save queries, all with one flag enabled.
133
+
134
+ </td>
135
+ <td width="50%" valign="middle" align="center">
136
+ <img src=".github/assets/app-shots/analytics.png" width="520" alt="Analytics"/>
137
+ </td>
138
+ </tr></table>
139
+
140
+ <table><tr>
141
+ <td width="50%" valign="middle">
142
+
143
+ ### <img src=".github/assets/logos/webhooks.png" alt="" width="40" align="top"/> &nbsp; Webhooks
144
+
145
+ React to every user event in real time with signed, tamper-proof webhooks. Retries and backoff are handled for you; verify in five lines and manage endpoints from the dashboard.
146
+
147
+ </td>
148
+ <td width="50%" valign="middle" align="center">
149
+ <img src=".github/assets/app-shots/webhooks.png" width="520" alt="Webhooks"/>
150
+ </td>
151
+ </tr></table>
152
+
153
+ <table><tr>
154
+ <td width="50%" valign="middle">
155
+
156
+ ### <img src=".github/assets/logos/data-vault.png" alt="" width="40" align="top"/> &nbsp; Data Vault
157
+
158
+ A safe for the secrets your users hand you, locked with your secret so we never see the plaintext. Store and retrieve tokens in two lines each, server-only by design.
159
+
160
+ </td>
161
+ <td width="50%" valign="middle" align="center">
162
+ <img src=".github/assets/app-shots/data-vault.png" width="520" alt="Data Vault"/>
163
+ </td>
164
+ </tr></table>
165
+
166
+ <table><tr>
167
+ <td width="50%" valign="middle">
168
+
169
+ ### <img src=".github/assets/logos/launch-checklist.png" alt="" width="40" align="top"/> &nbsp; Launch Checklist
170
+
171
+ Run through the must-do checks before flipping to production: domain setup, callbacks locked, secrets rotated. The progress tracker keeps your team aligned so nothing critical slips through on launch day.
172
+
173
+ </td>
174
+ <td width="50%" valign="middle" align="center">
175
+ <img src=".github/assets/app-shots/launch-checklist.png" width="520" alt="Launch Checklist"/>
176
+ </td>
177
+ </tr></table>
178
+
179
+ ## Contributing
180
+
181
+ Hexclave is open source, and contributions are welcome. Read
182
+ [`CONTRIBUTING.md`](./CONTRIBUTING.md) to get started, and say hello in
183
+ [Discord](https://discord.hexclave.com) before picking up anything large.
184
+ Found a security issue? Email security@hexclave.com.
185
+
186
+ ## ❤ Contributors
187
+
188
+ <a href="https://github.com/hexclave/hexclave/graphs/contributors">
189
+ <img src="https://contrib.rocks/image?repo=hexclave/hexclave&columns=9" alt="Contributors" width="100%" />
190
+ </a>
@@ -12,7 +12,7 @@ let react_jsx_runtime = require("react/jsx-runtime");
12
12
  let _hexclave_shared = require("@hexclave/shared");
13
13
  let ___components_elements_maybe_full_page_js = require("../components/elements/maybe-full-page.js");
14
14
  let ___components_link_js = require("../components/link.js");
15
- let ___lib_hexclave_app_index_js = require("../lib/hexclave-app/index.js");
15
+ let __error_page_js = require("./error-page.js");
16
16
 
17
17
  //#region src/components-page/oauth-callback.tsx
18
18
  function OAuthCallback({ fullPage }) {
@@ -20,36 +20,32 @@ function OAuthCallback({ fullPage }) {
20
20
  const app = (0, ___index_js.useStackApp)();
21
21
  const called = (0, react.useRef)(false);
22
22
  const [showRedirectLink, setShowRedirectLink] = (0, react.useState)(false);
23
- const [redirectUrl, setRedirectUrl] = (0, react.useState)(null);
23
+ const [errorSearchParams, setErrorSearchParams] = (0, react.useState)(null);
24
24
  (0, react.useEffect)(() => (0, _hexclave_shared_dist_utils_promises.runAsynchronously)(async () => {
25
25
  if (called.current) return;
26
26
  called.current = true;
27
- const redirectToError = async (url) => {
28
- const urlString = url.toString();
29
- if (app[___lib_hexclave_app_index_js.hexclaveAppInternalsSymbol].getRedirectMethod() === "none") {
30
- setRedirectUrl(urlString);
31
- return;
32
- }
33
- await app[___lib_hexclave_app_index_js.hexclaveAppInternalsSymbol].redirectToUrl(urlString, { replace: true });
34
- };
35
27
  try {
36
28
  if (!await app.callOAuthCallback()) await app.redirectToSignIn({ noRedirectBack: true });
37
29
  } catch (e) {
38
30
  if (_hexclave_shared.KnownError.isKnownError(e)) {
39
- const errorUrl = new URL(app.urls.error, window.location.href);
40
- errorUrl.searchParams.set("errorCode", e.errorCode);
41
- errorUrl.searchParams.set("message", e.message);
42
- errorUrl.searchParams.set("details", JSON.stringify(e.details ?? {}));
43
- await redirectToError(errorUrl);
31
+ setErrorSearchParams({
32
+ errorCode: e.errorCode,
33
+ message: e.message,
34
+ details: JSON.stringify(e.details ?? {})
35
+ });
44
36
  return;
45
37
  }
46
38
  (0, _hexclave_shared_dist_utils_errors.captureError)("<OAuthCallback />", e);
47
- await redirectToError(new URL(app.urls.error, window.location.href));
39
+ setErrorSearchParams({});
48
40
  }
49
41
  }), [app]);
50
42
  (0, react.useEffect)(() => {
51
43
  setTimeout(() => setShowRedirectLink(true), 3e3);
52
44
  }, []);
45
+ if (errorSearchParams != null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__error_page_js.ErrorPage, {
46
+ searchParams: errorSearchParams,
47
+ fullPage
48
+ });
53
49
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(___components_elements_maybe_full_page_js.MaybeFullPage, {
54
50
  fullPage: fullPage ?? false,
55
51
  containerClassName: "flex items-center justify-center",
@@ -58,11 +54,10 @@ function OAuthCallback({ fullPage }) {
58
54
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
59
55
  className: "flex flex-col justify-center items-center gap-4",
60
56
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_hexclave_ui.Spinner, { size: 20 })
61
- }), showRedirectLink || redirectUrl != null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [t("If you are not redirected automatically, "), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(___components_link_js.StyledLink, {
57
+ }), showRedirectLink ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [t("If you are not redirected automatically, "), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(___components_link_js.StyledLink, {
62
58
  className: "whitespace-nowrap",
63
- href: redirectUrl ?? "#",
59
+ href: "#",
64
60
  onClick: (e) => {
65
- if (redirectUrl != null) return;
66
61
  e.preventDefault();
67
62
  (0, _hexclave_shared_dist_utils_promises.runAsynchronously)(app.redirectToHome());
68
63
  },
@@ -1 +1 @@
1
- {"version":3,"file":"oauth-callback.js","names":["hexclaveAppInternalsSymbol","KnownError","MaybeFullPage","Spinner","StyledLink"],"sources":["../../src/components-page/oauth-callback.tsx"],"sourcesContent":["'use client';\n\n\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\n\nimport { KnownError } from \"@hexclave/shared\";\nimport { captureError } from \"@hexclave/shared/dist/utils/errors\";\nimport { runAsynchronously } from \"@hexclave/shared/dist/utils/promises\";\nimport { Spinner, cn } from \"@hexclave/ui\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { useStackApp } from \"..\";\nimport { MaybeFullPage } from \"../components/elements/maybe-full-page\";\nimport { StyledLink } from \"../components/link\";\nimport { hexclaveAppInternalsSymbol } from \"../lib/hexclave-app\";\nimport { useTranslation } from \"../lib/translations\";\n\nexport function OAuthCallback({ fullPage }: { fullPage?: boolean }) {\n const { t } = useTranslation();\n const app = useStackApp();\n const called = useRef(false);\n const [showRedirectLink, setShowRedirectLink] = useState(false);\n const [redirectUrl, setRedirectUrl] = useState<string | null>(null);\n\n useEffect(() => runAsynchronously(async () => {\n if (called.current) return;\n called.current = true;\n const redirectToError = async (url: URL) => {\n const urlString = url.toString();\n if (app[hexclaveAppInternalsSymbol].getRedirectMethod() === \"none\") {\n setRedirectUrl(urlString);\n return;\n }\n await app[hexclaveAppInternalsSymbol].redirectToUrl(urlString, { replace: true });\n };\n try {\n const hasRedirected = await app.callOAuthCallback();\n if (!hasRedirected) {\n await app.redirectToSignIn({ noRedirectBack: true });\n }\n } catch (e) {\n if (KnownError.isKnownError(e)) {\n const errorUrl = new URL(app.urls.error, window.location.href);\n errorUrl.searchParams.set(\"errorCode\", e.errorCode);\n errorUrl.searchParams.set(\"message\", e.message);\n errorUrl.searchParams.set(\"details\", JSON.stringify(e.details ?? {}));\n await redirectToError(errorUrl);\n return;\n }\n captureError(\"<OAuthCallback />\", e);\n await redirectToError(new URL(app.urls.error, window.location.href));\n }\n }), [app]);\n\n useEffect(() => {\n setTimeout(() => setShowRedirectLink(true), 3000);\n }, []);\n\n return (\n <MaybeFullPage\n fullPage={fullPage ?? false}\n containerClassName=\"flex items-center justify-center\"\n >\n <div\n className={cn(\n \"text-center justify-center items-center stack-scope flex flex-col gap-4 max-w-[380px]\",\n fullPage ? \"p-4\" : \"p-0\"\n )}\n >\n <div className=\"flex flex-col justify-center items-center gap-4\">\n <Spinner size={20} />\n </div>\n {showRedirectLink || redirectUrl != null ? <p>{t('If you are not redirected automatically, ')}<StyledLink\n className=\"whitespace-nowrap\"\n href={redirectUrl ?? \"#\"}\n onClick={(e) => {\n if (redirectUrl != null) return;\n e.preventDefault();\n runAsynchronously(app.redirectToHome());\n }}\n >{t(\"click here\")}</StyledLink></p> : null}\n </div>\n </MaybeFullPage>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAkBA,SAAgB,cAAc,EAAE,YAAoC;CAClE,MAAM,EAAE,kDAAsB;CAC9B,MAAM,oCAAmB;CACzB,MAAM,2BAAgB,MAAM;CAC5B,MAAM,CAAC,kBAAkB,2CAAgC,MAAM;CAC/D,MAAM,CAAC,aAAa,sCAA0C,KAAK;AAEnE,wFAAkC,YAAY;AAC5C,MAAI,OAAO,QAAS;AACpB,SAAO,UAAU;EACjB,MAAM,kBAAkB,OAAO,QAAa;GAC1C,MAAM,YAAY,IAAI,UAAU;AAChC,OAAI,IAAIA,yDAA4B,mBAAmB,KAAK,QAAQ;AAClE,mBAAe,UAAU;AACzB;;AAEF,SAAM,IAAIA,yDAA4B,cAAc,WAAW,EAAE,SAAS,MAAM,CAAC;;AAEnF,MAAI;AAEF,OAAI,CADkB,MAAM,IAAI,mBAAmB,CAEjD,OAAM,IAAI,iBAAiB,EAAE,gBAAgB,MAAM,CAAC;WAE/C,GAAG;AACV,OAAIC,4BAAW,aAAa,EAAE,EAAE;IAC9B,MAAM,WAAW,IAAI,IAAI,IAAI,KAAK,OAAO,OAAO,SAAS,KAAK;AAC9D,aAAS,aAAa,IAAI,aAAa,EAAE,UAAU;AACnD,aAAS,aAAa,IAAI,WAAW,EAAE,QAAQ;AAC/C,aAAS,aAAa,IAAI,WAAW,KAAK,UAAU,EAAE,WAAW,EAAE,CAAC,CAAC;AACrE,UAAM,gBAAgB,SAAS;AAC/B;;AAEF,wDAAa,qBAAqB,EAAE;AACpC,SAAM,gBAAgB,IAAI,IAAI,IAAI,KAAK,OAAO,OAAO,SAAS,KAAK,CAAC;;GAEtE,EAAE,CAAC,IAAI,CAAC;AAEV,4BAAgB;AACd,mBAAiB,oBAAoB,KAAK,EAAE,IAAK;IAChD,EAAE,CAAC;AAEN,QACE,2CAACC;EACC,UAAU,YAAY;EACtB,oBAAmB;YAEnB,4CAAC;GACC,gCACE,yFACA,WAAW,QAAQ,MACpB;cAED,2CAAC;IAAI,WAAU;cACb,2CAACC,wBAAQ,MAAM,KAAM;KACjB,EACL,oBAAoB,eAAe,OAAO,4CAAC,kBAAG,EAAE,4CAA4C,EAAC,2CAACC;IAC7F,WAAU;IACV,MAAM,eAAe;IACrB,UAAU,MAAM;AACd,SAAI,eAAe,KAAM;AACzB,OAAE,gBAAgB;AAClB,iEAAkB,IAAI,gBAAgB,CAAC;;cAEzC,EAAE,aAAa;KAAc,IAAI,GAAG;IAClC;GACQ"}
1
+ {"version":3,"file":"oauth-callback.js","names":["KnownError","ErrorPage","MaybeFullPage","Spinner","StyledLink"],"sources":["../../src/components-page/oauth-callback.tsx"],"sourcesContent":["'use client';\n\n\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\n\nimport { KnownError } from \"@hexclave/shared\";\nimport { captureError } from \"@hexclave/shared/dist/utils/errors\";\nimport { runAsynchronously } from \"@hexclave/shared/dist/utils/promises\";\nimport { Spinner, cn } from \"@hexclave/ui\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { useStackApp } from \"..\";\nimport { MaybeFullPage } from \"../components/elements/maybe-full-page\";\nimport { StyledLink } from \"../components/link\";\nimport { useTranslation } from \"../lib/translations\";\nimport { ErrorPage } from \"./error-page\";\n\nexport function OAuthCallback({ fullPage }: { fullPage?: boolean }) {\n const { t } = useTranslation();\n const app = useStackApp();\n const called = useRef(false);\n const [showRedirectLink, setShowRedirectLink] = useState(false);\n const [errorSearchParams, setErrorSearchParams] = useState<Record<string, string> | null>(null);\n\n useEffect(() => runAsynchronously(async () => {\n if (called.current) return;\n called.current = true;\n try {\n const hasRedirected = await app.callOAuthCallback();\n if (!hasRedirected) {\n await app.redirectToSignIn({ noRedirectBack: true });\n }\n } catch (e) {\n if (KnownError.isKnownError(e)) {\n setErrorSearchParams({\n errorCode: e.errorCode,\n message: e.message,\n details: JSON.stringify(e.details ?? {}),\n });\n return;\n }\n captureError(\"<OAuthCallback />\", e);\n setErrorSearchParams({});\n }\n }), [app]);\n\n useEffect(() => {\n setTimeout(() => setShowRedirectLink(true), 3000);\n }, []);\n\n if (errorSearchParams != null) {\n return <ErrorPage searchParams={errorSearchParams} fullPage={fullPage} />;\n }\n\n return (\n <MaybeFullPage\n fullPage={fullPage ?? false}\n containerClassName=\"flex items-center justify-center\"\n >\n <div\n className={cn(\n \"text-center justify-center items-center stack-scope flex flex-col gap-4 max-w-[380px]\",\n fullPage ? \"p-4\" : \"p-0\"\n )}\n >\n <div className=\"flex flex-col justify-center items-center gap-4\">\n <Spinner size={20} />\n </div>\n {showRedirectLink ? <p>{t('If you are not redirected automatically, ')}<StyledLink\n className=\"whitespace-nowrap\"\n href=\"#\"\n onClick={(e) => {\n e.preventDefault();\n runAsynchronously(app.redirectToHome());\n }}\n >{t(\"click here\")}</StyledLink></p> : null}\n </div>\n </MaybeFullPage>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAkBA,SAAgB,cAAc,EAAE,YAAoC;CAClE,MAAM,EAAE,kDAAsB;CAC9B,MAAM,oCAAmB;CACzB,MAAM,2BAAgB,MAAM;CAC5B,MAAM,CAAC,kBAAkB,2CAAgC,MAAM;CAC/D,MAAM,CAAC,mBAAmB,4CAAgE,KAAK;AAE/F,wFAAkC,YAAY;AAC5C,MAAI,OAAO,QAAS;AACpB,SAAO,UAAU;AACjB,MAAI;AAEF,OAAI,CADkB,MAAM,IAAI,mBAAmB,CAEjD,OAAM,IAAI,iBAAiB,EAAE,gBAAgB,MAAM,CAAC;WAE/C,GAAG;AACV,OAAIA,4BAAW,aAAa,EAAE,EAAE;AAC9B,yBAAqB;KACnB,WAAW,EAAE;KACb,SAAS,EAAE;KACX,SAAS,KAAK,UAAU,EAAE,WAAW,EAAE,CAAC;KACzC,CAAC;AACF;;AAEF,wDAAa,qBAAqB,EAAE;AACpC,wBAAqB,EAAE,CAAC;;GAE1B,EAAE,CAAC,IAAI,CAAC;AAEV,4BAAgB;AACd,mBAAiB,oBAAoB,KAAK,EAAE,IAAK;IAChD,EAAE,CAAC;AAEN,KAAI,qBAAqB,KACvB,QAAO,2CAACC;EAAU,cAAc;EAA6B;GAAY;AAG3E,QACE,2CAACC;EACC,UAAU,YAAY;EACtB,oBAAmB;YAEnB,4CAAC;GACC,gCACE,yFACA,WAAW,QAAQ,MACpB;cAED,2CAAC;IAAI,WAAU;cACb,2CAACC,wBAAQ,MAAM,KAAM;KACjB,EACL,mBAAmB,4CAAC,kBAAG,EAAE,4CAA4C,EAAC,2CAACC;IACtE,WAAU;IACV,MAAK;IACL,UAAU,MAAM;AACd,OAAE,gBAAgB;AAClB,iEAAkB,IAAI,gBAAgB,CAAC;;cAEzC,EAAE,aAAa;KAAc,IAAI,GAAG;IAClC;GACQ"}
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,90 @@
1
+ const require_chunk = require('../chunk-BE-pF4vm.js');
2
+ let react = require("react");
3
+ react = require_chunk.__toESM(react);
4
+ let react_jsx_runtime = require("react/jsx-runtime");
5
+ let _hexclave_shared = require("@hexclave/shared");
6
+ let react_dom_client = require("react-dom/client");
7
+ let vitest = require("vitest");
8
+ let __oauth_callback_js = require("./oauth-callback.js");
9
+ let ___providers_translation_provider_client_js = require("../providers/translation-provider-client.js");
10
+
11
+ //#region src/components-page/oauth-callback.test.tsx
12
+ // @vitest-environment jsdom
13
+ const appMockState = vitest.vi.hoisted(() => ({ app: null }));
14
+ vitest.vi.mock("..", () => ({ useStackApp: () => {
15
+ if (appMockState.app == null) throw new Error("Expected test app to be set before rendering.");
16
+ return appMockState.app;
17
+ } }));
18
+ vitest.vi.mock("@hexclave/ui", async () => {
19
+ await import("react");
20
+ return {
21
+ Button: (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
22
+ type: "button",
23
+ onClick: props.onClick,
24
+ children: props.children
25
+ }),
26
+ Spinner: () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { "data-testid": "spinner" }),
27
+ Typography: (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: props.children }),
28
+ cn: (...classes) => classes.filter(Boolean).join(" ")
29
+ };
30
+ });
31
+ const previousActEnvironment = Reflect.get(globalThis, "IS_REACT_ACT_ENVIRONMENT");
32
+ function createAppTestDouble(options) {
33
+ return {
34
+ callOAuthCallback: options.callOAuthCallback,
35
+ redirectToSignIn: vitest.vi.fn(async () => {}),
36
+ redirectToHome: vitest.vi.fn(async () => {})
37
+ };
38
+ }
39
+ let root = null;
40
+ let container = null;
41
+ async function renderWithApp(app) {
42
+ appMockState.app = app;
43
+ container = document.createElement("div");
44
+ document.body.append(container);
45
+ root = (0, react_dom_client.createRoot)(container);
46
+ await (0, react.act)(async () => {
47
+ root?.render(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(___providers_translation_provider_client_js.TranslationProviderClient, {
48
+ quetzalKeys: /* @__PURE__ */ new Map(),
49
+ quetzalLocale: /* @__PURE__ */ new Map(),
50
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__oauth_callback_js.OAuthCallback, {})
51
+ }));
52
+ });
53
+ }
54
+ async function flushEffects() {
55
+ await (0, react.act)(async () => {
56
+ await new Promise((resolve) => setTimeout(resolve, 0));
57
+ });
58
+ }
59
+ (0, vitest.describe)("OAuthCallback", () => {
60
+ (0, vitest.beforeEach)(() => {
61
+ Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", true);
62
+ });
63
+ (0, vitest.afterEach)(() => {
64
+ (0, react.act)(() => {
65
+ root?.unmount();
66
+ });
67
+ container?.remove();
68
+ root = null;
69
+ container = null;
70
+ appMockState.app = null;
71
+ vitest.vi.restoreAllMocks();
72
+ Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", previousActEnvironment);
73
+ });
74
+ (0, vitest.it)("renders backend-encoded OAuth callback errors on the callback page", async () => {
75
+ const errorMessage = "Your sign up was rejected by an administrator's sign-up rule.";
76
+ const callOAuthCallback = vitest.vi.fn(async () => {
77
+ throw new _hexclave_shared.KnownErrors.SignUpRejected(errorMessage);
78
+ });
79
+ const app = createAppTestDouble({ callOAuthCallback });
80
+ await renderWithApp(app);
81
+ await flushEffects();
82
+ (0, vitest.expect)(callOAuthCallback).toHaveBeenCalledOnce();
83
+ (0, vitest.expect)(container?.textContent).toContain("SIGN_UP_REJECTED");
84
+ (0, vitest.expect)(container?.textContent).toContain(errorMessage);
85
+ (0, vitest.expect)(app.redirectToSignIn).not.toHaveBeenCalled();
86
+ });
87
+ });
88
+
89
+ //#endregion
90
+ //# sourceMappingURL=oauth-callback.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-callback.test.js","names":["vi","TranslationProviderClient","OAuthCallback","KnownErrors"],"sources":["../../src/components-page/oauth-callback.test.tsx"],"sourcesContent":["\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\n// @vitest-environment jsdom\n\nimport { KnownErrors } from \"@hexclave/shared\";\nimport React, { act } from \"react\";\nimport { createRoot, type Root } from \"react-dom/client\";\nimport { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\nimport type { StackClientApp } from \"../lib/hexclave-app/apps/interfaces/client-app\";\nimport { TranslationProviderClient } from \"../providers/translation-provider-client\";\nimport { OAuthCallback } from \"./oauth-callback\";\n\nconst appMockState = vi.hoisted(() => ({ app: null as unknown }));\n\nvi.mock(\"..\", () => ({\n useStackApp: () => {\n if (appMockState.app == null) {\n throw new Error(\"Expected test app to be set before rendering.\");\n }\n return appMockState.app;\n },\n}));\n\nvi.mock(\"@hexclave/ui\", async () => {\n const React = await import(\"react\");\n return {\n Button: (props: { children: React.ReactNode, onClick?: () => void }) => (\n <button type=\"button\" onClick={props.onClick}>{props.children}</button>\n ),\n Spinner: () => <div data-testid=\"spinner\" />,\n Typography: (props: { children: React.ReactNode }) => <div>{props.children}</div>,\n cn: (...classes: (string | false | null | undefined)[]) => classes.filter(Boolean).join(\" \"),\n };\n});\n\nconst previousActEnvironment = Reflect.get(globalThis, \"IS_REACT_ACT_ENVIRONMENT\");\n\nfunction createAppTestDouble(options: {\n callOAuthCallback: () => Promise<boolean>,\n}) {\n const app = {\n callOAuthCallback: options.callOAuthCallback,\n redirectToSignIn: vi.fn(async () => {}),\n redirectToHome: vi.fn(async () => {}),\n };\n\n // This test double intentionally implements only the StackClientApp surface\n // that OAuthCallback and the rendered error card touch.\n return app as unknown as StackClientApp<true>;\n}\n\nlet root: Root | null = null;\nlet container: HTMLDivElement | null = null;\n\nasync function renderWithApp(app: StackClientApp<true>) {\n appMockState.app = app;\n container = document.createElement(\"div\");\n document.body.append(container);\n root = createRoot(container);\n await act(async () => {\n root?.render(\n <TranslationProviderClient quetzalKeys={new Map()} quetzalLocale={new Map()}>\n <OAuthCallback />\n </TranslationProviderClient>\n );\n });\n}\n\nasync function flushEffects() {\n await act(async () => {\n await new Promise((resolve) => setTimeout(resolve, 0));\n });\n}\n\ndescribe(\"OAuthCallback\", () => {\n beforeEach(() => {\n Reflect.set(globalThis, \"IS_REACT_ACT_ENVIRONMENT\", true);\n });\n\n afterEach(() => {\n act(() => {\n root?.unmount();\n });\n container?.remove();\n root = null;\n container = null;\n appMockState.app = null;\n vi.restoreAllMocks();\n Reflect.set(globalThis, \"IS_REACT_ACT_ENVIRONMENT\", previousActEnvironment);\n });\n\n it(\"renders backend-encoded OAuth callback errors on the callback page\", async () => {\n const errorMessage = \"Your sign up was rejected by an administrator's sign-up rule.\";\n const callOAuthCallback = vi.fn(async () => {\n throw new KnownErrors.SignUpRejected(errorMessage);\n });\n const app = createAppTestDouble({ callOAuthCallback });\n\n await renderWithApp(app);\n await flushEffects();\n\n expect(callOAuthCallback).toHaveBeenCalledOnce();\n expect(container?.textContent).toContain(\"SIGN_UP_REJECTED\");\n expect(container?.textContent).toContain(errorMessage);\n expect(app.redirectToSignIn).not.toHaveBeenCalled();\n });\n});\n"],"mappings":";;;;;;;;;;;;AAcA,MAAM,eAAeA,UAAG,eAAe,EAAE,KAAK,MAAiB,EAAE;AAEjEA,UAAG,KAAK,aAAa,EACnB,mBAAmB;AACjB,KAAI,aAAa,OAAO,KACtB,OAAM,IAAI,MAAM,gDAAgD;AAElE,QAAO,aAAa;GAEvB,EAAE;AAEHA,UAAG,KAAK,gBAAgB,YAAY;AACpB,OAAM,OAAO;AAC3B,QAAO;EACL,SAAS,UACP,2CAAC;GAAO,MAAK;GAAS,SAAS,MAAM;aAAU,MAAM;IAAkB;EAEzE,eAAe,2CAAC,SAAI,eAAY,YAAY;EAC5C,aAAa,UAAyC,2CAAC,mBAAK,MAAM,WAAe;EACjF,KAAK,GAAG,YAAmD,QAAQ,OAAO,QAAQ,CAAC,KAAK,IAAI;EAC7F;EACD;AAEF,MAAM,yBAAyB,QAAQ,IAAI,YAAY,2BAA2B;AAElF,SAAS,oBAAoB,SAE1B;AASD,QARY;EACV,mBAAmB,QAAQ;EAC3B,kBAAkBA,UAAG,GAAG,YAAY,GAAG;EACvC,gBAAgBA,UAAG,GAAG,YAAY,GAAG;EACtC;;AAOH,IAAI,OAAoB;AACxB,IAAI,YAAmC;AAEvC,eAAe,cAAc,KAA2B;AACtD,cAAa,MAAM;AACnB,aAAY,SAAS,cAAc,MAAM;AACzC,UAAS,KAAK,OAAO,UAAU;AAC/B,yCAAkB,UAAU;AAC5B,sBAAU,YAAY;AACpB,QAAM,OACJ,2CAACC;GAA0B,6BAAa,IAAI,KAAK;GAAE,+BAAe,IAAI,KAAK;aACzE,2CAACC,sCAAgB;IACS,CAC7B;GACD;;AAGJ,eAAe,eAAe;AAC5B,sBAAU,YAAY;AACpB,QAAM,IAAI,SAAS,YAAY,WAAW,SAAS,EAAE,CAAC;GACtD;;qBAGK,uBAAuB;AAC9B,8BAAiB;AACf,UAAQ,IAAI,YAAY,4BAA4B,KAAK;GACzD;AAEF,6BAAgB;AACd,uBAAU;AACR,SAAM,SAAS;IACf;AACF,aAAW,QAAQ;AACnB,SAAO;AACP,cAAY;AACZ,eAAa,MAAM;AACnB,YAAG,iBAAiB;AACpB,UAAQ,IAAI,YAAY,4BAA4B,uBAAuB;GAC3E;AAEF,gBAAG,sEAAsE,YAAY;EACnF,MAAM,eAAe;EACrB,MAAM,oBAAoBF,UAAG,GAAG,YAAY;AAC1C,SAAM,IAAIG,6BAAY,eAAe,aAAa;IAClD;EACF,MAAM,MAAM,oBAAoB,EAAE,mBAAmB,CAAC;AAEtD,QAAM,cAAc,IAAI;AACxB,QAAM,cAAc;AAEpB,qBAAO,kBAAkB,CAAC,sBAAsB;AAChD,qBAAO,WAAW,YAAY,CAAC,UAAU,mBAAmB;AAC5D,qBAAO,WAAW,YAAY,CAAC,UAAU,aAAa;AACtD,qBAAO,IAAI,iBAAiB,CAAC,IAAI,kBAAkB;GACnD;EACF"}
@@ -10,7 +10,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
10
10
  import { KnownError } from "@hexclave/shared";
11
11
  import { MaybeFullPage } from "../components/elements/maybe-full-page.js";
12
12
  import { StyledLink } from "../components/link.js";
13
- import { hexclaveAppInternalsSymbol } from "../lib/hexclave-app/index.js";
13
+ import { ErrorPage } from "./error-page.js";
14
14
 
15
15
  //#region src/components-page/oauth-callback.tsx
16
16
  function OAuthCallback({ fullPage }) {
@@ -18,36 +18,32 @@ function OAuthCallback({ fullPage }) {
18
18
  const app = useStackApp();
19
19
  const called = useRef(false);
20
20
  const [showRedirectLink, setShowRedirectLink] = useState(false);
21
- const [redirectUrl, setRedirectUrl] = useState(null);
21
+ const [errorSearchParams, setErrorSearchParams] = useState(null);
22
22
  useEffect(() => runAsynchronously(async () => {
23
23
  if (called.current) return;
24
24
  called.current = true;
25
- const redirectToError = async (url) => {
26
- const urlString = url.toString();
27
- if (app[hexclaveAppInternalsSymbol].getRedirectMethod() === "none") {
28
- setRedirectUrl(urlString);
29
- return;
30
- }
31
- await app[hexclaveAppInternalsSymbol].redirectToUrl(urlString, { replace: true });
32
- };
33
25
  try {
34
26
  if (!await app.callOAuthCallback()) await app.redirectToSignIn({ noRedirectBack: true });
35
27
  } catch (e) {
36
28
  if (KnownError.isKnownError(e)) {
37
- const errorUrl = new URL(app.urls.error, window.location.href);
38
- errorUrl.searchParams.set("errorCode", e.errorCode);
39
- errorUrl.searchParams.set("message", e.message);
40
- errorUrl.searchParams.set("details", JSON.stringify(e.details ?? {}));
41
- await redirectToError(errorUrl);
29
+ setErrorSearchParams({
30
+ errorCode: e.errorCode,
31
+ message: e.message,
32
+ details: JSON.stringify(e.details ?? {})
33
+ });
42
34
  return;
43
35
  }
44
36
  captureError("<OAuthCallback />", e);
45
- await redirectToError(new URL(app.urls.error, window.location.href));
37
+ setErrorSearchParams({});
46
38
  }
47
39
  }), [app]);
48
40
  useEffect(() => {
49
41
  setTimeout(() => setShowRedirectLink(true), 3e3);
50
42
  }, []);
43
+ if (errorSearchParams != null) return /* @__PURE__ */ jsx(ErrorPage, {
44
+ searchParams: errorSearchParams,
45
+ fullPage
46
+ });
51
47
  return /* @__PURE__ */ jsx(MaybeFullPage, {
52
48
  fullPage: fullPage ?? false,
53
49
  containerClassName: "flex items-center justify-center",
@@ -56,11 +52,10 @@ function OAuthCallback({ fullPage }) {
56
52
  children: [/* @__PURE__ */ jsx("div", {
57
53
  className: "flex flex-col justify-center items-center gap-4",
58
54
  children: /* @__PURE__ */ jsx(Spinner, { size: 20 })
59
- }), showRedirectLink || redirectUrl != null ? /* @__PURE__ */ jsxs("p", { children: [t("If you are not redirected automatically, "), /* @__PURE__ */ jsx(StyledLink, {
55
+ }), showRedirectLink ? /* @__PURE__ */ jsxs("p", { children: [t("If you are not redirected automatically, "), /* @__PURE__ */ jsx(StyledLink, {
60
56
  className: "whitespace-nowrap",
61
- href: redirectUrl ?? "#",
57
+ href: "#",
62
58
  onClick: (e) => {
63
- if (redirectUrl != null) return;
64
59
  e.preventDefault();
65
60
  runAsynchronously(app.redirectToHome());
66
61
  },
@@ -1 +1 @@
1
- {"version":3,"file":"oauth-callback.js","names":[],"sources":["../../../src/components-page/oauth-callback.tsx"],"sourcesContent":["'use client';\n\n\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\n\nimport { KnownError } from \"@hexclave/shared\";\nimport { captureError } from \"@hexclave/shared/dist/utils/errors\";\nimport { runAsynchronously } from \"@hexclave/shared/dist/utils/promises\";\nimport { Spinner, cn } from \"@hexclave/ui\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { useStackApp } from \"..\";\nimport { MaybeFullPage } from \"../components/elements/maybe-full-page\";\nimport { StyledLink } from \"../components/link\";\nimport { hexclaveAppInternalsSymbol } from \"../lib/hexclave-app\";\nimport { useTranslation } from \"../lib/translations\";\n\nexport function OAuthCallback({ fullPage }: { fullPage?: boolean }) {\n const { t } = useTranslation();\n const app = useStackApp();\n const called = useRef(false);\n const [showRedirectLink, setShowRedirectLink] = useState(false);\n const [redirectUrl, setRedirectUrl] = useState<string | null>(null);\n\n useEffect(() => runAsynchronously(async () => {\n if (called.current) return;\n called.current = true;\n const redirectToError = async (url: URL) => {\n const urlString = url.toString();\n if (app[hexclaveAppInternalsSymbol].getRedirectMethod() === \"none\") {\n setRedirectUrl(urlString);\n return;\n }\n await app[hexclaveAppInternalsSymbol].redirectToUrl(urlString, { replace: true });\n };\n try {\n const hasRedirected = await app.callOAuthCallback();\n if (!hasRedirected) {\n await app.redirectToSignIn({ noRedirectBack: true });\n }\n } catch (e) {\n if (KnownError.isKnownError(e)) {\n const errorUrl = new URL(app.urls.error, window.location.href);\n errorUrl.searchParams.set(\"errorCode\", e.errorCode);\n errorUrl.searchParams.set(\"message\", e.message);\n errorUrl.searchParams.set(\"details\", JSON.stringify(e.details ?? {}));\n await redirectToError(errorUrl);\n return;\n }\n captureError(\"<OAuthCallback />\", e);\n await redirectToError(new URL(app.urls.error, window.location.href));\n }\n }), [app]);\n\n useEffect(() => {\n setTimeout(() => setShowRedirectLink(true), 3000);\n }, []);\n\n return (\n <MaybeFullPage\n fullPage={fullPage ?? false}\n containerClassName=\"flex items-center justify-center\"\n >\n <div\n className={cn(\n \"text-center justify-center items-center stack-scope flex flex-col gap-4 max-w-[380px]\",\n fullPage ? \"p-4\" : \"p-0\"\n )}\n >\n <div className=\"flex flex-col justify-center items-center gap-4\">\n <Spinner size={20} />\n </div>\n {showRedirectLink || redirectUrl != null ? <p>{t('If you are not redirected automatically, ')}<StyledLink\n className=\"whitespace-nowrap\"\n href={redirectUrl ?? \"#\"}\n onClick={(e) => {\n if (redirectUrl != null) return;\n e.preventDefault();\n runAsynchronously(app.redirectToHome());\n }}\n >{t(\"click here\")}</StyledLink></p> : null}\n </div>\n </MaybeFullPage>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;AAkBA,SAAgB,cAAc,EAAE,YAAoC;CAClE,MAAM,EAAE,MAAM,gBAAgB;CAC9B,MAAM,MAAM,aAAa;CACzB,MAAM,SAAS,OAAO,MAAM;CAC5B,MAAM,CAAC,kBAAkB,uBAAuB,SAAS,MAAM;CAC/D,MAAM,CAAC,aAAa,kBAAkB,SAAwB,KAAK;AAEnE,iBAAgB,kBAAkB,YAAY;AAC5C,MAAI,OAAO,QAAS;AACpB,SAAO,UAAU;EACjB,MAAM,kBAAkB,OAAO,QAAa;GAC1C,MAAM,YAAY,IAAI,UAAU;AAChC,OAAI,IAAI,4BAA4B,mBAAmB,KAAK,QAAQ;AAClE,mBAAe,UAAU;AACzB;;AAEF,SAAM,IAAI,4BAA4B,cAAc,WAAW,EAAE,SAAS,MAAM,CAAC;;AAEnF,MAAI;AAEF,OAAI,CADkB,MAAM,IAAI,mBAAmB,CAEjD,OAAM,IAAI,iBAAiB,EAAE,gBAAgB,MAAM,CAAC;WAE/C,GAAG;AACV,OAAI,WAAW,aAAa,EAAE,EAAE;IAC9B,MAAM,WAAW,IAAI,IAAI,IAAI,KAAK,OAAO,OAAO,SAAS,KAAK;AAC9D,aAAS,aAAa,IAAI,aAAa,EAAE,UAAU;AACnD,aAAS,aAAa,IAAI,WAAW,EAAE,QAAQ;AAC/C,aAAS,aAAa,IAAI,WAAW,KAAK,UAAU,EAAE,WAAW,EAAE,CAAC,CAAC;AACrE,UAAM,gBAAgB,SAAS;AAC/B;;AAEF,gBAAa,qBAAqB,EAAE;AACpC,SAAM,gBAAgB,IAAI,IAAI,IAAI,KAAK,OAAO,OAAO,SAAS,KAAK,CAAC;;GAEtE,EAAE,CAAC,IAAI,CAAC;AAEV,iBAAgB;AACd,mBAAiB,oBAAoB,KAAK,EAAE,IAAK;IAChD,EAAE,CAAC;AAEN,QACE,oBAAC;EACC,UAAU,YAAY;EACtB,oBAAmB;YAEnB,qBAAC;GACC,WAAW,GACT,yFACA,WAAW,QAAQ,MACpB;cAED,oBAAC;IAAI,WAAU;cACb,oBAAC,WAAQ,MAAM,KAAM;KACjB,EACL,oBAAoB,eAAe,OAAO,qBAAC,kBAAG,EAAE,4CAA4C,EAAC,oBAAC;IAC7F,WAAU;IACV,MAAM,eAAe;IACrB,UAAU,MAAM;AACd,SAAI,eAAe,KAAM;AACzB,OAAE,gBAAgB;AAClB,uBAAkB,IAAI,gBAAgB,CAAC;;cAEzC,EAAE,aAAa;KAAc,IAAI,GAAG;IAClC;GACQ"}
1
+ {"version":3,"file":"oauth-callback.js","names":[],"sources":["../../../src/components-page/oauth-callback.tsx"],"sourcesContent":["'use client';\n\n\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\n\nimport { KnownError } from \"@hexclave/shared\";\nimport { captureError } from \"@hexclave/shared/dist/utils/errors\";\nimport { runAsynchronously } from \"@hexclave/shared/dist/utils/promises\";\nimport { Spinner, cn } from \"@hexclave/ui\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { useStackApp } from \"..\";\nimport { MaybeFullPage } from \"../components/elements/maybe-full-page\";\nimport { StyledLink } from \"../components/link\";\nimport { useTranslation } from \"../lib/translations\";\nimport { ErrorPage } from \"./error-page\";\n\nexport function OAuthCallback({ fullPage }: { fullPage?: boolean }) {\n const { t } = useTranslation();\n const app = useStackApp();\n const called = useRef(false);\n const [showRedirectLink, setShowRedirectLink] = useState(false);\n const [errorSearchParams, setErrorSearchParams] = useState<Record<string, string> | null>(null);\n\n useEffect(() => runAsynchronously(async () => {\n if (called.current) return;\n called.current = true;\n try {\n const hasRedirected = await app.callOAuthCallback();\n if (!hasRedirected) {\n await app.redirectToSignIn({ noRedirectBack: true });\n }\n } catch (e) {\n if (KnownError.isKnownError(e)) {\n setErrorSearchParams({\n errorCode: e.errorCode,\n message: e.message,\n details: JSON.stringify(e.details ?? {}),\n });\n return;\n }\n captureError(\"<OAuthCallback />\", e);\n setErrorSearchParams({});\n }\n }), [app]);\n\n useEffect(() => {\n setTimeout(() => setShowRedirectLink(true), 3000);\n }, []);\n\n if (errorSearchParams != null) {\n return <ErrorPage searchParams={errorSearchParams} fullPage={fullPage} />;\n }\n\n return (\n <MaybeFullPage\n fullPage={fullPage ?? false}\n containerClassName=\"flex items-center justify-center\"\n >\n <div\n className={cn(\n \"text-center justify-center items-center stack-scope flex flex-col gap-4 max-w-[380px]\",\n fullPage ? \"p-4\" : \"p-0\"\n )}\n >\n <div className=\"flex flex-col justify-center items-center gap-4\">\n <Spinner size={20} />\n </div>\n {showRedirectLink ? <p>{t('If you are not redirected automatically, ')}<StyledLink\n className=\"whitespace-nowrap\"\n href=\"#\"\n onClick={(e) => {\n e.preventDefault();\n runAsynchronously(app.redirectToHome());\n }}\n >{t(\"click here\")}</StyledLink></p> : null}\n </div>\n </MaybeFullPage>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;AAkBA,SAAgB,cAAc,EAAE,YAAoC;CAClE,MAAM,EAAE,MAAM,gBAAgB;CAC9B,MAAM,MAAM,aAAa;CACzB,MAAM,SAAS,OAAO,MAAM;CAC5B,MAAM,CAAC,kBAAkB,uBAAuB,SAAS,MAAM;CAC/D,MAAM,CAAC,mBAAmB,wBAAwB,SAAwC,KAAK;AAE/F,iBAAgB,kBAAkB,YAAY;AAC5C,MAAI,OAAO,QAAS;AACpB,SAAO,UAAU;AACjB,MAAI;AAEF,OAAI,CADkB,MAAM,IAAI,mBAAmB,CAEjD,OAAM,IAAI,iBAAiB,EAAE,gBAAgB,MAAM,CAAC;WAE/C,GAAG;AACV,OAAI,WAAW,aAAa,EAAE,EAAE;AAC9B,yBAAqB;KACnB,WAAW,EAAE;KACb,SAAS,EAAE;KACX,SAAS,KAAK,UAAU,EAAE,WAAW,EAAE,CAAC;KACzC,CAAC;AACF;;AAEF,gBAAa,qBAAqB,EAAE;AACpC,wBAAqB,EAAE,CAAC;;GAE1B,EAAE,CAAC,IAAI,CAAC;AAEV,iBAAgB;AACd,mBAAiB,oBAAoB,KAAK,EAAE,IAAK;IAChD,EAAE,CAAC;AAEN,KAAI,qBAAqB,KACvB,QAAO,oBAAC;EAAU,cAAc;EAA6B;GAAY;AAG3E,QACE,oBAAC;EACC,UAAU,YAAY;EACtB,oBAAmB;YAEnB,qBAAC;GACC,WAAW,GACT,yFACA,WAAW,QAAQ,MACpB;cAED,oBAAC;IAAI,WAAU;cACb,oBAAC,WAAQ,MAAM,KAAM;KACjB,EACL,mBAAmB,qBAAC,kBAAG,EAAE,4CAA4C,EAAC,oBAAC;IACtE,WAAU;IACV,MAAK;IACL,UAAU,MAAM;AACd,OAAE,gBAAgB;AAClB,uBAAkB,IAAI,gBAAgB,CAAC;;cAEzC,EAAE,aAAa;KAAc,IAAI,GAAG;IAClC;GACQ"}
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,89 @@
1
+ import React, { act } from "react";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { KnownErrors } from "@hexclave/shared";
4
+ import { createRoot } from "react-dom/client";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+ import { OAuthCallback } from "./oauth-callback.js";
7
+ import { TranslationProviderClient } from "../providers/translation-provider-client.js";
8
+
9
+ //#region src/components-page/oauth-callback.test.tsx
10
+ // @vitest-environment jsdom
11
+ const appMockState = vi.hoisted(() => ({ app: null }));
12
+ vi.mock("..", () => ({ useStackApp: () => {
13
+ if (appMockState.app == null) throw new Error("Expected test app to be set before rendering.");
14
+ return appMockState.app;
15
+ } }));
16
+ vi.mock("@hexclave/ui", async () => {
17
+ await import("react");
18
+ return {
19
+ Button: (props) => /* @__PURE__ */ jsx("button", {
20
+ type: "button",
21
+ onClick: props.onClick,
22
+ children: props.children
23
+ }),
24
+ Spinner: () => /* @__PURE__ */ jsx("div", { "data-testid": "spinner" }),
25
+ Typography: (props) => /* @__PURE__ */ jsx("div", { children: props.children }),
26
+ cn: (...classes) => classes.filter(Boolean).join(" ")
27
+ };
28
+ });
29
+ const previousActEnvironment = Reflect.get(globalThis, "IS_REACT_ACT_ENVIRONMENT");
30
+ function createAppTestDouble(options) {
31
+ return {
32
+ callOAuthCallback: options.callOAuthCallback,
33
+ redirectToSignIn: vi.fn(async () => {}),
34
+ redirectToHome: vi.fn(async () => {})
35
+ };
36
+ }
37
+ let root = null;
38
+ let container = null;
39
+ async function renderWithApp(app) {
40
+ appMockState.app = app;
41
+ container = document.createElement("div");
42
+ document.body.append(container);
43
+ root = createRoot(container);
44
+ await act(async () => {
45
+ root?.render(/* @__PURE__ */ jsx(TranslationProviderClient, {
46
+ quetzalKeys: /* @__PURE__ */ new Map(),
47
+ quetzalLocale: /* @__PURE__ */ new Map(),
48
+ children: /* @__PURE__ */ jsx(OAuthCallback, {})
49
+ }));
50
+ });
51
+ }
52
+ async function flushEffects() {
53
+ await act(async () => {
54
+ await new Promise((resolve) => setTimeout(resolve, 0));
55
+ });
56
+ }
57
+ describe("OAuthCallback", () => {
58
+ beforeEach(() => {
59
+ Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", true);
60
+ });
61
+ afterEach(() => {
62
+ act(() => {
63
+ root?.unmount();
64
+ });
65
+ container?.remove();
66
+ root = null;
67
+ container = null;
68
+ appMockState.app = null;
69
+ vi.restoreAllMocks();
70
+ Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", previousActEnvironment);
71
+ });
72
+ it("renders backend-encoded OAuth callback errors on the callback page", async () => {
73
+ const errorMessage = "Your sign up was rejected by an administrator's sign-up rule.";
74
+ const callOAuthCallback = vi.fn(async () => {
75
+ throw new KnownErrors.SignUpRejected(errorMessage);
76
+ });
77
+ const app = createAppTestDouble({ callOAuthCallback });
78
+ await renderWithApp(app);
79
+ await flushEffects();
80
+ expect(callOAuthCallback).toHaveBeenCalledOnce();
81
+ expect(container?.textContent).toContain("SIGN_UP_REJECTED");
82
+ expect(container?.textContent).toContain(errorMessage);
83
+ expect(app.redirectToSignIn).not.toHaveBeenCalled();
84
+ });
85
+ });
86
+
87
+ //#endregion
88
+ export { };
89
+ //# sourceMappingURL=oauth-callback.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-callback.test.js","names":[],"sources":["../../../src/components-page/oauth-callback.test.tsx"],"sourcesContent":["\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\n// @vitest-environment jsdom\n\nimport { KnownErrors } from \"@hexclave/shared\";\nimport React, { act } from \"react\";\nimport { createRoot, type Root } from \"react-dom/client\";\nimport { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\nimport type { StackClientApp } from \"../lib/hexclave-app/apps/interfaces/client-app\";\nimport { TranslationProviderClient } from \"../providers/translation-provider-client\";\nimport { OAuthCallback } from \"./oauth-callback\";\n\nconst appMockState = vi.hoisted(() => ({ app: null as unknown }));\n\nvi.mock(\"..\", () => ({\n useStackApp: () => {\n if (appMockState.app == null) {\n throw new Error(\"Expected test app to be set before rendering.\");\n }\n return appMockState.app;\n },\n}));\n\nvi.mock(\"@hexclave/ui\", async () => {\n const React = await import(\"react\");\n return {\n Button: (props: { children: React.ReactNode, onClick?: () => void }) => (\n <button type=\"button\" onClick={props.onClick}>{props.children}</button>\n ),\n Spinner: () => <div data-testid=\"spinner\" />,\n Typography: (props: { children: React.ReactNode }) => <div>{props.children}</div>,\n cn: (...classes: (string | false | null | undefined)[]) => classes.filter(Boolean).join(\" \"),\n };\n});\n\nconst previousActEnvironment = Reflect.get(globalThis, \"IS_REACT_ACT_ENVIRONMENT\");\n\nfunction createAppTestDouble(options: {\n callOAuthCallback: () => Promise<boolean>,\n}) {\n const app = {\n callOAuthCallback: options.callOAuthCallback,\n redirectToSignIn: vi.fn(async () => {}),\n redirectToHome: vi.fn(async () => {}),\n };\n\n // This test double intentionally implements only the StackClientApp surface\n // that OAuthCallback and the rendered error card touch.\n return app as unknown as StackClientApp<true>;\n}\n\nlet root: Root | null = null;\nlet container: HTMLDivElement | null = null;\n\nasync function renderWithApp(app: StackClientApp<true>) {\n appMockState.app = app;\n container = document.createElement(\"div\");\n document.body.append(container);\n root = createRoot(container);\n await act(async () => {\n root?.render(\n <TranslationProviderClient quetzalKeys={new Map()} quetzalLocale={new Map()}>\n <OAuthCallback />\n </TranslationProviderClient>\n );\n });\n}\n\nasync function flushEffects() {\n await act(async () => {\n await new Promise((resolve) => setTimeout(resolve, 0));\n });\n}\n\ndescribe(\"OAuthCallback\", () => {\n beforeEach(() => {\n Reflect.set(globalThis, \"IS_REACT_ACT_ENVIRONMENT\", true);\n });\n\n afterEach(() => {\n act(() => {\n root?.unmount();\n });\n container?.remove();\n root = null;\n container = null;\n appMockState.app = null;\n vi.restoreAllMocks();\n Reflect.set(globalThis, \"IS_REACT_ACT_ENVIRONMENT\", previousActEnvironment);\n });\n\n it(\"renders backend-encoded OAuth callback errors on the callback page\", async () => {\n const errorMessage = \"Your sign up was rejected by an administrator's sign-up rule.\";\n const callOAuthCallback = vi.fn(async () => {\n throw new KnownErrors.SignUpRejected(errorMessage);\n });\n const app = createAppTestDouble({ callOAuthCallback });\n\n await renderWithApp(app);\n await flushEffects();\n\n expect(callOAuthCallback).toHaveBeenCalledOnce();\n expect(container?.textContent).toContain(\"SIGN_UP_REJECTED\");\n expect(container?.textContent).toContain(errorMessage);\n expect(app.redirectToSignIn).not.toHaveBeenCalled();\n });\n});\n"],"mappings":";;;;;;;;;;AAcA,MAAM,eAAe,GAAG,eAAe,EAAE,KAAK,MAAiB,EAAE;AAEjE,GAAG,KAAK,aAAa,EACnB,mBAAmB;AACjB,KAAI,aAAa,OAAO,KACtB,OAAM,IAAI,MAAM,gDAAgD;AAElE,QAAO,aAAa;GAEvB,EAAE;AAEH,GAAG,KAAK,gBAAgB,YAAY;AACpB,OAAM,OAAO;AAC3B,QAAO;EACL,SAAS,UACP,oBAAC;GAAO,MAAK;GAAS,SAAS,MAAM;aAAU,MAAM;IAAkB;EAEzE,eAAe,oBAAC,SAAI,eAAY,YAAY;EAC5C,aAAa,UAAyC,oBAAC,mBAAK,MAAM,WAAe;EACjF,KAAK,GAAG,YAAmD,QAAQ,OAAO,QAAQ,CAAC,KAAK,IAAI;EAC7F;EACD;AAEF,MAAM,yBAAyB,QAAQ,IAAI,YAAY,2BAA2B;AAElF,SAAS,oBAAoB,SAE1B;AASD,QARY;EACV,mBAAmB,QAAQ;EAC3B,kBAAkB,GAAG,GAAG,YAAY,GAAG;EACvC,gBAAgB,GAAG,GAAG,YAAY,GAAG;EACtC;;AAOH,IAAI,OAAoB;AACxB,IAAI,YAAmC;AAEvC,eAAe,cAAc,KAA2B;AACtD,cAAa,MAAM;AACnB,aAAY,SAAS,cAAc,MAAM;AACzC,UAAS,KAAK,OAAO,UAAU;AAC/B,QAAO,WAAW,UAAU;AAC5B,OAAM,IAAI,YAAY;AACpB,QAAM,OACJ,oBAAC;GAA0B,6BAAa,IAAI,KAAK;GAAE,+BAAe,IAAI,KAAK;aACzE,oBAAC,kBAAgB;IACS,CAC7B;GACD;;AAGJ,eAAe,eAAe;AAC5B,OAAM,IAAI,YAAY;AACpB,QAAM,IAAI,SAAS,YAAY,WAAW,SAAS,EAAE,CAAC;GACtD;;AAGJ,SAAS,uBAAuB;AAC9B,kBAAiB;AACf,UAAQ,IAAI,YAAY,4BAA4B,KAAK;GACzD;AAEF,iBAAgB;AACd,YAAU;AACR,SAAM,SAAS;IACf;AACF,aAAW,QAAQ;AACnB,SAAO;AACP,cAAY;AACZ,eAAa,MAAM;AACnB,KAAG,iBAAiB;AACpB,UAAQ,IAAI,YAAY,4BAA4B,uBAAuB;GAC3E;AAEF,IAAG,sEAAsE,YAAY;EACnF,MAAM,eAAe;EACrB,MAAM,oBAAoB,GAAG,GAAG,YAAY;AAC1C,SAAM,IAAI,YAAY,eAAe,aAAa;IAClD;EACF,MAAM,MAAM,oBAAoB,EAAE,mBAAmB,CAAC;AAEtD,QAAM,cAAc,IAAI;AACxB,QAAM,cAAc;AAEpB,SAAO,kBAAkB,CAAC,sBAAsB;AAChD,SAAO,WAAW,YAAY,CAAC,UAAU,mBAAmB;AAC5D,SAAO,WAAW,YAAY,CAAC,UAAU,aAAa;AACtD,SAAO,IAAI,iBAAiB,CAAC,IAAI,kBAAkB;GACnD;EACF"}