diff --git a/alby.go b/alby.go
index 26b50246..874748fe 100644
--- a/alby.go
+++ b/alby.go
@@ -436,7 +436,6 @@ func (svc *AlbyOAuthService) SendPaymentSync(ctx context.Context, senderPubkey,
}
func (svc *AlbyOAuthService) AuthHandler(c echo.Context) error {
- fmt.Println("haha")
appName := c.QueryParam("c") // c - for client
// clear current session
sess, _ := session.Get(CookieName, c)
diff --git a/echo_handlers.go b/echo_handlers.go
index d1009c55..47f3e15d 100644
--- a/echo_handlers.go
+++ b/echo_handlers.go
@@ -61,13 +61,13 @@ func (svc *Service) RegisterSharedRoutes(e *echo.Echo) {
assetSubdir, _ := fs.Sub(embeddedAssets, "public")
assetHandler := http.FileServer(http.FS(assetSubdir))
e.GET("/public/*", echo.WrapHandler(http.StripPrefix("/public/", assetHandler)))
+ e.GET("/api/getCSRFToken", svc.CSRFHandler)
e.GET("/api/apps", svc.AppsListHandler)
- e.GET("/api/apps/new", svc.AppsNewHandler)
e.GET("/api/apps/:pubkey", svc.AppsShowHandler)
e.POST("/api/apps", svc.AppsCreateHandler)
e.POST("/api/apps/delete/:pubkey", svc.AppsDeleteHandler)
- e.GET("/api/logout", svc.LogoutHandler)
- e.GET("/about", svc.AboutHandler)
+ e.GET("/api/info", svc.InfoHandler)
+ e.GET("/logout", svc.LogoutHandler)
e.GET("/", svc.IndexHandler)
frontend.RegisterHandlers(e)
}
@@ -92,7 +92,7 @@ func (svc *Service) IndexHandler(c echo.Context) error {
if user != nil {
return c.Redirect(302, "/apps")
}
- return c.Render(http.StatusOK, fmt.Sprintf("%s", strings.ToLower(svc.cfg.LNBackendType)), map[string]interface{}{})
+ return c.Redirect(302, "/login")
}
func (svc *Service) AboutHandler(c echo.Context) error {
@@ -150,7 +150,11 @@ func (svc *Service) AppsShowHandler(c echo.Context) error {
})
}
if user == nil {
- return c.Redirect(302, "/")
+ return c.JSON(http.StatusBadRequest, ErrorResponse{
+ Error: true,
+ Code: 8,
+ Message: "User does not exist",
+ })
}
app := App{}
@@ -158,7 +162,11 @@ func (svc *Service) AppsShowHandler(c echo.Context) error {
if app.NostrPubkey == "" {
// TODO: Show not found?
- return c.Redirect(302, "/?q=notfound")
+ return c.JSON(http.StatusBadRequest, ErrorResponse{
+ Error: true,
+ Code: 8,
+ Message: "App does not exist",
+ })
}
lastEvent := NostrEvent{}
@@ -234,97 +242,28 @@ func getEndOfBudgetString(endOfBudget time.Time) (result string) {
return fmt.Sprintf("%d months", months)
}
-func (svc *Service) AppsNewHandler(c echo.Context) error {
- appName := c.QueryParam("name")
- if appName == "" {
- // c - for client (deprecated)
- appName = c.QueryParam("c")
- }
- pubkey := c.QueryParam("pubkey")
- returnTo := c.QueryParam("return_to")
- maxAmount := c.QueryParam("max_amount")
- budgetRenewal := strings.ToLower(c.QueryParam("budget_renewal"))
- expiresAt := c.QueryParam("expires_at") // YYYY-MM-DD or MM/DD/YYYY or timestamp in seconds
- if expiresAtTimestamp, err := strconv.Atoi(expiresAt); err == nil {
- expiresAt = time.Unix(int64(expiresAtTimestamp), 0).Format(time.RFC3339)
- }
- expiresAtISO, _ := time.Parse(time.RFC3339, expiresAt)
- expiresAtFormatted := expiresAtISO.Format("January 2, 2006 03:04 PM")
-
- requestMethods := c.QueryParam("request_methods")
- customRequestMethods := requestMethods
- if requestMethods == "" {
- // if no request methods are given, enable them all by default
- keys := []string{}
- for key := range nip47MethodDescriptions {
- keys = append(keys, key)
- }
-
- requestMethods = strings.Join(keys, " ")
- }
+func (svc *Service) CSRFHandler(c echo.Context) error {
csrf, _ := c.Get(middleware.DefaultCSRFConfig.ContextKey).(string)
-
- user, err := svc.GetUser(c)
- if err != nil {
- return err
- }
- if user == nil {
- sess, _ := session.Get(CookieName, c)
- sess.Values["return_to"] = c.Path() + "?" + c.QueryString()
- sess.Options.MaxAge = 0
- sess.Options.SameSite = http.SameSiteLaxMode
- if svc.cfg.CookieDomain != "" {
- sess.Options.Domain = svc.cfg.CookieDomain
- }
- sess.Save(c.Request(), c.Response())
- return c.Redirect(302, fmt.Sprintf("/%s/auth?c=%s", strings.ToLower(svc.cfg.LNBackendType), appName))
- }
-
- //construction to return a map with all possible permissions
- //and indicate which ones are checked by default in the front-end
- type RequestMethodHelper struct {
- Description string
- Icon string
- Checked bool
- }
-
- requestMethodHelper := map[string]*RequestMethodHelper{}
- for k, v := range nip47MethodDescriptions {
- requestMethodHelper[k] = &RequestMethodHelper{
- Description: v,
- Icon: nip47MethodIcons[k],
- }
- }
-
- for _, m := range strings.Split(requestMethods, " ") {
- if _, ok := nip47MethodDescriptions[m]; ok {
- requestMethodHelper[m].Checked = true
- }
- }
-
- return c.Render(http.StatusOK, "apps/new.html", map[string]interface{}{
- "User": user,
- "Name": appName,
- "Pubkey": pubkey,
- "ReturnTo": returnTo,
- "MaxAmount": maxAmount,
- "BudgetRenewal": budgetRenewal,
- "ExpiresAt": expiresAt,
- "ExpiresAtFormatted": expiresAtFormatted,
- "RequestMethods": requestMethods,
- "CustomRequestMethods": customRequestMethods,
- "RequestMethodHelper": requestMethodHelper,
- "Csrf": csrf,
+ return c.JSON(http.StatusOK, &CSRFResponse{
+ Csrf: csrf,
})
}
func (svc *Service) AppsCreateHandler(c echo.Context) error {
user, err := svc.GetUser(c)
if err != nil {
- return err
+ return c.JSON(http.StatusBadRequest, ErrorResponse{
+ Error: true,
+ Code: 8,
+ Message: fmt.Sprintf("Bad arguments %s", err.Error()),
+ })
}
if user == nil {
- return c.Redirect(302, "/")
+ return c.JSON(http.StatusBadRequest, ErrorResponse{
+ Error: true,
+ Code: 8,
+ Message: "User does not exist",
+ })
}
name := c.FormValue("name")
@@ -339,18 +278,27 @@ func (svc *Service) AppsCreateHandler(c echo.Context) error {
decoded, err := hex.DecodeString(pairingPublicKey)
if err != nil || len(decoded) != 32 {
svc.Logger.Errorf("Invalid public key format: %s", pairingPublicKey)
- return c.Redirect(302, "/apps")
+ return c.JSON(http.StatusBadRequest, ErrorResponse{
+ Error: true,
+ Code: 8,
+ Message: fmt.Sprintf("Invalid public key format: %s", pairingPublicKey),
+ })
}
}
app := App{Name: name, NostrPubkey: pairingPublicKey}
- maxAmount, _ := strconv.Atoi(c.FormValue("MaxAmount"))
- budgetRenewal := c.FormValue("BudgetRenewal")
+ maxAmount, _ := strconv.Atoi(c.FormValue("maxAmount"))
+ budgetRenewal := c.FormValue("budgetRenewal")
expiresAt := time.Time{}
- if c.FormValue("ExpiresAt") != "" {
- expiresAt, err = time.Parse(time.RFC3339, c.FormValue("ExpiresAt"))
+ if c.FormValue("expiresAt") != "" {
+ expiresAt, err = time.Parse(time.RFC3339, c.FormValue("expiresAt"))
if err != nil {
- return fmt.Errorf("Invalid ExpiresAt: %v", err)
+ svc.Logger.Errorf("Invalid expiresAt: %s", pairingPublicKey)
+ return c.JSON(http.StatusBadRequest, ErrorResponse{
+ Error: true,
+ Code: 8,
+ Message: fmt.Sprintf("Invalid expiresAt: %v", err),
+ })
}
}
@@ -364,7 +312,7 @@ func (svc *Service) AppsCreateHandler(c echo.Context) error {
return err
}
- requestMethods := c.FormValue("RequestMethods")
+ requestMethods := c.FormValue("requestMethods")
if requestMethods == "" {
return fmt.Errorf("Won't create an app without request methods.")
}
@@ -397,7 +345,11 @@ func (svc *Service) AppsCreateHandler(c echo.Context) error {
"pairingPublicKey": pairingPublicKey,
"name": name,
}).Errorf("Failed to save app: %v", err)
- return c.Redirect(302, "/apps")
+ return c.JSON(http.StatusInternalServerError, ErrorResponse{
+ Error: true,
+ Code: 8,
+ Message: fmt.Sprintf("Failed to save app: %v", err),
+ })
}
publicRelayUrl := svc.cfg.PublicRelay
@@ -405,6 +357,11 @@ func (svc *Service) AppsCreateHandler(c echo.Context) error {
publicRelayUrl = svc.cfg.Relay
}
+ responseBody := &CreateAppResponse{}
+ responseBody.Name = name
+ responseBody.Pubkey = pairingPublicKey
+ responseBody.PairingSecret = pairingSecretKey
+
if c.FormValue("returnTo") != "" {
returnToUrl, err := url.Parse(c.FormValue("returnTo"))
if err == nil {
@@ -415,7 +372,7 @@ func (svc *Service) AppsCreateHandler(c echo.Context) error {
query.Add("lud16", user.LightningAddress)
}
returnToUrl.RawQuery = query.Encode()
- return c.Redirect(302, returnToUrl.String())
+ responseBody.ReturnTo = returnToUrl.String()
}
}
@@ -423,14 +380,8 @@ func (svc *Service) AppsCreateHandler(c echo.Context) error {
if user.LightningAddress != "" {
lud16 = fmt.Sprintf("&lud16=%s", user.LightningAddress)
}
- pairingUri := template.URL(fmt.Sprintf("nostr+walletconnect://%s?relay=%s&secret=%s%s", svc.cfg.IdentityPubkey, publicRelayUrl, pairingSecretKey, lud16))
- return c.Render(http.StatusOK, "apps/create.html", map[string]interface{}{
- "User": user,
- "PairingUri": pairingUri,
- "PairingSecret": pairingSecretKey,
- "Pubkey": pairingPublicKey,
- "Name": name,
- })
+ responseBody.PairingUri = fmt.Sprintf("nostr+walletconnect://%s?relay=%s&secret=%s%s", svc.cfg.IdentityPubkey, publicRelayUrl, pairingSecretKey, lud16)
+ return c.JSON(http.StatusOK, responseBody)
}
func (svc *Service) AppsDeleteHandler(c echo.Context) error {
@@ -439,12 +390,16 @@ func (svc *Service) AppsDeleteHandler(c echo.Context) error {
return err
}
if user == nil {
- return c.Redirect(302, "/")
+ return c.JSON(http.StatusBadRequest, ErrorResponse{
+ Error: true,
+ Code: 8,
+ Message: "User does not exist",
+ })
}
app := App{}
svc.db.Where("user_id = ? AND nostr_pubkey = ?", user.ID, c.Param("pubkey")).First(&app)
svc.db.Delete(&app)
- return c.Redirect(302, "/apps")
+ return c.JSON(http.StatusOK, "App deleted successfully")
}
func (svc *Service) LogoutHandler(c echo.Context) error {
@@ -454,5 +409,18 @@ func (svc *Service) LogoutHandler(c echo.Context) error {
sess.Options.Domain = svc.cfg.CookieDomain
}
sess.Save(c.Request(), c.Response())
- return c.Redirect(302, "/")
+ return c.JSON(http.StatusOK, "Logout successful")
+}
+
+func (svc *Service) InfoHandler(c echo.Context) error {
+ csrf, _ := c.Get(middleware.DefaultCSRFConfig.ContextKey).(string)
+ user, err := svc.GetUser(c)
+ if err != nil {
+ return err
+ }
+ responseBody := &InfoResponse{}
+ responseBody.BackendType = svc.cfg.LNBackendType
+ responseBody.User = *user
+ responseBody.Csrf = csrf
+ return c.JSON(http.StatusOK, responseBody)
}
diff --git a/frontend/index.html b/frontend/index.html
index 2982423c..248504e6 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -4,7 +4,7 @@
Alby - Nostr Wallet Connect
-
+
diff --git a/frontend/package.json b/frontend/package.json
index 351c4871..2facd7f3 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -10,11 +10,16 @@
"preview": "vite preview"
},
"dependencies": {
+ "axios": "^1.6.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-qr-code": "^2.0.12",
"react-router-dom": "^6.21.0"
},
"devDependencies": {
+ "@tailwindcss/aspect-ratio": "^0.4.2",
+ "@tailwindcss/forms": "^0.5.7",
+ "@tailwindcss/typography": "^0.5.10",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.0.0",
diff --git a/frontend/src/App.css b/frontend/src/App.css
deleted file mode 100644
index b9d355df..00000000
--- a/frontend/src/App.css
+++ /dev/null
@@ -1,42 +0,0 @@
-#root {
- max-width: 1280px;
- margin: 0 auto;
- padding: 2rem;
- text-align: center;
-}
-
-.logo {
- height: 6em;
- padding: 1.5em;
- will-change: filter;
- transition: filter 300ms;
-}
-.logo:hover {
- filter: drop-shadow(0 0 2em #646cffaa);
-}
-.logo.react:hover {
- filter: drop-shadow(0 0 2em #61dafbaa);
-}
-
-@keyframes logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
-
-@media (prefers-reduced-motion: no-preference) {
- a:nth-of-type(2) .logo {
- animation: logo-spin infinite 20s linear;
- }
-}
-
-.card {
- padding: 2em;
-}
-
-.read-the-docs {
- color: #888;
-}
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 195354d0..1b86eb01 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -2,14 +2,16 @@ import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import { UserProvider } from "./context/UserContext";
import RequireAuth from "./context/RequireAuth";
-import Navbar from './components/navbar';
-import Footer from './components/footer';
-import NotFound from './screens/NotFound';
+
import About from "./screens/About";
import Connections from "./screens/apps/Index";
-
-import './App.css'
import Show from "./screens/apps/Show";
+import Login from "./screens/Login";
+import NotFound from './screens/NotFound';
+
+import Navbar from './components/Navbar';
+import Footer from './components/Footer';
+import New from "./screens/apps/New";
function App() {
return (
@@ -27,9 +29,10 @@ function App() {
} />
} />
} />
+ } />
} />
- Login} />
+ } />
} />
diff --git a/frontend/src/components/QRCode.tsx b/frontend/src/components/QRCode.tsx
new file mode 100644
index 00000000..d51ee18f
--- /dev/null
+++ b/frontend/src/components/QRCode.tsx
@@ -0,0 +1,35 @@
+import ReactQRCode from "react-qr-code";
+
+export type Props = {
+ value: string;
+ size?: number;
+ className?: string;
+
+ // set the level to Q if there are overlays
+ // Q will improve error correction (so we can add overlays covering up to 25% of the QR)
+ // at the price of decreased information density (meaning the QR codes "pixels" have to be
+ // smaller to encode the same information).
+ // While that isn't that much of a problem for lightning addresses (because they are usually quite short),
+ // for invoices that contain larger amount of data those QR codes can get "harder" to read.
+ // (meaning you have to aim your phone very precisely and have to wait longer for the reader
+ // to recognize the QR code)
+ level?: "Q" | undefined;
+};
+
+export default function QRCode({ value, size, level, className }: Props) {
+ // TODO: Theme option in settings?
+ const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches
+ const fgColor = isDark ? "#FFFFFF" : "#000000";
+ const bgColor = isDark ? "#000000" : "#FFFFFF";
+
+ return (
+
+ );
+}
diff --git a/frontend/src/components/navbar.tsx b/frontend/src/components/navbar.tsx
index 9817ffee..cc17b2fe 100644
--- a/frontend/src/components/navbar.tsx
+++ b/frontend/src/components/navbar.tsx
@@ -1,8 +1,9 @@
import { Outlet } from "react-router-dom";
+import { useUser } from "../context/UserContext";
function Navbar() {
- // const { user } = useAccount()
+ const { logout } = useUser()
return (
<>
@@ -46,13 +47,13 @@ function Navbar() {
src="/public/images/caret.svg"
/>
-
diff --git a/frontend/src/context/RequireAuth.tsx b/frontend/src/context/RequireAuth.tsx
index 699df083..71b011c8 100644
--- a/frontend/src/context/RequireAuth.tsx
+++ b/frontend/src/context/RequireAuth.tsx
@@ -2,14 +2,15 @@ import { Navigate, useLocation } from "react-router-dom";
import { useUser } from "./UserContext";
function RequireAuth({ children }: { children: JSX.Element }) {
- const auth = useUser();
+ const { info, loading } = useUser();
const location = useLocation();
- if (auth.loading) {
+ if (loading) {
return null;
}
- if (!auth.user) {
+ if (!info?.user) {
+ // TODO: Use the location to redirect back in /alby/auth?c=
return ;
}
diff --git a/frontend/src/context/UserContext.tsx b/frontend/src/context/UserContext.tsx
index 46371cc1..232d0927 100644
--- a/frontend/src/context/UserContext.tsx
+++ b/frontend/src/context/UserContext.tsx
@@ -5,39 +5,50 @@ import {
useEffect,
useState,
} from "react";
+import { InfoResponse, UserInfo } from "../types";
+import axios from "axios";
interface UserContextType {
- user: Record | null;
+ info: UserInfo | null;
loading: boolean;
- logout: (callback: VoidFunction) => void;
+ logout: () => void;
}
const UserContext = createContext({} as UserContextType);
export function UserProvider({ children }: { children: React.ReactNode }) {
- const [user, setUser] = useState(null);
+ const [info, setInfo] = useState(null);
const [loading, setLoading] = useState(true);
- const logout = () => { //callback: VoidFunction param?
- // do an api request and logout
- return;
- // return msg.request("lock").then(() => {
- // setUserId("");
- // callback();
- // });
+ const logout = async () => {
+ try {
+ await axios.get('/logout');
+ } catch (error) {
+ // TODO: Handle failure
+ console.error('Error during logout:', error);
+ }
+ setInfo(null);
};
+ const getInfo = async () => {
+ try {
+ const response = await axios.get('/api/info');
+ const data: InfoResponse = response.data;
+ setInfo(data);
+ } catch (error) {
+ console.error('Error getting user info:', error);
+ } finally {
+ setLoading(false);
+ }
+ }
+
// Invoked only on on mount.
useEffect(() => {
- // do an api call to /user with the cookie and set the user
- setUser({
- id: "1234",
- })
- setLoading(false)
+ getInfo()
}, []);
const value = {
- user,
+ info,
loading,
logout
};
diff --git a/frontend/src/screens/Login.tsx b/frontend/src/screens/Login.tsx
index 4fc4eab5..f31e4dc7 100644
--- a/frontend/src/screens/Login.tsx
+++ b/frontend/src/screens/Login.tsx
@@ -1,4 +1,17 @@
+import { useNavigate } from "react-router-dom";
+import { useUser } from "../context/UserContext";
+import { useEffect } from "react";
+
function Login() {
+ const { info } = useUser()
+ const navigate = useNavigate()
+
+ useEffect(() => {
+ if (info?.user) {
+ navigate('/');
+ }
+ }, [navigate, info?.user]);
+
return(
![]()
{
const pairingUri = "YOUR_PAIRING_URI"; // Replace with actual data or props
diff --git a/frontend/src/screens/apps/Index.tsx b/frontend/src/screens/apps/Index.tsx
index ef4c45cc..ed636b61 100644
--- a/frontend/src/screens/apps/Index.tsx
+++ b/frontend/src/screens/apps/Index.tsx
@@ -2,8 +2,9 @@
import { useEffect, useState } from "react";
import { useNavigate } from 'react-router-dom';
-import Loading from '../../components/loading';
+import Loading from '../../components/Loading';
import { App, NostrEvent, ListAppsResponse } from "../../types";
+import axios from "axios";
function Connections() {
const [apps, setApps] = useState
(null);
@@ -18,8 +19,8 @@ function Connections() {
useEffect(() => {
const fetchAppsData = async () => {
try {
- const response = await fetch("/api/apps");
- const data: ListAppsResponse = await response.json();
+ const response = await axios.get("/api/apps");
+ const data: ListAppsResponse = response.data;
setApps(data.apps);
setEventsCounts(data.eventsCounts);
setLastEvents(data.lastEvent);
@@ -38,7 +39,7 @@ function Connections() {
<>
Connected apps
-
+
{
- const [formData, setFormData] = useState({
- csrf: '',
- pubkey: '',
- returnTo: '',
- name: '',
- requestMethods: '',
- expiresAt: '',
- maxAmount: '',
- budgetRenewal: 'monthly',
- customRequestMethods: '',
- // requestMethodHelper: [],
- userEmail: '',
- });
-
- useEffect(() => {
- // Replace this with your actual API call
- const fetchData = async () => {
- const mockApiResponse = {
- // Mock data structure, replace with actual API response structure
- csrf: 'csrf-token',
- pubkey: 'public-key',
- returnTo: 'return-url',
- name: 'App Name',
- requestMethods: 'default-methods',
- expiresAt: 'expiry-date',
- maxAmount: '10000',
- budgetRenewal: 'monthly',
- customRequestMethods: '',
- // requestMethodHelper: [
- // { key: 'method1', description: 'Method 1', icon: 'icon1', checked: true },
- // { key: 'method2', description: 'Method 2', icon: 'icon2', checked: false },
- // // Add more methods as needed
- // ],
- userEmail: 'user@example.com',
- };
- setFormData({ ...formData, ...mockApiResponse });
- };
-
- fetchData();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- const handleInputChange = (event: React.ChangeEvent) => {
- setFormData({ ...formData, [event.target.name]: event.target.value });
- };
+ const { info } = useUser();
+ // const navigate = useNavigate();
+ const currentUser: User = (info as UserInfo).user!;
+
+ const location = useLocation();
+ const queryParams = new URLSearchParams(location.search);
+
+ const [edit, setEdit] = useState(false);
+
+ const nameParam = (queryParams.get('name') || queryParams.get('c')) ?? ""
+ const [appName, setAppName] = useState(nameParam);
+ const pubkey = queryParams.get('pubkey') ?? "";
+ const returnTo = queryParams.get('return_to') ?? "";
+
+ const budgetRenewalParam = queryParams.get('budget_renewal') as BudgetRenewalType;
+ const budgetRenewal: BudgetRenewalType = validBudgetRenewals.includes(budgetRenewalParam) ? budgetRenewalParam : "monthly";
+
+ const reqMethodsParam = queryParams.get('request_methods');
+ const [requestMethods, setRequestMethods] = useState(reqMethodsParam ?? Object.keys(nip47MethodDescriptions).join(' '));
+
+ const maxAmountParam = queryParams.get('max_amount') ?? "";
+ const [maxAmount, setMaxAmount] = useState(parseInt(maxAmountParam));
+
+ const parseExpiresParam = (expiresParam: string): string => {
+ if (/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(expiresParam)) {
+ const d = new Date(expiresParam);
+ const isIso = d instanceof Date && !isNaN(d.getTime()) && d.toISOString() === expiresAtParam;
+ if (isIso) {
+ return expiresParam
+ }
+ }
+ if (!isNaN(parseInt(expiresParam))){
+ return (new Date(parseInt(expiresAtParam as string) * 1000)).toISOString()
+ }
+ return ""
+ }
+
+ // Only timestamp in seconds or ISO string is expected
+ const expiresAtParam = parseExpiresParam(queryParams.get('expires_at') ?? "")
+ const [expiresAt, setExpiresAt] = useState(expiresAtParam ?? "");
+ const [days, setDays] = useState(0);
+ const [expireOptions, setExpireOptions] = useState(false);
- const handleFormSubmit = async (event: React.FormEvent) => {
+ const today = new Date();
+ const handleDays = (days: number) => {
+ setDays(days);
+ if (!days) {
+ setExpiresAt("");
+ return;
+ }
+ const expiryDate = new Date(today.getTime() + days * 24 * 60 * 60 * 1000);
+ setExpiresAt(expiryDate.toISOString());
+ }
+
+ const handleRequestMethodChange = (event: React.ChangeEvent) => {
+ const rm = event.target.value;
+ if (requestMethods.includes(rm)) {
+ // If checked and item is already in the list, remove it
+ const newMethods = requestMethods.split(" ").filter((reqMethod) => reqMethod !== rm).join(" ")
+ setRequestMethods(newMethods);
+ } else {
+ setRequestMethods(`${requestMethods} ${rm}`);
+ }
+ }
+
+ const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
- const response = await fetch('/apps', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify(formData),
+ if (!info) return;
+ // Here you'd handle form submission. For example:
+ console.log('Form Data', {
+ appName, pubkey, returnTo, maxAmount, budgetRenewal, expiresAt, requestMethods
});
- console.log(response)
- // Handle response
+ const formData = new FormData();
+ formData.append("name", appName);
+ formData.append("pubkey", pubkey);
+ formData.append("maxAmount", maxAmount.toString());
+ formData.append("budgetRenewal", budgetRenewal);
+ formData.append("expiresAt", expiresAt);
+ formData.append("requestMethods", requestMethods);
+ formData.append("returnTo", returnTo);
+ formData.append("_csrf", info.csrf);
+ try {
+ const response = await axios.post("/api/apps", formData)
+ console.log(response)
+ // TODO: Navigate to success screen with data
+ // navigate("/apps/success");
+ } catch (error) {
+ // TODO: Deal with invalid pubkey format error
+ // Invalid expiresAt error
+ console.error('Error deleting app:', error);
+ }
};
- // Functionality for checkbox, expiry date, and other interactions...
- // You will need to implement these based on your specific needs
-
return (
-
-