Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
16 changes: 16 additions & 0 deletions src/app/[locale]/@home/(home)/(bottomsheet)/[widgetRoute]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { notFound } from "next/navigation"

import { AuthenticatedWidgetRoute } from "@/widgets/AuthenticatedWidgetRoute"
import { getWidgetByRoute } from "@/widgets/registry"

export default async function WidgetRoutePage({
params,
}: {
params: Promise<{ widgetRoute: string }>
}) {
const { widgetRoute } = await params
const widget = getWidgetByRoute(widgetRoute)
if (!widget) notFound()

return <AuthenticatedWidgetRoute widgetId={widget.widgetId} />
}
24 changes: 20 additions & 4 deletions src/components/Form/RoundButton/RoundButton.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
"use client"

import type { ReactNode } from "react"
import { BsLink45Deg, BsPlusCircle } from "react-icons/bs"
import { Icon, type IconName } from "ui"

export type RoundButtonProps = {
onClick?: () => void
buttonType: RoundButtonType
buttonType?: RoundButtonType
icon?: IconName
iconElement?: ReactNode
text?: string
fill?: boolean
indicator?: "connected" | "available"
}

export const enum RoundButtonType {
Expand All @@ -32,20 +36,31 @@ const buttonIcons: Record<RoundButtonType, IconName> = {

export const RoundButton = ({
buttonType,
icon,
iconElement,
onClick,
text,
fill = false,
indicator,
}: RoundButtonProps) => {
const selectedIcon =
icon ?? (buttonType === undefined ? undefined : buttonIcons[buttonType])

return (
<div
className="flex flex-col items-center gap-2 max-w-[60px] select-none cursor-pointer"
onClick={onClick}
>
<div className="rounded-full p-px gradient-background">
<div className="flex aspect-square w-14 h-14 p-4 rounded-full justify-center items-center bg-[var(--token-bg)] btn-circle relative">
<Icon name={buttonIcons[buttonType]} size="big" color="white" />
{selectedIcon ? (
<Icon name={selectedIcon} size="big" color="white" />
) : (
iconElement
)}

{fill && buttonType === RoundButtonType.WalletConnect ? (
{indicator === "connected" ||
(fill && buttonType === RoundButtonType.WalletConnect) ? (
<BsLink45Deg
className="absolute bottom-0 right-0 text-white bg-[#1884FF] rounded"
stroke="white"
Expand All @@ -54,7 +69,8 @@ export const RoundButton = ({
style={{ borderRadius: "50%", padding: "3px" }}
/>
) : null}
{fill && buttonType === RoundButtonType.GoodDollar ? (
{indicator === "available" ||
(fill && buttonType === RoundButtonType.GoodDollar) ? (
<BsPlusCircle
className="absolute bottom-0 right-0 text-white bg-[#1884FF] rounded"
stroke="white"
Expand Down
48 changes: 48 additions & 0 deletions src/login/context/SessionContext/storage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"

import type { ISigner, ISignerSession } from "@/login/types"

const localStorageMock = () => ({
getItem: vi.fn(),
setItem: vi.fn(),
removeItem: vi.fn(),
})

describe("session storage", () => {
beforeEach(() => {
vi.resetModules()
})

afterEach(() => {
vi.unstubAllGlobals()
})

it("purges a legacy persisted signer session on startup", async () => {
const storage = localStorageMock()
vi.stubGlobal("localStorage", storage)

await import("./storage")

expect(storage.removeItem).toHaveBeenCalledWith("SIGNER_SESSION")
expect(storage.getItem).not.toHaveBeenCalled()
})

it("keeps private-key sessions in memory only", async () => {
const storage = localStorageMock()
vi.stubGlobal("localStorage", storage)
const { sessionState, setSession } = await import("./storage")
const session: ISignerSession = {
type: "PRIVATE_KEY",
sessionOrigin: "test",
signer: {} as ISigner,
masterSeed: "secret",
}

setSession(session)

expect(sessionState.session).toBe(session)
expect(storage.setItem).not.toHaveBeenCalled()
expect(storage.removeItem).toHaveBeenCalledWith("SIGNER_SESSION")
expect(sessionState.isLoading).toBe(false)
})
})
58 changes: 8 additions & 50 deletions src/login/context/SessionContext/storage.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,14 @@
import type { Jsonify } from "type-fest"
import { proxy, ref } from "valtio"

import { getPrivateKeySession } from "@/login/adapters/privatekey"
import type { Addresses, ISigner, ISignerSession } from "@/login/types"
import { resetWalletConnectDialogs } from "@/sections/WalletConnect/store/walletConnectDialogStore"

const SIGNER_SESSION_KEY = "SIGNER_SESSION"

export const getSessionFromLocalStorage =
async (): Promise<ISignerSession | null> => {
if (typeof localStorage === "undefined") {
return null
}
const session = localStorage.getItem(SIGNER_SESSION_KEY)
if (!session) {
return null
}
const sessionJSON = JSON.parse(session) as Jsonify<ISignerSession>
return await fromJSON(sessionJSON)
}

const fromJSON = async (
sessionJSON: Partial<Jsonify<ISignerSession>>,
): Promise<ISignerSession | null> => {
switch (sessionJSON.type) {
case "PRIVATE_KEY": {
const { userName, profileImage, authProvider, masterSeed } = sessionJSON
if (!masterSeed) {
return null
}
return await getPrivateKeySession(
masterSeed,
"localStorage",
authProvider ?? "NA",
userName,
profileImage,
)
}
const clearPersistedSession = () => {
if (typeof localStorage !== "undefined") {
localStorage.removeItem(SIGNER_SESSION_KEY)
}
return null
}

type SessionState = {
Expand All @@ -64,27 +35,14 @@ export const setSession = (session: ISignerSession | null) => {
sessionState.addresses = undefined
}

if (sessionState.session) {
localStorage.setItem(
SIGNER_SESSION_KEY,
JSON.stringify(sessionState.session),
)
} else {
localStorage.removeItem(SIGNER_SESSION_KEY)
}

clearPersistedSession()
sessionState.isLoading = false
}

export const logout = () => {
resetWalletConnectDialogs()
setSession(null)
}

if (typeof window !== "undefined") {
getSessionFromLocalStorage()
.then(setSession)
.catch((e) => {
console.error("error getting persisted session", e)
setSession(null)
})
}
clearPersistedSession()
sessionState.isLoading = false
1 change: 1 addition & 0 deletions src/login/hooks/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { Addresses, ISignerSession } from "../types"
export type ISessionContext = Partial<ISignerSession> & {
addresses?: Addresses
isLoading: boolean
masterSeed?: string
logout?: () => void
setSession(session: ISignerSession): void
}
Expand Down
107 changes: 42 additions & 65 deletions src/sections/Home/components/WalletSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@ import { useSnapshot } from "valtio"
import { useTranslation } from "translations"
import { AnalyticsEventTypes } from "@/analytics/types"
import { useAnalytics } from "@/analytics/useAnalytics"
import {
RoundButton,
RoundButtonType,
} from "@/components/Form/RoundButton/RoundButton"
import { RoundButton } from "@/components/Form/RoundButton/RoundButton"
import { BottomSheet } from "@/components/Snippet/BottomSheet/BottomSheet"
import { useBottomSheetSnapshot } from "@/components/Snippet/BottomSheet/bottomSheetStore"
import { config } from "@/config"
Expand All @@ -31,6 +28,10 @@ import { pwaVersionStore } from "@/stores/versioningStore"
import { isDeltaMobile, isPasskeyEnabled } from "@/utils/getClientEnvironment"
import { postMessageToReactNative } from "@/utils/messageReactNative"
import { isPwa } from "@/utils/pwa"
import {
coreDashboardActions,
widgetDashboardActions,
} from "@/widgets/registry"

import { Menu } from "./Menu"
import { ProfileCard } from "./ProfileCard"
Expand Down Expand Up @@ -187,61 +188,15 @@ export default function WalletSection({
}
}, [hasMultipleActionRows])

const sendLink = (
<Link href={`/${locale}/send`} scroll={false} prefetch={true}>
<RoundButton
buttonType={RoundButtonType.Send}
text={homeTranslations.send}
/>
</Link>
)

const receiveLink = (
<Link href={`/${locale}/receive`} scroll={false} prefetch={true}>
<RoundButton
buttonType={RoundButtonType.Receive}
text={homeTranslations.receive}
/>
</Link>
)

const swapLink = (
<Link href={`/${locale}/swap`} scroll={false} prefetch={true}>
<RoundButton
buttonType={RoundButtonType.Swap}
text={homeTranslations.swap}
/>
</Link>
)

const predictionsLink = (
<Link href={`/${locale}/predictions`} scroll={false} prefetch={true}>
<RoundButton
buttonType={RoundButtonType.Predictions}
text={homeTranslations.predictions}
/>
</Link>
)

const goodDollarLink = (
<Link href={`/${locale}/gooddollar`} scroll={false} prefetch={true}>
<RoundButton
buttonType={RoundButtonType.GoodDollar}
text={homeTranslations.gooddollar}
fill={canClaim}
/>
</Link>
)

const walletConnectLink = (
<Link href={`/${locale}/walletconnect`} scroll={false} prefetch={true}>
<RoundButton
buttonType={RoundButtonType.WalletConnect}
text={homeTranslations.walletConnect}
fill={sessions.length > 0}
/>
</Link>
)
const actionLabels: Record<string, string> = {
gooddollar: homeTranslations.gooddollar,
send: homeTranslations.send,
receive: homeTranslations.receive,
swap: homeTranslations.swap,
predictions: homeTranslations.predictions,
walletconnect: homeTranslations.walletConnect,
}
const dashboardActions = [...coreDashboardActions, ...widgetDashboardActions]

return (
<>
Expand Down Expand Up @@ -273,12 +228,34 @@ export default function WalletSection({
}
data-testid="wallet-actions"
>
{goodDollarLink}
{sendLink}
{receiveLink}
{swapLink}
{predictionsLink}
{walletConnectLink}
{dashboardActions.map((action) => {
const icon =
action.icon.kind === "system" ? action.icon.name : undefined
const iconElement =
action.icon.kind === "local" ? action.icon.render() : undefined
const indicator =
action.id === "gooddollar" && canClaim
? "available"
: action.id === "walletconnect" && sessions.length > 0
? "connected"
: undefined

return (
<Link
key={action.id}
href={`/${locale}/${action.routeSlug}`}
scroll={false}
prefetch={true}
>
<RoundButton
icon={icon}
iconElement={iconElement}
text={actionLabels[action.id] ?? action.label}
indicator={indicator}
/>
</Link>
)
})}
</div>
{hasMultipleActionRows ? (
<div
Expand Down
12 changes: 3 additions & 9 deletions src/sections/Options/OptionsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { truncateString } from "@/components/Utils/format"
import VersionTag from "@/components/VersionTag/VersionTag"
import { type ISigner, useSessionContext } from "@/login"
import { getPrivateKeyHex } from "@/login/adapters/privatekey"
import { getSessionFromLocalStorage } from "@/login/context/SessionContext/storage"

import OptionsMenu from "./components/OptionsMenu"
import styles from "./OptionsView.module.css"
Expand All @@ -21,14 +20,12 @@ export default function OptionsView() {
const { createToast } = useToast()
const optionsTranslations = translations.options
setBottomSheetProps({ title: optionsTranslations.title })
const { signer, type } = useSessionContext()
const { signer, type, masterSeed } = useSessionContext()
const { captureEvent } = useAnalytics()
const [expanded, setExpanded] = useState(false)
const [selectedChainType, setSelectedChainType] = useState<keyof ISigner>()
const handleCopyPrivateKey = async () => {
if (!selectedChainType) return
const session = await getSessionFromLocalStorage()
if (!session || session.type !== "PRIVATE_KEY") return
if (!selectedChainType || type !== "PRIVATE_KEY" || !masterSeed) return

const status = await openDialog({
title: optionsTranslations.confirmation,
Expand All @@ -38,10 +35,7 @@ export default function OptionsView() {
})

if (status === "accepted") {
const privateKey = await getPrivateKeyHex(
selectedChainType,
session.masterSeed,
)
const privateKey = await getPrivateKeyHex(selectedChainType, masterSeed)
navigator.clipboard.writeText(privateKey)
captureEvent({ type: AnalyticsEventTypes.PrivateKeyCopied })
setSelectedChainType(undefined)
Expand Down
Loading
Loading