Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions apps/login/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
36 changes: 36 additions & 0 deletions apps/login/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.

This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
16 changes: 16 additions & 0 deletions apps/login/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { FlatCompat } from "@eslint/eslintrc";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const compat = new FlatCompat({
baseDirectory: __dirname,
});

const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];

export default eslintConfig;
28 changes: 28 additions & 0 deletions apps/login/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
/* config options here */
async headers() {
return [
{
source: "/api/request",
headers: [
{
key: "Access-Control-Allow-Origin",
value: "*", // Set your origin
},
{
key: "Access-Control-Allow-Methods",
value: "GET, POST, PUT, DELETE, OPTIONS",
},
{
key: "Access-Control-Allow-Headers",
value: "Content-Type, Authorization",
},
],
},
];
},
};

export default nextConfig;
33 changes: 33 additions & 0 deletions apps/login/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "thirdweb-login",
"version": "0.1.0",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"format": "biome format ./src --write",
"lint": "biome check ./src && knip && eslint ./src",
"fix": "biome check ./src --fix && eslint ./src --fix",
"typecheck": "tsc --noEmit",
"knip": "knip"
},
"dependencies": {
"next": "15.1.6",
"react": "19.0.0",
"react-dom": "19.0.0",
"thirdweb": "workspace:*"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@types/node": "22.10.10",
"@types/react": "19.0.8",
"@types/react-dom": "19.0.3",
"eslint": "^9",
"eslint-config-next": "15.1.6",
"postcss": "8.5.1",
"tailwindcss": "3.4.17",
"typescript": "5.7.3"
}
}
8 changes: 8 additions & 0 deletions apps/login/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};

export default config;
144 changes: 144 additions & 0 deletions apps/login/public/tw-login.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
(() => {
const { targetId, clientId, baseUrl } = getSetup();

// the code to verify login was not tampered with
let code = "";

const USER_ADDRESS_KEY = "tw.login:userAddress";
const SESSION_KEY_ADDRESS_KEY = "tw.login:sessionKeyAddress";

function main() {
// check if redirected first, this sets up the logged in state if it was from redirect
const params = parseURLHash(new URL(window.location));
console.log(params);
// TECHNICALLY this should verify the code... but can't do that without backend of some sort
if (params) {
// reset the code
code = "";
// write the userAddress to local storage
localStorage.setItem(USER_ADDRESS_KEY, params.userAddress);
// write the sessionKeyAddress to local storage
localStorage.setItem(SESSION_KEY_ADDRESS_KEY, params.sessionKeyAddress);
// reset the URL hash
window.location.hash = "";
}

const userAddress = localStorage.getItem(USER_ADDRESS_KEY);
const sessionKeyAddress = localStorage.getItem(SESSION_KEY_ADDRESS_KEY);

if (userAddress && sessionKeyAddress) {
// handle logged in state
handleIsLoggedIn();
} else {
// handle not logged in state
handleNotLoggedIn();
}
}

function handleIsLoggedIn() {
console.log("handleIsLoggedIn");

window.thirdweb = {
isLoggedIn: true,
getAddress: () => getAddress(),
logout: () => {
window.localStorage.removeItem(USER_ADDRESS_KEY);
window.localStorage.removeItem(SESSION_KEY_ADDRESS_KEY);
window.location.reload();
},
makeRequest: async () => {
const res = await fetch(`${baseUrl}/api/request`, {
method: "POST",
body: JSON.stringify({
userAddress: getAddress(),
sessionKeyAddress: getSessionKeyAddress(),
}),
});
const data = await res.json();
console.log(data);
},
};
}

function handleNotLoggedIn() {
window.thirdweb = { login: onLogin, isLoggedIn: false };
}

function onLogin() {
code = window.crypto.getRandomValues(new Uint8Array(4)).join("");
// redirect to the login page
const redirect = new URL(baseUrl);
redirect.searchParams.set("code", code);
redirect.searchParams.set("clientId", clientId);
redirect.searchParams.set("redirect", window.location.href);
window.location.href = redirect.href;
}

function getAddress() {
return localStorage.getItem(USER_ADDRESS_KEY);
}

function getSessionKeyAddress() {
return localStorage.getItem(SESSION_KEY_ADDRESS_KEY);
}

// utils

function getSetup() {
const el = document.currentScript;
if (!el) {
throw new Error("Could not find script element");
}
const baseUrl = new URL(el.src).origin;
const dataset = el.dataset;
const targetId = dataset.target || "tw-login";
const clientId = dataset.clientId;
if (!clientId) {
throw new Error("Missing client-id");
}
return { targetId, clientId, baseUrl };
}

/**
* @param {URL} url
* @returns null | { [key: string]: string }
*/
function parseURLHash(url) {
if (!url.hash) {
return null;
}
try {
return decodeHash(url.hash);
} catch {
// if this fails, invalid data -> return null
return null;
}
}

/**
* Decodes a URL hash string to extract the three keys.
*
* @param {string} hash - A string like "#eyJrZXkxIjoiVmFsdWU..."
* @returns {{ userAddress: string, sessionKeyAddress: string, code: string }} An object with the three keys
*/
function decodeHash(hash) {
// Remove the "#" prefix, if present.
const base64Data = hash.startsWith("#") ? hash.slice(1) : hash;

// Decode the Base64 string, then parse the JSON.
const jsonString = atob(base64Data);
const data = JSON.parse(jsonString);

// data should have the shape { userAddress, sessionKeyAddress, code }.
if (
"userAddress" in data &&
"sessionKeyAddress" in data &&
"code" in data
) {
return data;
}
return null;
}

main();
})();
36 changes: 36 additions & 0 deletions apps/login/src/app/api/request/route.salty
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { type NextRequest, NextResponse } from "next/server";

export const POST = async (req: NextRequest) => {
const body = await req.json();
const userAddress = body.userAddress;
const sessionKeyAddress = body.sessionKeyAddress;
if (!userAddress || !sessionKeyAddress) {
return NextResponse.json(
{
message: "Missing userAddress or sessionKeyAddress",
},
{ status: 400 },
);
}
const url = `${ENGINE_URL}/contract/84532/0x638263e3eAa3917a53630e61B1fBa685308024fa/erc1155/claim-to`;

console.log("url", url);

const res = await fetch(url, {
method: "POST",
headers: {
authorization: `Bearer ${ACCESS_TOKEN}`,
"x-account-address": userAddress,
"x-backend-wallet-address": sessionKeyAddress,
"content-type": "application/json",
},
body: JSON.stringify({
receiver: userAddress,
tokenId: "0",
quantity: "1",
}),
});
const data = await res.json();

return NextResponse.json(data, { status: res.status });
};
Binary file added apps/login/src/app/favicon.ico
Binary file not shown.
Loading
Loading