diff --git a/.gitignore b/.gitignore
index 9051dd5..396c80c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,6 +17,7 @@ yarn-error.log*
# build
/main
+/server
/hooks
/utils
/generators
diff --git a/TODO.md b/TODO.md
index 1831a91..d8d215e 100644
--- a/TODO.md
+++ b/TODO.md
@@ -4,6 +4,8 @@ Below is a TODO list for further development of `next-auth-lightning-provider`.
### Alpha
+- consider ssr pages router (getServerSideProps)
+- consider using next router for redirect instead of window navigator
- `Response body object should not be disturbed or locked` in `ui-app-router` example create request
- test deploy all the various example apps
- ar-generic
diff --git a/examples/ui-app-router/app/components/LightningAuth.tsx b/examples/ui-app-router/app/components/LightningAuth.tsx
index fdb3240..41f4517 100644
--- a/examples/ui-app-router/app/components/LightningAuth.tsx
+++ b/examples/ui-app-router/app/components/LightningAuth.tsx
@@ -1,21 +1,14 @@
"use client";
-import { useLightningAuth } from "next-auth-lightning-provider/hooks";
+import { useLightningPolling } from "next-auth-lightning-provider/hooks";
+import { LightningAuthClientSession } from "next-auth-lightning-provider/server";
export default function LightningAuth({
- redirectUri,
- state,
+ session,
}: {
- redirectUri: string;
- state: string;
+ session: LightningAuthClientSession;
}) {
- const { lnurl, qr, button } = useLightningAuth({ redirectUri, state });
-
- if (!lnurl) {
- return (
-
loading...
- );
- }
+ const { lnurl, qr, button } = useLightningPolling(session);
return (
;
+}) {
+ let session, error;
+ try {
+ session = await createLightningAuth(searchParams);
+ } catch (e: any) {
+ error = e.message || "Something went wrong";
+ }
- if (!redirectUri || !state) {
- return (
-
- Missing query params
-
- );
+ if (error || !session) {
+ return
{error}
;
}
return (
-
+
);
}
diff --git a/package.json b/package.json
index 2a90748..ddb60f2 100644
--- a/package.json
+++ b/package.json
@@ -36,6 +36,8 @@
"hooks/**/*.js",
"utils/**/*.d.ts",
"utils/**/*.js",
+ "server/**/*.d.ts",
+ "server/**/*.js",
"main/**/*.d.ts",
"main/**/*.js",
"index.d.ts",
@@ -44,7 +46,7 @@
"main": "./index.js",
"scripts": {
"build": "tsc && tsc-alias",
- "clean": "rm -rf main hooks utils generators index.d.ts index.d.ts.map index.js",
+ "clean": "rm -rf main server hooks utils generators index.d.ts index.d.ts.map index.js",
"dev": "concurrently \"tsc -w\" \"tsc-alias -w\"",
"test": "jest --watch",
"prebuild": "npm run clean && jest"
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
index 34da9b4..cb05295 100644
--- a/src/hooks/index.ts
+++ b/src/hooks/index.ts
@@ -1 +1,2 @@
export * from "./useLightningAuth";
+export * from "./useLightningPolling";
diff --git a/src/hooks/useLightningAuth.ts b/src/hooks/useLightningAuth.ts
index 755cb5b..c23fa35 100644
--- a/src/hooks/useLightningAuth.ts
+++ b/src/hooks/useLightningAuth.ts
@@ -11,7 +11,7 @@ const maxNetworkRequestsFailures = 3;
* A React hook that, on mount, will trigger an API request and create a new Lightning auth session.
* Thereafter, it'll poll the API and checks if the Lightning auth QR has been scanned.
* If enough time elapses without a sign in attempt, the Lightning auth session will be refreshed.
- * Once a success status is received from polling, the user will be redirected to the `redirectUri`.
+ * Once a success status is received from polling, the user will be redirected to the `next-auth` redirect url.
*
* @param {String} redirectUri - the `redirect_uri` query param generated by `next-auth`
* @param {String} state - the `state` query param generated by `next-auth`
@@ -28,11 +28,11 @@ export function useLightningAuth({
redirectUri: string;
state: string;
}): {
- lnurl: string | null;
+ lnurl: string;
qr: string;
button: string;
} {
- const [lnurl, setUrl] = useState
(null);
+ const [lnurl, setUrl] = useState("");
useEffect(() => {
let session: {
@@ -145,13 +145,13 @@ export function useLightningAuth({
}
session = d;
- if (!session || !session.lnurl) return;
+ if (!session) return;
// setup intervals and create first qr code
pollTimeoutId = setTimeout(poll, session?.pollInterval);
createIntervalId = setInterval(create, session?.createInterval);
- setUrl(session?.lnurl || null);
+ setUrl(session.lnurl);
})
.catch((e) => {
if (!createController.signal.aborted) {
@@ -167,5 +167,9 @@ export function useLightningAuth({
const { qr, button } = formatLightningAuth(lnurl);
- return { lnurl, qr, button };
+ return {
+ lnurl,
+ qr,
+ button,
+ };
}
diff --git a/src/hooks/useLightningPolling.ts b/src/hooks/useLightningPolling.ts
new file mode 100644
index 0000000..230eeea
--- /dev/null
+++ b/src/hooks/useLightningPolling.ts
@@ -0,0 +1,125 @@
+"use client";
+
+import { useEffect } from "react";
+import { useRouter } from "next/navigation";
+
+import { hardConfig } from "../main/config/hard";
+import { LightningAuthClientSession } from "../server";
+import { formatLightningAuth } from "../utils/lnurl";
+
+const maxNetworkRequestsFailures = 3;
+
+/**
+ * A React hook that, on mount, will poll the API and checks if the Lightning auth QR has been scanned.
+ * If enough time elapses without a sign in attempt, the page will be refreshed.
+ * Once a success status is received from polling, the user will be redirected to the `next-auth` redirect url.
+ *
+ * @param {LightningAuthClientSession} session - a session object generated by invoking the `createLightningAuth` method
+ *
+ * @returns {Object}
+ * @returns {String} lnurl - the raw LNURL, should be made available for copy-pasting
+ * @returns {String} qr - a url pointing the lnurl-auth QR Code image, should be used in the src prop of img tags
+ * @returns {String} button - a deep-link that will open in Lightning enabled wallets, should be used in the href prop of anchor tags
+ */
+export function useLightningPolling(session: LightningAuthClientSession): {
+ lnurl: string | null;
+ qr: string;
+ button: string;
+} {
+ const router = useRouter();
+
+ useEffect(() => {
+ let pollTimeoutId: NodeJS.Timeout | undefined;
+ let createIntervalId: NodeJS.Timeout | undefined;
+ let networkRequestCount: number = 0;
+ let errorUrl: string | undefined;
+ const pollController = new AbortController();
+
+ // cleanup when the hook unmounts of polling is successful
+ const cleanup = () => {
+ clearTimeout(pollTimeoutId);
+ clearInterval(createIntervalId);
+ pollController.abort();
+ };
+
+ // redirect user to error page if something goes wrong
+ function error(e: any) {
+ console.error(e);
+ if (errorUrl) {
+ window.location.replace(errorUrl);
+ } else {
+ // if no errorUrl exists send to defaul `next-auth` error page
+ const params = new URLSearchParams();
+ params.append("error", "OAuthSignin");
+ window.location.replace(`/api/auth/error?${params.toString()}`);
+ }
+ }
+
+ // poll the api to see if the user has successfully authenticated
+ function poll() {
+ if (!session?.data?.k1) return;
+ const k1 = session.data.k1;
+
+ const params = new URLSearchParams({ k1 });
+
+ return fetch(hardConfig.apis.poll, {
+ method: "POST",
+ headers: { "content-type": "application/x-www-form-urlencoded" },
+ body: params,
+ cache: "default",
+ signal: pollController.signal,
+ })
+ .then((r) => {
+ if (r.status === 410) {
+ // if resource not found throw error immediately,
+ // this means the user's auth session has been deleted.
+ networkRequestCount = maxNetworkRequestsFailures;
+ }
+ return r.json();
+ })
+ .catch((e) => {
+ // if there are more than X network errors, then trigger redirect
+ networkRequestCount++;
+ if (networkRequestCount >= maxNetworkRequestsFailures) {
+ pollTimeoutId = setTimeout(poll, session.intervals.poll);
+ throw e;
+ }
+ })
+ .then((d) => {
+ if (d && d.error) {
+ if (d.url) errorUrl = d.url;
+ throw new Error(d.message || d.error);
+ }
+
+ if (d) networkRequestCount = 0;
+ if (d && d.message) throw new Error(d.message);
+ pollTimeoutId = setTimeout(poll, session.intervals.poll);
+
+ if (d && d.success) {
+ cleanup();
+ let url = new URL(session.query.redirectUri);
+ url.searchParams.append("state", session.query.state);
+ url.searchParams.append("code", k1);
+ router.replace(url.toString());
+ }
+ })
+ .catch((e) => {
+ if (!pollController.signal.aborted) {
+ error(e);
+ }
+ });
+ }
+
+ function create() {
+ router.refresh();
+ }
+
+ pollTimeoutId = setTimeout(poll, session.intervals.poll);
+ createIntervalId = setInterval(create, session.intervals.create);
+
+ return () => cleanup();
+ }, []);
+
+ const { lnurl, qr, button } = formatLightningAuth(session.data.lnurl);
+ return { lnurl, qr, button };
+}
diff --git a/src/server/createLightningAuth.ts b/src/server/createLightningAuth.ts
new file mode 100644
index 0000000..a6ffc44
--- /dev/null
+++ b/src/server/createLightningAuth.ts
@@ -0,0 +1,53 @@
+import { formatLightningAuth } from "../utils/lnurl";
+import { hardConfig } from "../main/config";
+
+export type LightningAuthClientSession = {
+ data: {
+ k1: string;
+ lnurl: string;
+ };
+ intervals: {
+ poll: number;
+ create: number;
+ };
+ query: {
+ state: string;
+ redirectUri: string;
+ };
+};
+
+export default async function createLightningAuth(
+ searchParams: any
+): Promise {
+ const siteUrl = searchParams.client_id;
+ const state = searchParams.state;
+ const redirectUri = searchParams.redirect_uri;
+ if (!redirectUri || !state) {
+ throw new Error("Missing query params");
+ }
+
+ const params = new URLSearchParams({ state });
+ const response = await fetch(siteUrl + hardConfig.apis.create, {
+ method: "POST",
+ headers: { "content-type": "application/x-www-form-urlencoded" },
+ body: params,
+ cache: "no-cache",
+ });
+ const data = await response.json();
+
+ if (!data.lnurl) {
+ throw new Error("Missing lnurl");
+ }
+
+ return {
+ data: {
+ k1: data.k1,
+ lnurl: data.lnurl,
+ },
+ intervals: {
+ poll: data.pollInterval,
+ create: data.createInterval,
+ },
+ query: { state, redirectUri },
+ };
+}
diff --git a/src/server/index.ts b/src/server/index.ts
new file mode 100644
index 0000000..bc455db
--- /dev/null
+++ b/src/server/index.ts
@@ -0,0 +1,4 @@
+export {
+ default as createLightningAuth,
+ LightningAuthClientSession,
+} from "./createLightningAuth";
diff --git a/src/utils/lnurl.ts b/src/utils/lnurl.ts
index 9561cf6..9104283 100644
--- a/src/utils/lnurl.ts
+++ b/src/utils/lnurl.ts
@@ -2,7 +2,7 @@ import { hardConfig } from "../main/config/hard";
export function formatLightningAuth(lnurl?: string | null) {
if (!lnurl) {
- return { lnurl, qr: "", button: "" };
+ return { lnurl: "", qr: "", button: "" };
}
const qr = `${hardConfig.apis.qr}/${lnurl}`;
diff --git a/tsconfig.json b/tsconfig.json
index 56a220c..eeef1a7 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -35,6 +35,7 @@
"main",
"generators",
"hooks",
+ "server",
"utils",
"examples",
"node_modules"