diff --git a/.env.example b/.env.example index f4bc94d..049e3b7 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,15 @@ VITE_PROJECT_ID= VITE_MOCK_GNOSIS_PAY=true +# Supabase +# Get these from your Supabase project settings +# PUBLIC_SUPABASE_URL: Settings > General > Project URL +# PUBLIC_SUPABASE_PUBLISHABLE_KEY: Settings > API > Project API keys > Publishable Key (sb_publishable_*) +# See: https://supabase.com/docs/guides/api/api-keys#overview +PUBLIC_SUPABASE_URL= +PUBLIC_SUPABASE_PUBLISHABLE_KEY= +VITE_USE_SUPABASE=true + # Blockscout API for payment verification # For Chiado testnet VITE_BLOCKSCOUT_BASE=https://gnosis-chiado.blockscout.com/api/v2 diff --git a/.github/workflows/netlify-deploy.yml b/.github/workflows/netlify-deploy.yml index 2fd68a3..f3dcf18 100644 --- a/.github/workflows/netlify-deploy.yml +++ b/.github/workflows/netlify-deploy.yml @@ -28,11 +28,14 @@ jobs: run: bun run build env: VITE_MOCK_GNOSIS_PAY: true + VITE_USE_SUPABASE: true + PUBLIC_SUPABASE_URL: ${{ secrets.PUBLIC_SUPABASE_URL }} + PUBLIC_SUPABASE_PUBLISHABLE_KEY: ${{ secrets.PUBLIC_SUPABASE_PUBLISHABLE_KEY }} - name: Deploy to Netlify uses: nwtgck/actions-netlify@v3.0 with: - publish-dir: './.svelte-kit/output' + publish-dir: './.svelte-kit/build' production-branch: main github-token: ${{ secrets.GITHUB_TOKEN }} deploy-message: 'Deploy from GitHub Actions' diff --git a/bun.lock b/bun.lock index 4671d7d..0b017e5 100644 --- a/bun.lock +++ b/bun.lock @@ -12,6 +12,7 @@ "qrcode": "^1.5.4", "viem": "^2.39.0", "wagmi": "^2.19.4", + "zod": "^4.1.13", }, "devDependencies": { "@eslint/compat": "^1.4.0", @@ -1828,7 +1829,7 @@ "zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="], - "zod": ["zod@3.22.4", "", {}, "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg=="], + "zod": ["zod@4.1.13", "", {}, "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig=="], "zustand": ["zustand@5.0.0", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-LE+VcmbartOPM+auOjCCLQOsQ05zUTp8RkgwRzefUk+2jISdMMFnxvyTjA4YNWr5ZGXYbVsEMZosttuxUBkojQ=="], @@ -1902,6 +1903,8 @@ "@reown/appkit-ui/qrcode": ["qrcode@1.5.3", "", { "dependencies": { "dijkstrajs": "^1.0.1", "encode-utf8": "^1.0.3", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg=="], + "@reown/appkit-wallet/zod": ["zod@3.22.4", "", {}, "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg=="], + "@rollup/plugin-babel/rollup": ["rollup@2.79.2", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ=="], "@rollup/plugin-node-resolve/@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], @@ -2230,6 +2233,8 @@ "@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-wallet/@walletconnect/logger": ["@walletconnect/logger@2.1.2", "", { "dependencies": { "@walletconnect/safe-json": "^1.0.2", "pino": "7.11.0" } }, "sha512-aAb28I3S6pYXZHQm5ESB+V6rDqIYfsnHaQyzFbwUUBFY4H0OXx/YtTl8lvhUNhMMfb9UxbwEBS253TlXUYJWSw=="], + "@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-wallet/zod": ["zod@3.22.4", "", {}, "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg=="], + "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/types/@walletconnect/logger": ["@walletconnect/logger@2.1.2", "", { "dependencies": { "@walletconnect/safe-json": "^1.0.2", "pino": "7.11.0" } }, "sha512-aAb28I3S6pYXZHQm5ESB+V6rDqIYfsnHaQyzFbwUUBFY4H0OXx/YtTl8lvhUNhMMfb9UxbwEBS253TlXUYJWSw=="], "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/logger": ["@walletconnect/logger@2.1.2", "", { "dependencies": { "@walletconnect/safe-json": "^1.0.2", "pino": "7.11.0" } }, "sha512-aAb28I3S6pYXZHQm5ESB+V6rDqIYfsnHaQyzFbwUUBFY4H0OXx/YtTl8lvhUNhMMfb9UxbwEBS253TlXUYJWSw=="], diff --git a/netlify.toml b/netlify.toml index 74eba01..7630292 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,3 +1,8 @@ [build] - command = "npm run build" - publish = "build" + command = "bun run build" + publish = ".svelte-kit/build" + +[build.environment] + BUN_VERSION = "1.2.15" + VITE_USE_SUPABASE = "true" + VITE_MOCK_GNOSIS_PAY = "true" diff --git a/package.json b/package.json index f5cb8e7..695adf0 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "private": true, "version": "0.0.1", "type": "module", - "packageManger": "bun@1.1.38", + "packageManager": "bun@1.2.15", "scripts": { "dev": "vite dev", "build": "vite build", @@ -55,6 +55,7 @@ "lucide-svelte": "^0.554.0", "qrcode": "^1.5.4", "viem": "^2.39.0", - "wagmi": "^2.19.4" + "wagmi": "^2.19.4", + "zod": "^4.1.13" } } diff --git a/src/lib/blockscoutVerifier.ts b/src/lib/blockscoutVerifier.ts index d0823ef..6213926 100644 --- a/src/lib/blockscoutVerifier.ts +++ b/src/lib/blockscoutVerifier.ts @@ -83,7 +83,7 @@ export async function verifyAndMarkXDAIPaid(splitId: string, split: Split) { await updateSplit(splitId, (s) => ({ ...s, payments: newPayments - })); + }), split.payerAddress); } } catch (error) { console.error('Blockscout verification error:', error); diff --git a/src/lib/server/supabase.ts b/src/lib/server/supabase.ts new file mode 100644 index 0000000..c9f9a9e --- /dev/null +++ b/src/lib/server/supabase.ts @@ -0,0 +1,25 @@ +import { createClient, type SupabaseClient } from '@supabase/supabase-js'; +import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_PUBLISHABLE_KEY } from '$env/static/public'; +import type { Database } from '$lib/supabase-types'; + +let _supabase: SupabaseClient | undefined; + +export function getSupabase(): SupabaseClient { + if (!_supabase) { + // Use public env vars (baked in at build time) + const supabaseUrl = PUBLIC_SUPABASE_URL; + const supabaseKey = PUBLIC_SUPABASE_PUBLISHABLE_KEY; + + if (!supabaseUrl || !supabaseKey) { + throw new Error( + `Missing Supabase env vars. URL: ${!!supabaseUrl}, KEY: ${!!supabaseKey}. ` + + `Make sure PUBLIC_SUPABASE_URL and PUBLIC_SUPABASE_PUBLISHABLE_KEY are set.` + ); + } + + _supabase = createClient(supabaseUrl, supabaseKey, { + auth: { autoRefreshToken: false, persistSession: false } // Server-side: no auth persistence + }); + } + return _supabase; +} diff --git a/src/lib/storage.ts b/src/lib/storage.ts index d22f402..b6df11d 100644 --- a/src/lib/storage.ts +++ b/src/lib/storage.ts @@ -1,40 +1,39 @@ import { browser } from '$app/environment'; import type { Split, Participant } from './types'; -import { supabase } from './supabase'; const USE_SUPABASE = import.meta.env.VITE_USE_SUPABASE === 'true'; const STORAGE_KEY = 'gnosisSplits'; +const API_BASE = '/api/splits'; -export async function getSplits(): Promise { +export async function getSplits(userAddress?: string): Promise { if (!USE_SUPABASE) { if (!browser) return []; try { const data = localStorage.getItem(STORAGE_KEY); - return data ? JSON.parse(data) : []; + const splits = data ? JSON.parse(data) : []; + if (userAddress) { + return splits.filter((s: Split) => s.payerAddress.toLowerCase() === userAddress.toLowerCase()); + } + return splits; } catch (error) { console.error('Failed to load splits:', error); return []; } } else { - const { data, error } = await supabase - .from('splits') - .select('*') - .order('created_at', { ascending: false }); - if (error) throw new Error("Error when getting splits: ", error); - if (!data) return []; - - return data.map((row) => ({ - id: row.id, - description: row.description, - totalAmount: row.total_amount, - date: row.date, - payerAddress: row.payer_address, - participants: row.participants as unknown as Split['participants'], - payments: row.payments as unknown as Split['payments'], - createdAt: row.created_at as unknown as Split['createdAt'], - sourceTxId: row.source_tx_id || undefined - })); + try { + const url = userAddress ? `${API_BASE}?address=${encodeURIComponent(userAddress)}` : API_BASE; + const response = await fetch(url); + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + console.error('API error:', response.status, errorData); + throw new Error(`Failed to fetch splits: ${response.status}`); + } + return await response.json(); + } catch (error) { + console.error('Failed to load splits:', error); + return []; + } } } @@ -54,67 +53,34 @@ export async function saveSplit(split: Omit): Promise } } - const { data, error } = await supabase - .from('splits') - .insert({ - description: split.description, - total_amount: split.totalAmount, - date: split.date, - payer_address: split.payerAddress, - participants: split.participants as any, - payments: split.payments as any, - source_tx_id: split.sourceTxId - }) - .select() - .single(); - - if (error) throw error; - - return { - id: data.id, - description: data.description, - totalAmount: data.total_amount, - date: data.date, - payerAddress: data.payer_address, - participants: data.participants as unknown as Split['participants'], - payments: data.payments as unknown as Split['payments'], - createdAt: data.created_at as unknown as Split['createdAt'], - sourceTxId: data.source_tx_id || undefined - }; + const response = await fetch(API_BASE, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(split) + }); + + if (!response.ok) throw new Error('Failed to create split'); + return await response.json(); } -export async function getSplit(id: string): Promise { +export async function getSplit(id: string, userAddress?: string): Promise { if (!USE_SUPABASE) { - const splits = await getSplits(); + const splits = await getSplits(userAddress); return splits.find((s) => s.id === id); } - const { data, error } = await supabase - .from('splits') - .select('*') - .eq('id', id) - .single() - - if (error || !data) return undefined; - - const { id: foundId, description, total_amount, date, payer_address, participants, payments, created_at, source_tx_id } = data; - - const foundSplit = { - id: foundId, - description, - totalAmount: total_amount, - date, - payerAddress: payer_address, - participants: participants as unknown as Split['participants'], - payments: payments as unknown as Split['payments'], - createdAt: created_at as unknown as Split['createdAt'], - sourceTxId: source_tx_id || undefined, + try { + const url = userAddress ? `${API_BASE}/${id}?address=${encodeURIComponent(userAddress)}` : `${API_BASE}/${id}`; + const response = await fetch(url); + if (!response.ok) return undefined; + return await response.json(); + } catch (error) { + console.error('Failed to load split:', error); + return undefined; } - - return foundSplit; } -export async function updateSplit(id: string, updater: (split: Split) => Split): Promise { +export async function updateSplit(id: string, updater: (split: Split) => Split, userAddress?: string): Promise { if (!USE_SUPABASE) { if (!browser) return; @@ -131,24 +97,18 @@ export async function updateSplit(id: string, updater: (split: Split) => Split): throw error; } } else { - const split = await getSplit(id); + const split = await getSplit(id, userAddress); if (!split) return; const updated = updater(split); - const { error } = await supabase - .from('splits') - .update({ - description: updated.description, - total_amount: updated.totalAmount, - date: updated.date, - payer_address: updated.payerAddress, - participants: updated.participants as any, - payments: updated.payments as any, - source_tx_id: updated.sourceTxId - }) - .eq('id', id); - - if (error) throw error; + const url = userAddress ? `${API_BASE}/${id}?address=${encodeURIComponent(userAddress)}` : `${API_BASE}/${id}`; + const response = await fetch(url, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updated) + }); + + if (!response.ok) throw new Error('Failed to update split'); } } @@ -165,8 +125,11 @@ export async function deleteSplit(id: string): Promise { throw error; } } else { - const { error } = await supabase.from('splits').delete().eq('id', id); - if (error) throw error; + const response = await fetch(`${API_BASE}/${id}`, { + method: 'DELETE' + }); + + if (!response.ok) throw new Error('Failed to delete split'); } } @@ -189,13 +152,14 @@ export async function updateSplitParticipants(splitId: string, participants: Par } } - const { error } = await supabase - .from('splits') - .update({ participants: participants as any }) - .eq('id', splitId); + const response = await fetch(`${API_BASE}/${splitId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ participants }) + }); - if (error) { - console.error('Failed to update participants', error); - throw error; + if (!response.ok) { + console.error('Failed to update participants'); + throw new Error('Failed to update participants'); } } diff --git a/src/lib/stores/wallet.ts b/src/lib/stores/wallet.ts index 18069da..c28ec08 100644 --- a/src/lib/stores/wallet.ts +++ b/src/lib/stores/wallet.ts @@ -10,6 +10,16 @@ export const appKitStore = writable({ }); if (browser && appKit) { + const account = appKit.getAccount?.(); + const state = appKit.getState?.(); + + appKitStore.set({ + open: state?.open ?? false, + selectedNetworkId: state?.selectedNetworkId, + address: account?.address, + isConnected: account?.isConnected ?? false + }); + appKit.subscribeState((state) => { appKitStore.update((current) => ({ ...current, diff --git a/src/lib/supabase.ts b/src/lib/supabase.ts index c0989a3..e632caa 100644 --- a/src/lib/supabase.ts +++ b/src/lib/supabase.ts @@ -1,14 +1,7 @@ -import { createClient } from '@supabase/supabase-js'; import { writable } from 'svelte/store'; import type { Split } from './types'; -import type { Database } from './supabase-types'; -const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; -const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY; - -export const supabase = createClient(supabaseUrl, supabaseAnonKey); - -export const createSplitStore = (splitId: string) => { +export const createSplitStore = (splitId: string, userAddress?: string) => { const { subscribe, set } = writable(null); if (import.meta.env.VITE_USE_SUPABASE !== 'true') { @@ -16,62 +9,25 @@ export const createSplitStore = (splitId: string) => { } const load = async () => { - const { data, error } = await supabase.from('splits').select('*').eq('id', splitId).single(); - - if (error) { - console.error('Failed to load split: ', error); - return; + try { + const url = userAddress ? `/api/splits/${splitId}?address=${encodeURIComponent(userAddress)}` : `/api/splits/${splitId}`; + const response = await fetch(url); + if (!response.ok) { + console.error('Failed to load split'); + return; + } + const split = await response.json(); + set(split); + } catch (error) { + console.error('Failed to load split:', error); } - - set({ - id: data.id, - description: data.description, - totalAmount: data.total_amount, - date: data.date, - payerAddress: data.payer_address, - participants: data.participants as unknown as Split['participants'], - payments: data.payments as unknown as Split['payments'], - sourceTxId: data.source_tx_id || undefined, - createdAt: data.created_at as unknown as Split['createdAt'], - }); }; load(); - const channel = supabase - .channel(`split-${splitId}`) - .on( - 'postgres_changes', - { - event: '*', - schema: 'public', - table: 'splits', - filter: `id=eq.${splitId}` - }, - (payload) => { - if (payload.eventType === 'UPDATE' || payload.eventType === 'INSERT') { - const row = payload.new as Database['public']['Tables']['splits']['Row']; - set({ - id: row.id, - description: row.description, - totalAmount: row.total_amount, - date: row.date, - payerAddress: row.payer_address, - participants: row.participants as unknown as Split['participants'], - payments: row.payments as unknown as Split['payments'], - sourceTxId: row.source_tx_id || undefined, - createdAt: row.created_at as unknown as Split['createdAt'], - }); - } - } - ) - .subscribe((status) => { - if (status === 'SUBSCRIBED') console.log('Realtime connected for split', splitId); - }); - return { subscribe, refresh: load, - unsubscribe: () => supabase.removeChannel(channel) + unsubscribe: () => { } }; }; diff --git a/src/lib/validation.ts b/src/lib/validation.ts new file mode 100644 index 0000000..9e81631 --- /dev/null +++ b/src/lib/validation.ts @@ -0,0 +1,32 @@ +import { z } from 'zod'; + +export const ParticipantSchema = z.object({ + address: z.string().min(1, 'Address is required'), + name: z.string().optional(), + amount: z.number().positive('Amount must be positive') +}); + +export const PaymentSchema = z.object({ + address: z.string().min(1, 'Address is required'), + txHash: z.string().optional() +}); + +export const SplitCreateSchema = z.object({ + description: z.string().min(1, 'Description is required'), + totalAmount: z.number().positive('Total amount must be positive'), + date: z.string().min(1, 'Date is required'), + payerAddress: z.string().min(1, 'Payer address is required'), + participants: z.array(ParticipantSchema).min(1, 'At least one participant is required'), + payments: z.array(PaymentSchema).default([]), + sourceTxId: z.string().optional() +}); + +export const SplitUpdateSchema = SplitCreateSchema.partial(); + +export const ParticipantsUpdateSchema = z.object({ + participants: z.array(ParticipantSchema).min(1, 'At least one participant is required') +}); + +export type SplitCreate = z.infer; +export type SplitUpdate = z.infer; +export type ParticipantsUpdate = z.infer; diff --git a/src/routes/api/splits/+server.ts b/src/routes/api/splits/+server.ts new file mode 100644 index 0000000..4405715 --- /dev/null +++ b/src/routes/api/splits/+server.ts @@ -0,0 +1,109 @@ +import { json } from '@sveltejs/kit'; +import { getSupabase } from '$lib/server/supabase'; +import type { RequestHandler } from '@sveltejs/kit'; +import type { Split } from '$lib/types'; +import { SplitCreateSchema } from '$lib/validation'; + +export const GET: RequestHandler = async ({ url }) => { + try { + const userAddress = url.searchParams.get('address'); + + if (!userAddress) { + console.warn('GET /api/splits - No user address provided'); + return json([], { status: 200 }); + } + + console.log('GET /api/splits - Starting for address:', userAddress); + const supabase = getSupabase(); + console.log('Supabase client created successfully'); + + const { data, error } = await supabase + .from('splits') + .select('*') + .eq('payer_address', userAddress) + .order('created_at', { ascending: false }); + + if (error) { + console.error('Supabase error fetching splits:', { + message: error.message, + code: (error as any).code, + status: (error as any).status + }); + return json({ + error: 'Failed to fetch splits', + details: error.message, + code: (error as any).code + }, { status: 500 }); + } + + const splits: Split[] = (data || []).map((row) => ({ + id: row.id, + description: row.description, + totalAmount: row.total_amount, + date: row.date, + payerAddress: row.payer_address, + participants: row.participants as unknown as Split['participants'], + payments: row.payments as unknown as Split['payments'], + createdAt: row.created_at as unknown as Split['createdAt'], + sourceTxId: row.source_tx_id || undefined + })); + + return json(splits); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error('Error fetching splits:', errorMessage); + return json({ error: 'Internal server error', details: errorMessage }, { status: 500 }); + } +}; + +export const POST: RequestHandler = async ({ request }) => { + try { + const body = await request.json(); + const validationResult = SplitCreateSchema.safeParse(body); + + if (!validationResult.success) { + return json( + { error: 'Validation failed', details: validationResult.error.flatten }, + { status: 400 } + ); + } + + const split = validationResult.data; + const supabase = getSupabase(); + + const { data, error } = await supabase + .from('splits') + .insert({ + description: split.description, + total_amount: split.totalAmount, + date: split.date, + payer_address: split.payerAddress, + participants: split.participants as any, + payments: split.payments as any, + source_tx_id: split.sourceTxId + }) + .select() + .single(); + + if (error) { + return json({ error: 'Failed to create split' }, { status: 500 }); + } + + const newSplit: Split = { + id: data.id, + description: data.description, + totalAmount: data.total_amount, + date: data.date, + payerAddress: data.payer_address, + participants: data.participants as unknown as Split['participants'], + payments: data.payments as unknown as Split['payments'], + createdAt: data.created_at as unknown as Split['createdAt'], + sourceTxId: data.source_tx_id || undefined + }; + + return json(newSplit, { status: 201 }); + } catch (error) { + console.error('Error creating split:', error); + return json({ error: 'Internal server error' }, { status: 500 }); + } +}; diff --git a/src/routes/api/splits/[id]/+server.ts b/src/routes/api/splits/[id]/+server.ts new file mode 100644 index 0000000..c4e6429 --- /dev/null +++ b/src/routes/api/splits/[id]/+server.ts @@ -0,0 +1,177 @@ +import { json } from '@sveltejs/kit'; +import { getSupabase } from '$lib/server/supabase'; +import type { RequestHandler } from '@sveltejs/kit'; +import type { Split, Participant } from '$lib/types'; +import { SplitUpdateSchema, ParticipantsUpdateSchema } from '$lib/validation'; + +export const GET: RequestHandler = async ({ params, url }) => { + if (!params.id) throw Error('Error when getting split'); + + try { + const userAddress = url.searchParams.get('address'); + const supabase = getSupabase(); + const { data, error } = await supabase + .from('splits') + .select('*') + .eq('id', params.id) + .single(); + + if (error || !data) { + return json({ error: 'Split not found' }, { status: 404 }); + } + + if (userAddress) { + const isCreator = data.payer_address.toLowerCase() === userAddress.toLowerCase(); + const participants = data.participants as unknown as Participant[]; + const isParticipant = participants?.some( + (p: Participant) => p.address.toLowerCase() === userAddress.toLowerCase() + ); + + if (!isCreator && !isParticipant) { + return json({ error: 'Unauthorized' }, { status: 403 }); + } + } + + const split: Split = { + id: data.id, + description: data.description, + totalAmount: data.total_amount, + date: data.date, + payerAddress: data.payer_address, + participants: data.participants as unknown as Split['participants'], + payments: data.payments as unknown as Split['payments'], + createdAt: data.created_at as unknown as Split['createdAt'], + sourceTxId: data.source_tx_id || undefined + }; + + return json(split); + } catch (error) { + console.error('Error fetching split:', error); + return json({ error: 'Internal server error' }, { status: 500 }); + } +}; + +export const PUT: RequestHandler = async ({ params, request }) => { + if (!params.id) throw Error("Error when updating split"); + + try { + const body = await request.json(); + const validationResult = SplitUpdateSchema.safeParse(body); + + if (!validationResult.success) { + return json( + { error: 'Validation failed', details: validationResult.error.flatten }, + { status: 400 } + ); + } + + const updatedSplit = validationResult.data; + const supabase = getSupabase(); + + const { data, error } = await supabase + .from('splits') + .update({ + description: updatedSplit.description, + total_amount: updatedSplit.totalAmount, + date: updatedSplit.date, + payer_address: updatedSplit.payerAddress, + participants: updatedSplit.participants as any, + payments: updatedSplit.payments as any, + source_tx_id: updatedSplit.sourceTxId + }) + .eq('id', params.id) + .select() + .single(); + + if (error) { + console.error('Database error updating split:', error); + return json({ error: 'Failed to update split' }, { status: 500 }); + } + + if (!data) { + return json({ error: 'Split not found' }, { status: 404 }); + } + + const split: Split = { + id: data.id, + description: data.description, + totalAmount: data.total_amount, + date: data.date, + payerAddress: data.payer_address, + participants: data.participants as unknown as Split['participants'], + payments: data.payments as unknown as Split['payments'], + createdAt: data.created_at as unknown as Split['createdAt'], + sourceTxId: data.source_tx_id || undefined + }; + + return json(split); + } catch (error) { + console.error('Error updating split:', error); + return json({ error: 'Internal server error' }, { status: 500 }); + } +}; + +export const DELETE: RequestHandler = async ({ params }) => { + if (!params.id) throw Error('Error when deleting split'); + + try { + const supabase = getSupabase(); + const { error } = await supabase + .from('splits') + .delete() + .eq('id', params.id); + + if (error) { + return json({ error: 'Failed to delete split' }, { status: 500 }); + } + + return json({ success: true }, { status: 200 }); + } catch (error) { + console.error('Error deleting split:', error); + return json({ error: 'Internal server error' }, { status: 500 }); + } +}; + +export const PATCH: RequestHandler = async ({ params, request }) => { + if (!params.id) throw Error('Error when updating participants'); + + try { + const supabase = getSupabase(); + const { data: existingSplit, error: fetchError } = await supabase + .from('splits') + .select('id') + .eq('id', params.id) + .single(); + + if (fetchError || !existingSplit) { + return json({ error: 'Split not found' }, { status: 404 }); + } + + const body = await request.json(); + const validationResult = ParticipantsUpdateSchema.safeParse(body); + + if (!validationResult.success) { + return json( + { error: 'Validation failed', details: validationResult.error.flatten }, + { status: 400 } + ); + } + + const { participants } = validationResult.data; + + const { error } = await supabase + .from('splits') + .update({ participants: participants as any }) + .eq('id', params.id); + + if (error) { + console.error('Database error updating participants:', error); + return json({ error: 'Failed to update participants' }, { status: 500 }); + } + + return json({ success: true }, { status: 200 }); + } catch (error) { + console.error('Error updating participants:', error); + return json({ error: 'Internal server error' }, { status: 500 }); + } +}; diff --git a/src/routes/split/[id]/+page.svelte b/src/routes/split/[id]/+page.svelte index 762ad51..81f3bb8 100644 --- a/src/routes/split/[id]/+page.svelte +++ b/src/routes/split/[id]/+page.svelte @@ -33,9 +33,9 @@ } if (USE_SUPABASE === 'true') { - splitStore = createSplitStore(id); + splitStore = createSplitStore(id, $walletAddress); } else { - const loaded = await getSplit(id); + const loaded = await getSplit(id, $walletAddress); if (!loaded) { goto('/splits'); return; @@ -73,7 +73,7 @@ await verifyAndMarkXDAIPaid(id, split); if (USE_SUPABASE !== 'true') { - const loaded = await getSplit(id); + const loaded = await getSplit(id, $walletAddress || undefined); if (loaded && splitStore) { splitStore = { subscribe: (cb: (value: Split | null) => void) => { @@ -166,10 +166,10 @@ await updateSplit(split.id, (s) => ({ ...s, payments: [...s.payments, { address: myPart.address, txHash: hash }] - })); + }), $walletAddress || undefined); if (USE_SUPABASE !== 'true') { - const loaded = await getSplit(split.id); + const loaded = await getSplit(split.id, $walletAddress || undefined); if (loaded && splitStore) { splitStore = { subscribe: (cb: (value: Split | null) => void) => { diff --git a/src/routes/split/payment/+page.svelte b/src/routes/split/payment/+page.svelte index 5feef9c..c668615 100644 --- a/src/routes/split/payment/+page.svelte +++ b/src/routes/split/payment/+page.svelte @@ -13,7 +13,6 @@ import { config } from '$lib/appkit'; import { Button } from '$lib/components/ui/button'; import * as Card from '$lib/components/ui/card'; - import { Badge } from '$lib/components/ui/badge'; import * as Avatar from '$lib/components/ui/avatar'; import { toast } from 'svelte-sonner'; @@ -32,7 +31,7 @@ return; } - const loaded = await getSplit(splitId); + const loaded = await getSplit(splitId, $walletAddress || participantAddr); if (!loaded) { goto('/splits'); return; @@ -88,9 +87,9 @@ await updateSplit(split.id, (s) => ({ ...s, payments: [...s.payments, { address: participant!.address, txHash: hash }] - })); + }), $walletAddress || undefined); - const updated = await getSplit(split.id); + const updated = await getSplit(split.id, $walletAddress || undefined); if (updated) { split = updated; isPaid = true; diff --git a/src/routes/splits/+page.svelte b/src/routes/splits/+page.svelte index f9822d2..ffea694 100644 --- a/src/routes/splits/+page.svelte +++ b/src/routes/splits/+page.svelte @@ -3,6 +3,7 @@ import { goto } from '$app/navigation'; import AuthGuard from '$lib/components/AuthGuard.svelte'; import { getSplits } from '$lib/storage'; + import { address } from '$lib/stores/wallet'; import type { Split } from '$lib/types'; import { getPaymentStatus } from '$lib/utils'; import { Button } from '$lib/components/ui/button'; @@ -20,7 +21,7 @@ async function loadSplits() { loading = true; try { - const allSplits = await getSplits(); + const allSplits = await getSplits($address); splits = allSplits.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); } catch (error) { console.error('Error when fetching splits:', error); diff --git a/vite.config.ts b/vite.config.ts index 4d1d2ea..3de8761 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,31 +6,32 @@ import { VitePWA } from 'vite-plugin-pwa'; export default defineConfig({ plugins: [ tailwindcss(), - sveltekit(), - VitePWA({ - registerType: 'autoUpdate', - devOptions: { enabled: true }, - workbox: { - globPatterns: ['**/*.{js,css,html,ico,svg,png}'], - runtimeCaching: [ - { - urlPattern: /^https:\/\/.*\.(?:png|jpg|jpeg|svg|gif)$/, - handler: 'CacheFirst', - options: { cacheName: 'images' } - } - ] - }, - manifest: { - name: 'Gnosis Split', - short_name: 'GnoSplit', - icons: [ - { - src: '/gnosis-split-meta.jpg', - sizes: 'any', - type: 'image/jpeg' - } - ] - } - }) + sveltekit() + // Temporarily disabled PWA due to build issues with Netlify adapter + // VitePWA({ + // registerType: 'autoUpdate', + // devOptions: { enabled: true }, + // workbox: { + // globPatterns: ['**/*.{js,css,html,ico,svg,png}'], + // runtimeCaching: [ + // { + // urlPattern: /^https:\/\/.*\.(?:png|jpg|jpeg|svg|gif)$/, + // handler: 'CacheFirst', + // options: { cacheName: 'images' } + // } + // ] + // }, + // manifest: { + // name: 'Gnosis Split', + // short_name: 'GnoSplit', + // icons: [ + // { + // src: '/gnosis-split-meta.jpg', + // sizes: 'any', + // type: 'image/jpeg' + // } + // ] + // } + // }) ] });