diff --git a/apps/supabase/src/supa.schemas.ts b/apps/supabase/src/supa.schemas.ts index f47a1ead2..c059cedfa 100644 --- a/apps/supabase/src/supa.schemas.ts +++ b/apps/supabase/src/supa.schemas.ts @@ -301,21 +301,27 @@ export const transferUpdateSchema = z.object({ export const transferRelationshipsSchema = z.tuple([]); export const userRowSchema = z.object({ - address: z.string(), + account: z.string(), + address: z.array(z.string()), created_at: z.string(), id: z.number(), + short_link: z.string().nullable(), }); export const userInsertSchema = z.object({ - address: z.string(), + account: z.string(), + address: z.array(z.string()), created_at: z.string().optional(), - id: z.number(), + id: z.number().optional(), + short_link: z.string().optional().nullable(), }); export const userUpdateSchema = z.object({ - address: z.string().optional(), + account: z.string().optional(), + address: z.array(z.string()).optional(), created_at: z.string().optional(), id: z.number().optional(), + short_link: z.string().optional().nullable(), }); export const userRelationshipsSchema = z.tuple([]); diff --git a/apps/supabase/src/supa.types.ts b/apps/supabase/src/supa.types.ts index e8d3dddae..e75d329cf 100644 --- a/apps/supabase/src/supa.types.ts +++ b/apps/supabase/src/supa.types.ts @@ -281,19 +281,25 @@ export type Database = { } user: { Row: { - address: string + account: string + address: string[] created_at: string id: number + short_link: string | null } Insert: { - address: string + account: string + address: string[] created_at?: string - id: number + id?: number + short_link?: string | null } Update: { - address?: string + account?: string + address?: string[] created_at?: string id?: number + short_link?: string | null } Relationships: [] } diff --git a/apps/webapp/actions.ts b/apps/webapp/actions.ts index 3be6d092e..76376e672 100644 --- a/apps/webapp/actions.ts +++ b/apps/webapp/actions.ts @@ -1,13 +1,13 @@ 'use server' -import { cookies } from 'next/headers' -import { fromEntries } from 'app-lib' import { handleAxiosError } from '@/lib/utils' +import { createSupabaseServerClient } from '@/services/supabase' +import { presaleInsertSchema } from '@repo/supabase' +import { fromEntries } from 'app-lib' import axios from 'axios' +import { cookies } from 'next/headers' import { Resend } from 'resend' import { z } from 'zod' -import { createSupabaseServerClient } from '@/services/supabase' -import { presaleInsertSchema } from '@repo/supabase' // get session object by id export async function getSesssion(formData: FormData) { @@ -114,26 +114,26 @@ export async function subscribeToNewsletter( } // generate dub.co links -export async function generateShortLink(path: string) { +export async function generateShortLink(url: string) { const cookieStorage = cookies() try { const getShareLinkCookies = cookieStorage.get('bitlauncher-share-link') const resolved: DubShareLinkResponse = !getShareLinkCookies ? await axios - .post( - `https://api.dub.co/links?workspaceId=${process.env.DUB_WORKSPACE_ID}`, - { - domain: 'bitcash.to', - url: path - }, - { - headers: { - Authorization: `Bearer ${process.env.DUB_API_KEY}`, - 'Content-Type': 'application/json' - } + .post( + `https://api.dub.co/links?workspaceId=${process.env.DUB_WORKSPACE_ID}`, + { + domain: 'bitcash.to', + url + }, + { + headers: { + Authorization: `Bearer ${process.env.DUB_API_KEY}`, + 'Content-Type': 'application/json' } - ) - .then(res => res.data) + } + ) + .then(res => res.data) : (JSON.parse(getShareLinkCookies.value) as DubShareLinkResponse) if (!resolved) throw new Error('Failed to generate short link') diff --git a/apps/webapp/components/routes/project/copy-shorlink.tsx b/apps/webapp/components/routes/project/copy-shorlink.tsx index 1cd546c40..eb22b7473 100644 --- a/apps/webapp/components/routes/project/copy-shorlink.tsx +++ b/apps/webapp/components/routes/project/copy-shorlink.tsx @@ -1,30 +1,87 @@ 'use client' -import React, { useState } from 'react' import { AnimatePresence } from 'framer-motion' import { LucideCheck, LucideLoader2, LucideShare, LucideX } from 'lucide-react' +import { useState } from 'react' import { generateShortLink } from '@/actions' import { useSession } from '@/hooks/use-session' +import { uniq } from 'lodash' import { useParams } from 'next/navigation' +import { useAccount } from 'wagmi' +import { useSupabaseClient } from '../../../services/supabase/client' export function CopyShortlinkIcon() { const [status, setStatus] = useState< 'default' | 'loading' | 'copied' | 'error' >('default') const { session } = useSession() + const { address } = useAccount() + const supabase = useSupabaseClient() const existingParams = useParams() // Get existing query parameters const param = session - ? `${existingParams ? '&' : '?'}referrer=${session.account}` + ? `${existingParams ? '&' : '?'}referrer=${session.account}?source=bitlauncher.ai` : '' + const checkShareLink = async () => { + const { data, error } = await supabase + .from('user') + // select shareLink from user where linkPath = 'https://bitlauncher.ai${window.location.pathname}${param}' + .select('id, account, address, short_link') + .eq('account', session?.account || '') + .contains('address', [address || '']) + .single() + + if (error) { + console.error('Failed to check share link:', error) + setStatus('error') + + return { data: null, error } + } + + let returnData = data + + if (!data.short_link) { + const { data: dubCoShortLink, error: dubCoError } = await generateShortLink( + 'https://bitlauncher.ai' + window.location.pathname + param + ) + + if (dubCoError) { + console.error('❌ Failed to check share link:', dubCoError) + setStatus('error') + + return { data: null, error: dubCoError } + } + + // ? Doing upsert (account, address) for current users with active sessions + const updatedAddresses = [ + ...data?.address.length + ? [...data?.address, address as string] + : [address as string] + ] + await supabase.from('user').upsert({ + id: data.id, + account: session?.account || '', + address: uniq(updatedAddresses), + short_link: dubCoShortLink?.shortLink, + }, { + onConflict: 'account', + }) + + returnData = { ...data, short_link: dubCoShortLink?.shortLink as string } + + setTimeout(() => setStatus('default'), 5000) + } + + return { data: returnData, error: null } + } + const copyToClipboard = async () => { setStatus('loading') try { - const { data, error } = await generateShortLink( - 'https://bitlauncher.ai' + window.location.pathname + param - ) + const { data, error } = await checkShareLink() + if (error || !data) throw new Error(error?.message || 'Unknown error') - navigator.clipboard.writeText(data.shortLink) + navigator.clipboard.writeText(data?.short_link || '') setStatus('copied') setTimeout(() => setStatus('default'), 5000) } catch (error) { diff --git a/apps/webapp/hooks/use-session.tsx b/apps/webapp/hooks/use-session.tsx index 90a525fda..7a2a1af7e 100644 --- a/apps/webapp/hooks/use-session.tsx +++ b/apps/webapp/hooks/use-session.tsx @@ -4,15 +4,16 @@ import { getSesssion } from '@/actions' import { genLoginSigningRequest } from '@/lib/eos' import { useSupabaseClient } from '@/services/supabase' import { createContextHook } from '@blockmatic/hooks-utils' -import { identify } from '@multibase/js' import { useConnectModal } from '@rainbow-me/rainbowkit' import { Tables } from '@repo/supabase' +import { uniq } from 'lodash' import { usePathname, useRouter, useSearchParams } from 'next/navigation' import React, { ReactNode, useEffect, useState } from 'react' import { isMobile } from 'react-device-detect' +import toast from 'react-hot-toast' import { useAsync, useLocalStorage, useToggle } from 'react-use' -import { useAccount } from 'wagmi' import { v4 as uuidv4 } from 'uuid' +import { Config, useAccount, UseAccountReturnType } from 'wagmi' import { useMultibase } from './use-multibase' // Exports @@ -40,7 +41,53 @@ function useSessionFn() { let registerUri = 'https://app.bitcash.org?share=JVnL7qzrU' + const verifyUpsertAccount = async ({ session, account }: { + session: Tables<'session'> | null | undefined; + account: UseAccountReturnType + }) => { + if (!session) return + + console.log('🔐 verifyAccount') + + let user + const updatedAddresses = [] + + try { + user = await supabase.from('user') + .select('id, address') + .eq('account', session.account) + .single() + updatedAddresses.push( + ...user?.data?.address.length + ? [...user?.data?.address, account.address as string] + : [account.address as string] + ) + } catch (error) { + console.error('🔐 verifyAccount error', error) + } + + await supabase.from('user').upsert({ + id: user?.data?.id, + account: session.account, + address: uniq(updatedAddresses.filter(Boolean)), + }, { + onConflict: 'account', + }) + + // * If the address is new, we show the toaster. + if (account.address && !user?.data?.address.includes(account.address as string)) { + toast('Address linked successfully!', { icon: '🔗' }) + } + } + + useEffect(() => { + if (!account.address) return + console.log('🔐 update account with received address') + verifyUpsertAccount({ session, account }) + }, [account.address]) + const startSession = (session: Tables<'session'>) => { + verifyUpsertAccount({ session, account }) setSession(session) identifyUser( account.address || '0x', @@ -111,9 +158,8 @@ function useSessionFn() { if (searchParams.get('referrer')) { sessionStorage.setItem('referrer', searchParams.get('referrer') || '') } - registerUri = `https://app.bitcash.org/create-account?referrer=${ - searchParams.get('referrer') || sessionStorage.getItem('referrer') - }&source=bitlauncher.ai` + registerUri = `https://app.bitcash.org/create-account?referrer=${searchParams.get('referrer') || sessionStorage.getItem('referrer') + }&source=bitlauncher.ai` }, []) // default moblie login mode is redirect @@ -132,6 +178,7 @@ function useSessionFn() { params.append('callback', encodedCallbackUrl) const referrer = sessionStorage.getItem('referrer') if (referrer) params.append('referrer', referrer) + params.append('source', 'bitlauncher.ai') location.href = `https://app.bitcash.org?${params.toString()}` } diff --git a/apps/webapp/services/supabase/server.ts b/apps/webapp/services/supabase/server.ts index 8f6b785a5..1eef734bd 100644 --- a/apps/webapp/services/supabase/server.ts +++ b/apps/webapp/services/supabase/server.ts @@ -1,13 +1,17 @@ 'use server' -import { createServerClient, CookieOptions } from '@supabase/ssr' -import { cookies } from 'next/headers' -import { Database } from '@repo/supabase' import { appConfig } from '@/lib/config' +import { Database } from '@repo/supabase' +import { CookieOptions, createServerClient } from '@supabase/ssr' +import { cookies } from 'next/headers' export async function createSupabaseServerClient() { const cookieStore = cookies() + // ! Signaled as deprecated: createServerClient, + /** (from IDE) + * @deprecated Please specify `getAll` and `setAll` cookie methods instead of the `get`, `set` and `remove`.These will not be supported in the next major version. + **/ return createServerClient( appConfig.supabase.url, appConfig.supabase.anonKey,