Account Switching (#644)
* WIP: Account switching * Fix empty USER query ANON_USER_ID was undefined and thus the query for @anon had no variables. * Apply multiAuthMiddleware in /api/graphql * Fix 'you must be logged in' query error on switch to anon * Add smart 'switch account' button "smart" means that it only shows if there are accounts to which one can switch * Fix multiAuth not set in backend * Comment fixes, minor changes * Use fw-bold instead of 'selected' * Close dropdown and offcanvas Inside a dropdown, we can rely on autoClose but need to wrap the buttons with <Dropdown.Item> for that to work. For the offcanvas, we need to pass down handleClose. * Use button to add account * Some pages require hard reload on account switch * Reinit settings form on account switch * Also don't refetch WalletHistory * Formatting * Use width: fit-content for standalone SignUpButton * Remove unused className * Use fw-bold and text-underline on selected * Fix inconsistent padding of login buttons * Fix duplicate redirect from /settings on anon switch * Never throw during refetch * Throw errors which extend GraphQLError * Only use meAnonSats if logged out * Use reactive variable for meAnonSats The previous commit broke the UI update after anon zaps because we actually updated item.meSats in the cache and not item.meAnonSats. Updating item.meAnonSats was not possible because it's a local field. For that, one needs to use reactive variables. We do this now and thus also don't need the useEffect hack in item-info.js anymore. * Switch to new user * Fix missing cleanup during logout If we logged in but never switched to any other account, the 'multi_auth.user-id' cookie was not set. This meant that during logout, the other 'multi_auth.*' cookies were not deleted. This broke the account switch modal. This is fixed by setting the 'multi_auth.user-id' cookie on login. Additionally, we now cleanup if cookie pointer OR session is set (instead of only if both are set). * Fix comments in middleware * Remove unnecessary effect dependencies setState is stable and thus only noise in effect dependencies * Show but disable unavailable auth methods * make signup button consistent with others * Always reload page on switch * refine account switch styling * logout barrier --------- Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com> Co-authored-by: k00b <k00b@stacker.news>
This commit is contained in:
parent
36e9f3f16f
commit
a6713f9793
|
@ -130,8 +130,9 @@ export default {
|
||||||
|
|
||||||
return await models.user.findUnique({ where: { id: me.id } })
|
return await models.user.findUnique({ where: { id: me.id } })
|
||||||
},
|
},
|
||||||
user: async (parent, { name }, { models }) => {
|
user: async (parent, { id, name }, { models }) => {
|
||||||
return await models.user.findUnique({ where: { name } })
|
if (id) id = Number(id)
|
||||||
|
return await models.user.findUnique({ where: { id, name } })
|
||||||
},
|
},
|
||||||
users: async (parent, args, { models }) =>
|
users: async (parent, args, { models }) =>
|
||||||
await models.user.findMany(),
|
await models.user.findMany(),
|
||||||
|
|
|
@ -139,7 +139,13 @@ export function getGetServerSideProps (
|
||||||
|
|
||||||
const client = await getSSRApolloClient({ req, res })
|
const client = await getSSRApolloClient({ req, res })
|
||||||
|
|
||||||
const { data: { me } } = await client.query({ query: ME })
|
let { data: { me } } = await client.query({ query: ME })
|
||||||
|
|
||||||
|
// required to redirect to /signup on page reload
|
||||||
|
// if we switched to anon and authentication is required
|
||||||
|
if (req.cookies['multi_auth.user-id'] === 'anonymous') {
|
||||||
|
me = null
|
||||||
|
}
|
||||||
|
|
||||||
if (authRequired && !me) {
|
if (authRequired && !me) {
|
||||||
let callback = process.env.NEXT_PUBLIC_URL + req.url
|
let callback = process.env.NEXT_PUBLIC_URL + req.url
|
||||||
|
|
|
@ -4,7 +4,7 @@ export default gql`
|
||||||
extend type Query {
|
extend type Query {
|
||||||
me: User
|
me: User
|
||||||
settings: User
|
settings: User
|
||||||
user(name: String!): User
|
user(id: ID, name: String): User
|
||||||
users: [User!]
|
users: [User!]
|
||||||
nameAvailable(name: String!): Boolean!
|
nameAvailable(name: String!): Boolean!
|
||||||
topUsers(cursor: String, when: String, from: String, to: String, by: String, limit: Limit): UsersNullable!
|
topUsers(cursor: String, when: String, from: String, to: String, by: String, limit: Limit): UsersNullable!
|
||||||
|
|
|
@ -0,0 +1,158 @@
|
||||||
|
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import cookie from 'cookie'
|
||||||
|
import { useMe } from '@/components/me'
|
||||||
|
import { USER_ID, SSR } from '@/lib/constants'
|
||||||
|
import { USER } from '@/fragments/users'
|
||||||
|
import { useQuery } from '@apollo/client'
|
||||||
|
import { UserListRow } from '@/components/user-list'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import AddIcon from '@/svgs/add-fill.svg'
|
||||||
|
|
||||||
|
const AccountContext = createContext()
|
||||||
|
|
||||||
|
const b64Decode = str => Buffer.from(str, 'base64').toString('utf-8')
|
||||||
|
const b64Encode = obj => Buffer.from(JSON.stringify(obj)).toString('base64')
|
||||||
|
|
||||||
|
const maybeSecureCookie = cookie => {
|
||||||
|
return window.location.protocol === 'https:' ? cookie + '; Secure' : cookie
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AccountProvider = ({ children }) => {
|
||||||
|
const { me } = useMe()
|
||||||
|
const [accounts, setAccounts] = useState([])
|
||||||
|
const [meAnon, setMeAnon] = useState(true)
|
||||||
|
|
||||||
|
const updateAccountsFromCookie = useCallback(() => {
|
||||||
|
try {
|
||||||
|
const { multi_auth: multiAuthCookie } = cookie.parse(document.cookie)
|
||||||
|
const accounts = multiAuthCookie
|
||||||
|
? JSON.parse(b64Decode(multiAuthCookie))
|
||||||
|
: me ? [{ id: Number(me.id), name: me.name, photoId: me.photoId }] : []
|
||||||
|
setAccounts(accounts)
|
||||||
|
// required for backwards compatibility: sync cookie with accounts if no multi auth cookie exists
|
||||||
|
// this is the case for sessions that existed before we deployed account switching
|
||||||
|
if (!multiAuthCookie && !!me) {
|
||||||
|
document.cookie = maybeSecureCookie(`multi_auth=${b64Encode(accounts)}; Path=/`)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('error parsing cookies:', err)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(updateAccountsFromCookie, [])
|
||||||
|
|
||||||
|
const addAccount = useCallback(user => {
|
||||||
|
setAccounts(accounts => [...accounts, user])
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const removeAccount = useCallback(userId => {
|
||||||
|
setAccounts(accounts => accounts.filter(({ id }) => id !== userId))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const multiAuthSignout = useCallback(async () => {
|
||||||
|
const { status } = await fetch('/api/signout', { credentials: 'include' })
|
||||||
|
// if status is 302, this means the server was able to switch us to the next available account
|
||||||
|
// and the current account was simply removed from the list of available accounts including the corresponding JWT.
|
||||||
|
const switchSuccess = status === 302
|
||||||
|
if (switchSuccess) updateAccountsFromCookie()
|
||||||
|
return switchSuccess
|
||||||
|
}, [updateAccountsFromCookie])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (SSR) return
|
||||||
|
const { 'multi_auth.user-id': multiAuthUserIdCookie } = cookie.parse(document.cookie)
|
||||||
|
setMeAnon(multiAuthUserIdCookie === 'anonymous')
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({ accounts, addAccount, removeAccount, meAnon, setMeAnon, multiAuthSignout }),
|
||||||
|
[accounts, addAccount, removeAccount, meAnon, setMeAnon, multiAuthSignout])
|
||||||
|
return <AccountContext.Provider value={value}>{children}</AccountContext.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAccounts = () => useContext(AccountContext)
|
||||||
|
|
||||||
|
const AccountListRow = ({ account, ...props }) => {
|
||||||
|
const { meAnon, setMeAnon } = useAccounts()
|
||||||
|
const { me, refreshMe } = useMe()
|
||||||
|
const anonRow = account.id === USER_ID.anon
|
||||||
|
const selected = (meAnon && anonRow) || Number(me?.id) === Number(account.id)
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// fetch updated names and photo ids since they might have changed since we were issued the JWTs
|
||||||
|
const [name, setName] = useState(account.name)
|
||||||
|
const [photoId, setPhotoId] = useState(account.photoId)
|
||||||
|
useQuery(USER,
|
||||||
|
{
|
||||||
|
variables: { id: account.id },
|
||||||
|
onCompleted ({ user: { name, photoId } }) {
|
||||||
|
if (photoId) setPhotoId(photoId)
|
||||||
|
if (name) setName(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const onClick = async (e) => {
|
||||||
|
// prevent navigation
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
// update pointer cookie
|
||||||
|
document.cookie = maybeSecureCookie(`multi_auth.user-id=${anonRow ? 'anonymous' : account.id}; Path=/`)
|
||||||
|
|
||||||
|
// update state
|
||||||
|
if (anonRow) {
|
||||||
|
// order is important to prevent flashes of no session
|
||||||
|
setMeAnon(true)
|
||||||
|
await refreshMe()
|
||||||
|
} else {
|
||||||
|
await refreshMe()
|
||||||
|
// order is important to prevent flashes of inconsistent data in switch account dialog
|
||||||
|
setMeAnon(account.id === USER_ID.anon)
|
||||||
|
}
|
||||||
|
|
||||||
|
// reload whatever page we're on to avoid any bugs due to missing authorization etc.
|
||||||
|
router.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='d-flex flex-row'>
|
||||||
|
<UserListRow
|
||||||
|
user={{ ...account, photoId, name }}
|
||||||
|
className='d-flex align-items-center me-2'
|
||||||
|
{...props}
|
||||||
|
onNymClick={onClick}
|
||||||
|
selected={selected}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SwitchAccountList () {
|
||||||
|
const { accounts } = useAccounts()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// can't show hat since the streak is not included in the JWT payload
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='my-2'>
|
||||||
|
<div className='d-flex flex-column flex-wrap mt-2 mb-3'>
|
||||||
|
<h4 className='text-muted'>Accounts</h4>
|
||||||
|
<AccountListRow account={{ id: USER_ID.anon, name: 'anon' }} showHat={false} />
|
||||||
|
{
|
||||||
|
accounts.map((account) => <AccountListRow key={account.id} account={account} showHat={false} />)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
href={{
|
||||||
|
pathname: '/login',
|
||||||
|
query: { callbackUrl: window.location.origin + router.asPath, multiAuth: true }
|
||||||
|
}}
|
||||||
|
className='text-reset fw-bold'
|
||||||
|
>
|
||||||
|
<AddIcon height={20} width={20} /> another account
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -27,7 +27,7 @@ const FormStatus = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AdvPostForm ({ children, item, storageKeyPrefix }) {
|
export default function AdvPostForm ({ children, item, storageKeyPrefix }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const { merge } = useFeeButton()
|
const { merge } = useFeeButton()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [itemType, setItemType] = useState()
|
const [itemType, setItemType] = useState()
|
||||||
|
|
|
@ -17,7 +17,7 @@ export function autowithdrawInitial ({ me }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AutowithdrawSettings ({ wallet }) {
|
export function AutowithdrawSettings ({ wallet }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const threshold = autoWithdrawThreshold({ me })
|
const threshold = autoWithdrawThreshold({ me })
|
||||||
|
|
||||||
const [sendThreshold, setSendThreshold] = useState(Math.max(Math.floor(threshold / 10), 1))
|
const [sendThreshold, setSendThreshold] = useState(Math.max(Math.floor(threshold / 10), 1))
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { BALANCE_LIMIT_MSATS } from '@/lib/constants'
|
||||||
import { msatsToSats, numWithUnits } from '@/lib/format'
|
import { msatsToSats, numWithUnits } from '@/lib/format'
|
||||||
|
|
||||||
export function WelcomeBanner ({ Banner }) {
|
export function WelcomeBanner ({ Banner }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const [hidden, setHidden] = useState(true)
|
const [hidden, setHidden] = useState(true)
|
||||||
const handleClose = async () => {
|
const handleClose = async () => {
|
||||||
|
@ -70,7 +70,7 @@ export function WelcomeBanner ({ Banner }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MadnessBanner ({ handleClose }) {
|
export function MadnessBanner ({ handleClose }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
return (
|
return (
|
||||||
<Alert className={styles.banner} key='info' variant='info' onClose={handleClose} dismissible>
|
<Alert className={styles.banner} key='info' variant='info' onClose={handleClose} dismissible>
|
||||||
<Alert.Heading>
|
<Alert.Heading>
|
||||||
|
@ -102,7 +102,7 @@ export function MadnessBanner ({ handleClose }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WalletLimitBanner () {
|
export function WalletLimitBanner () {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
|
|
||||||
const limitReached = me?.privates?.sats >= msatsToSats(BALANCE_LIMIT_MSATS)
|
const limitReached = me?.privates?.sats >= msatsToSats(BALANCE_LIMIT_MSATS)
|
||||||
if (!me || !limitReached) return
|
if (!me || !limitReached) return
|
||||||
|
|
|
@ -23,7 +23,7 @@ export function BountyForm ({
|
||||||
children
|
children
|
||||||
}) {
|
}) {
|
||||||
const client = useApolloClient()
|
const client = useApolloClient()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const schema = bountySchema({ client, me, existingBoost: item?.boost })
|
const schema = bountySchema({ client, me, existingBoost: item?.boost })
|
||||||
|
|
||||||
const onSubmit = useItemSubmit(UPSERT_BOUNTY, { item, sub })
|
const onSubmit = useItemSubmit(UPSERT_BOUNTY, { item, sub })
|
||||||
|
|
|
@ -96,7 +96,7 @@ export default function Comment ({
|
||||||
rootText, noComments, noReply, truncate, depth, pin
|
rootText, noComments, noReply, truncate, depth, pin
|
||||||
}) {
|
}) {
|
||||||
const [edit, setEdit] = useState()
|
const [edit, setEdit] = useState()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const isHiddenFreebie = me?.privates?.satsFilter !== 0 && !item.mine && item.freebie && !item.freedFreebie
|
const isHiddenFreebie = me?.privates?.satsFilter !== 0 && !item.mine && item.freebie && !item.freedFreebie
|
||||||
const [collapse, setCollapse] = useState(
|
const [collapse, setCollapse] = useState(
|
||||||
(isHiddenFreebie || item?.user?.meMute || (item?.outlawed && !me?.privates?.wildWestMode)) && !includeParent
|
(isHiddenFreebie || item?.user?.meMute || (item?.outlawed && !me?.privates?.wildWestMode)) && !includeParent
|
||||||
|
|
|
@ -22,7 +22,7 @@ export function DiscussionForm ({
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const client = useApolloClient()
|
const client = useApolloClient()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const onSubmit = useItemSubmit(UPSERT_DISCUSSION, { item, sub })
|
const onSubmit = useItemSubmit(UPSERT_DISCUSSION, { item, sub })
|
||||||
const schema = discussionSchema({ client, me, existingBoost: item?.boost })
|
const schema = discussionSchema({ client, me, existingBoost: item?.boost })
|
||||||
// if Web Share Target API was used
|
// if Web Share Target API was used
|
||||||
|
|
|
@ -64,7 +64,7 @@ export function postCommentUseRemoteLineItems ({ parentId } = {}) {
|
||||||
export function FeeButtonProvider ({ baseLineItems = {}, useRemoteLineItems = () => null, children }) {
|
export function FeeButtonProvider ({ baseLineItems = {}, useRemoteLineItems = () => null, children }) {
|
||||||
const [lineItems, setLineItems] = useState({})
|
const [lineItems, setLineItems] = useState({})
|
||||||
const [disabled, setDisabled] = useState(false)
|
const [disabled, setDisabled] = useState(false)
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
|
|
||||||
const remoteLineItems = useRemoteLineItems()
|
const remoteLineItems = useRemoteLineItems()
|
||||||
|
|
||||||
|
@ -115,7 +115,7 @@ function FreebieDialog () {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FeeButton ({ ChildButton = SubmitButton, variant, text, disabled }) {
|
export default function FeeButton ({ ChildButton = SubmitButton, variant, text, disabled }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const { lines, total, disabled: ctxDisabled, free } = useFeeButton()
|
const { lines, total, disabled: ctxDisabled, free } = useFeeButton()
|
||||||
const feeText = free
|
const feeText = free
|
||||||
? 'free'
|
? 'free'
|
||||||
|
|
|
@ -808,7 +808,7 @@ export function Form ({
|
||||||
}) {
|
}) {
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const initialErrorToasted = useRef(false)
|
const initialErrorToasted = useRef(false)
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialError && !initialErrorToasted.current) {
|
if (initialError && !initialErrorToasted.current) {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { abbrNum, numWithUnits } from '@/lib/format'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
|
|
||||||
export default function HiddenWalletSummary ({ abbreviate, fixedWidth }) {
|
export default function HiddenWalletSummary ({ abbreviate, fixedWidth }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const [hover, setHover] = useState(false)
|
const [hover, setHover] = useState(false)
|
||||||
|
|
||||||
const fixedWidthAbbrSats = useMemo(() => {
|
const fixedWidthAbbrSats = useMemo(() => {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { nextTip, defaultTipIncludingRandom } from './upvote'
|
||||||
import { ZAP_UNDO_DELAY_MS } from '@/lib/constants'
|
import { ZAP_UNDO_DELAY_MS } from '@/lib/constants'
|
||||||
import { usePaidMutation } from './use-paid-mutation'
|
import { usePaidMutation } from './use-paid-mutation'
|
||||||
import { ACT_MUTATION } from '@/fragments/paidAction'
|
import { ACT_MUTATION } from '@/fragments/paidAction'
|
||||||
|
import { meAnonSats } from '@/lib/apollo'
|
||||||
|
|
||||||
const defaultTips = [100, 1000, 10_000, 100_000]
|
const defaultTips = [100, 1000, 10_000, 100_000]
|
||||||
|
|
||||||
|
@ -43,14 +44,18 @@ const addCustomTip = (amount) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const setItemMeAnonSats = ({ id, amount }) => {
|
const setItemMeAnonSats = ({ id, amount }) => {
|
||||||
|
const reactiveVar = meAnonSats[id]
|
||||||
|
const existingAmount = reactiveVar()
|
||||||
|
reactiveVar(existingAmount + amount)
|
||||||
|
|
||||||
|
// save for next page load
|
||||||
const storageKey = `TIP-item:${id}`
|
const storageKey = `TIP-item:${id}`
|
||||||
const existingAmount = Number(window.localStorage.getItem(storageKey) || '0')
|
|
||||||
window.localStorage.setItem(storageKey, existingAmount + amount)
|
window.localStorage.setItem(storageKey, existingAmount + amount)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ItemAct ({ onClose, item, down, children, abortSignal }) {
|
export default function ItemAct ({ onClose, item, down, children, abortSignal }) {
|
||||||
const inputRef = useRef(null)
|
const inputRef = useRef(null)
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const [oValue, setOValue] = useState()
|
const [oValue, setOValue] = useState()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -203,7 +208,7 @@ export function useAct ({ query = ACT_MUTATION, ...options } = {}) {
|
||||||
|
|
||||||
export function useZap () {
|
export function useZap () {
|
||||||
const act = useAct()
|
const act = useAct()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const strike = useLightning()
|
const strike = useLightning()
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ import { useQuoteReply } from './use-quote-reply'
|
||||||
import { UNKNOWN_LINK_REL } from '@/lib/constants'
|
import { UNKNOWN_LINK_REL } from '@/lib/constants'
|
||||||
|
|
||||||
function BioItem ({ item, handleClick }) {
|
function BioItem ({ item, handleClick }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
if (!item.text) {
|
if (!item.text) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,13 +33,12 @@ export default function ItemInfo ({
|
||||||
onQuoteReply, extraBadges, nested, pinnable, showActionDropdown = true, showUser = true
|
onQuoteReply, extraBadges, nested, pinnable, showActionDropdown = true, showUser = true
|
||||||
}) {
|
}) {
|
||||||
const editThreshold = new Date(item.invoice?.confirmedAt ?? item.createdAt).getTime() + 10 * 60000
|
const editThreshold = new Date(item.invoice?.confirmedAt ?? item.createdAt).getTime() + 10 * 60000
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [canEdit, setCanEdit] =
|
const [canEdit, setCanEdit] =
|
||||||
useState(item.mine && (Date.now() < editThreshold))
|
useState(item.mine && (Date.now() < editThreshold))
|
||||||
const [hasNewComments, setHasNewComments] = useState(false)
|
const [hasNewComments, setHasNewComments] = useState(false)
|
||||||
const [meTotalSats, setMeTotalSats] = useState(0)
|
|
||||||
const root = useRoot()
|
const root = useRoot()
|
||||||
const retryCreateItem = useRetryCreateItem({ id: item.id })
|
const retryCreateItem = useRetryCreateItem({ id: item.id })
|
||||||
const sub = item?.sub || root?.sub
|
const sub = item?.sub || root?.sub
|
||||||
|
@ -54,10 +53,6 @@ export default function ItemInfo ({
|
||||||
setCanEdit(item.mine && (Date.now() < editThreshold))
|
setCanEdit(item.mine && (Date.now() < editThreshold))
|
||||||
}, [item.mine, editThreshold])
|
}, [item.mine, editThreshold])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (item) setMeTotalSats(item.meSats || item.meAnonSats || 0)
|
|
||||||
}, [item?.meSats, item?.meAnonSats])
|
|
||||||
|
|
||||||
// territory founders can pin any post in their territory
|
// territory founders can pin any post in their territory
|
||||||
// and OPs can pin any root reply in their post
|
// and OPs can pin any root reply in their post
|
||||||
const isPost = !item.parentId
|
const isPost = !item.parentId
|
||||||
|
@ -65,6 +60,7 @@ export default function ItemInfo ({
|
||||||
const myPost = (me && root && Number(me.id) === Number(root.user.id))
|
const myPost = (me && root && Number(me.id) === Number(root.user.id))
|
||||||
const rootReply = item.path.split('.').length === 2
|
const rootReply = item.path.split('.').length === 2
|
||||||
const canPin = (isPost && mySub) || (myPost && rootReply)
|
const canPin = (isPost && mySub) || (myPost && rootReply)
|
||||||
|
const meSats = (me ? item.meSats : item.meAnonSats) || 0
|
||||||
|
|
||||||
const EditInfo = () => {
|
const EditInfo = () => {
|
||||||
const waitForQrPayment = useQrPayment()
|
const waitForQrPayment = useQrPayment()
|
||||||
|
@ -131,7 +127,7 @@ export default function ItemInfo ({
|
||||||
unitPlural: 'stackers'
|
unitPlural: 'stackers'
|
||||||
})} ${item.mine
|
})} ${item.mine
|
||||||
? `\\ ${numWithUnits(item.meSats, { abbreviate: false })} to post`
|
? `\\ ${numWithUnits(item.meSats, { abbreviate: false })} to post`
|
||||||
: `(${numWithUnits(meTotalSats, { abbreviate: false })}${item.meDontLikeSats
|
: `(${numWithUnits(meSats, { abbreviate: false })}${item.meDontLikeSats
|
||||||
? ` & ${numWithUnits(item.meDontLikeSats, { abbreviate: false, unitSingular: 'downsat', unitPlural: 'downsats' })}`
|
? ` & ${numWithUnits(item.meDontLikeSats, { abbreviate: false, unitSingular: 'downsat', unitPlural: 'downsats' })}`
|
||||||
: ''} from me)`} `}
|
: ''} from me)`} `}
|
||||||
>
|
>
|
||||||
|
@ -229,7 +225,7 @@ export default function ItemInfo ({
|
||||||
<CrosspostDropdownItem item={item} />}
|
<CrosspostDropdownItem item={item} />}
|
||||||
{me && !item.position &&
|
{me && !item.position &&
|
||||||
!item.mine && !item.deletedAt &&
|
!item.mine && !item.deletedAt &&
|
||||||
(item.meDontLikeSats > meTotalSats
|
(item.meDontLikeSats > meSats
|
||||||
? <DropdownItemUpVote item={item} />
|
? <DropdownItemUpVote item={item} />
|
||||||
: <DontLikeThisDropdownItem item={item} />)}
|
: <DontLikeThisDropdownItem item={item} />)}
|
||||||
{me && sub && !item.mine && !item.outlawed && Number(me.id) === Number(sub.userId) && sub.moderated &&
|
{me && sub && !item.mine && !item.outlawed && Number(me.id) === Number(sub.userId) && sub.moderated &&
|
||||||
|
@ -273,7 +269,7 @@ export default function ItemInfo ({
|
||||||
}
|
}
|
||||||
|
|
||||||
function InfoDropdownItem ({ item }) {
|
function InfoDropdownItem ({ item }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const showModal = useShowModal()
|
const showModal = useShowModal()
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
|
|
|
@ -50,7 +50,7 @@ export function SearchTitle ({ title }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function mediaType ({ url, imgproxyUrls }) {
|
function mediaType ({ url, imgproxyUrls }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const src = IMGPROXY_URL_REGEXP.test(url) ? decodeProxyUrl(url) : url
|
const src = IMGPROXY_URL_REGEXP.test(url) ? decodeProxyUrl(url) : url
|
||||||
if (!imgproxyUrls?.[src] ||
|
if (!imgproxyUrls?.[src] ||
|
||||||
me?.privates?.showImagesAndVideos === false ||
|
me?.privates?.showImagesAndVideos === false ||
|
||||||
|
|
|
@ -11,7 +11,7 @@ import BackIcon from '@/svgs/arrow-left-line.svg'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { FAST_POLL_INTERVAL, SSR } from '@/lib/constants'
|
import { FAST_POLL_INTERVAL, SSR } from '@/lib/constants'
|
||||||
|
|
||||||
function QrAuth ({ k1, encodedUrl, callbackUrl }) {
|
function QrAuth ({ k1, encodedUrl, callbackUrl, multiAuth }) {
|
||||||
const query = gql`
|
const query = gql`
|
||||||
{
|
{
|
||||||
lnAuth(k1: "${k1}") {
|
lnAuth(k1: "${k1}") {
|
||||||
|
@ -23,7 +23,7 @@ function QrAuth ({ k1, encodedUrl, callbackUrl }) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.lnAuth?.pubkey) {
|
if (data?.lnAuth?.pubkey) {
|
||||||
signIn('lightning', { ...data.lnAuth, callbackUrl })
|
signIn('lightning', { ...data.lnAuth, callbackUrl, multiAuth })
|
||||||
}
|
}
|
||||||
}, [data?.lnAuth])
|
}, [data?.lnAuth])
|
||||||
|
|
||||||
|
@ -101,15 +101,15 @@ function LightningExplainer ({ text, children }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LightningAuthWithExplainer ({ text, callbackUrl }) {
|
export function LightningAuthWithExplainer ({ text, callbackUrl, multiAuth }) {
|
||||||
return (
|
return (
|
||||||
<LightningExplainer text={text}>
|
<LightningExplainer text={text}>
|
||||||
<LightningAuth callbackUrl={callbackUrl} />
|
<LightningAuth callbackUrl={callbackUrl} multiAuth={multiAuth} />
|
||||||
</LightningExplainer>
|
</LightningExplainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LightningAuth ({ callbackUrl }) {
|
export function LightningAuth ({ callbackUrl, multiAuth }) {
|
||||||
// query for challenge
|
// query for challenge
|
||||||
const [createAuth, { data, error }] = useMutation(gql`
|
const [createAuth, { data, error }] = useMutation(gql`
|
||||||
mutation createAuth {
|
mutation createAuth {
|
||||||
|
@ -125,5 +125,5 @@ export function LightningAuth ({ callbackUrl }) {
|
||||||
|
|
||||||
if (error) return <div>error</div>
|
if (error) return <div>error</div>
|
||||||
|
|
||||||
return data ? <QrAuth {...data.createAuth} callbackUrl={callbackUrl} /> : <QrSkeleton status='generating' />
|
return data ? <QrAuth {...data.createAuth} callbackUrl={callbackUrl} multiAuth={multiAuth} /> : <QrSkeleton status='generating' />
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ import useDebounceCallback from './use-debounce-callback'
|
||||||
export function LinkForm ({ item, sub, editThreshold, children }) {
|
export function LinkForm ({ item, sub, editThreshold, children }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const client = useApolloClient()
|
const client = useApolloClient()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const schema = linkSchema({ client, me, existingBoost: item?.boost })
|
const schema = linkSchema({ client, me, existingBoost: item?.boost })
|
||||||
// if Web Share Target API was used
|
// if Web Share Target API was used
|
||||||
const shareUrl = router.query.url
|
const shareUrl = router.query.url
|
||||||
|
|
|
@ -49,7 +49,7 @@ export const LoggerProvider = ({ children }) => {
|
||||||
const ServiceWorkerLoggerContext = createContext()
|
const ServiceWorkerLoggerContext = createContext()
|
||||||
|
|
||||||
function ServiceWorkerLoggerProvider ({ children }) {
|
function ServiceWorkerLoggerProvider ({ children }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const [name, setName] = useState()
|
const [name, setName] = useState()
|
||||||
const [os, setOS] = useState()
|
const [os, setOS] = useState()
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import LightningIcon from '@/svgs/bolt.svg'
|
||||||
import NostrIcon from '@/svgs/nostr.svg'
|
import NostrIcon from '@/svgs/nostr.svg'
|
||||||
import Button from 'react-bootstrap/Button'
|
import Button from 'react-bootstrap/Button'
|
||||||
|
|
||||||
export default function LoginButton ({ text, type, className, onClick }) {
|
export default function LoginButton ({ text, type, className, onClick, disabled }) {
|
||||||
let Icon, variant
|
let Icon, variant
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'twitter':
|
case 'twitter':
|
||||||
|
@ -29,7 +29,7 @@ export default function LoginButton ({ text, type, className, onClick }) {
|
||||||
const name = type.charAt(0).toUpperCase() + type.substr(1).toLowerCase()
|
const name = type.charAt(0).toUpperCase() + type.substr(1).toLowerCase()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button className={className} variant={variant} onClick={onClick}>
|
<Button className={className} variant={variant} onClick={onClick} disabled={disabled}>
|
||||||
<Icon
|
<Icon
|
||||||
width={20}
|
width={20}
|
||||||
height={20} className='me-3'
|
height={20} className='me-3'
|
||||||
|
|
|
@ -5,11 +5,14 @@ import { useState } from 'react'
|
||||||
import Alert from 'react-bootstrap/Alert'
|
import Alert from 'react-bootstrap/Alert'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { LightningAuthWithExplainer } from './lightning-auth'
|
import { LightningAuthWithExplainer } from './lightning-auth'
|
||||||
import NostrAuth from './nostr-auth'
|
import { NostrAuthWithExplainer } from './nostr-auth'
|
||||||
import LoginButton from './login-button'
|
import LoginButton from './login-button'
|
||||||
import { emailSchema } from '@/lib/validate'
|
import { emailSchema } from '@/lib/validate'
|
||||||
|
import { OverlayTrigger, Tooltip } from 'react-bootstrap'
|
||||||
|
|
||||||
|
export function EmailLoginForm ({ text, callbackUrl, multiAuth }) {
|
||||||
|
const disabled = multiAuth
|
||||||
|
|
||||||
export function EmailLoginForm ({ text, callbackUrl }) {
|
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
initial={{
|
initial={{
|
||||||
|
@ -17,7 +20,7 @@ export function EmailLoginForm ({ text, callbackUrl }) {
|
||||||
}}
|
}}
|
||||||
schema={emailSchema}
|
schema={emailSchema}
|
||||||
onSubmit={async ({ email }) => {
|
onSubmit={async ({ email }) => {
|
||||||
signIn('email', { email, callbackUrl })
|
signIn('email', { email, callbackUrl, multiAuth })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
|
@ -26,8 +29,9 @@ export function EmailLoginForm ({ text, callbackUrl }) {
|
||||||
placeholder='email@example.com'
|
placeholder='email@example.com'
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
<SubmitButton variant='secondary' className={styles.providerButton}>{text || 'Login'} with Email</SubmitButton>
|
<SubmitButton disabled={disabled} variant='secondary' className={styles.providerButton}>{text || 'Login'} with Email</SubmitButton>
|
||||||
</Form>
|
</Form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -48,16 +52,16 @@ export function authErrorMessage (error) {
|
||||||
return error && (authErrorMessages[error] ?? authErrorMessages.default)
|
return error && (authErrorMessages[error] ?? authErrorMessages.default)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Login ({ providers, callbackUrl, error, text, Header, Footer }) {
|
export default function Login ({ providers, callbackUrl, multiAuth, error, text, Header, Footer }) {
|
||||||
const [errorMessage, setErrorMessage] = useState(authErrorMessage(error))
|
const [errorMessage, setErrorMessage] = useState(authErrorMessage(error))
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
if (router.query.type === 'lightning') {
|
if (router.query.type === 'lightning') {
|
||||||
return <LightningAuthWithExplainer callbackUrl={callbackUrl} text={text} />
|
return <LightningAuthWithExplainer callbackUrl={callbackUrl} text={text} multiAuth={multiAuth} />
|
||||||
}
|
}
|
||||||
|
|
||||||
if (router.query.type === 'nostr') {
|
if (router.query.type === 'nostr') {
|
||||||
return <NostrAuth callbackUrl={callbackUrl} text={text} />
|
return <NostrAuthWithExplainer callbackUrl={callbackUrl} text={text} multiAuth={multiAuth} />
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -74,10 +78,16 @@ export default function Login ({ providers, callbackUrl, error, text, Header, Fo
|
||||||
switch (provider.name) {
|
switch (provider.name) {
|
||||||
case 'Email':
|
case 'Email':
|
||||||
return (
|
return (
|
||||||
<div className='w-100' key={provider.id}>
|
<OverlayTrigger
|
||||||
<div className='mt-2 text-center text-muted fw-bold'>or</div>
|
placement='bottom'
|
||||||
<EmailLoginForm text={text} callbackUrl={callbackUrl} />
|
overlay={multiAuth ? <Tooltip>not available for account switching yet</Tooltip> : <></>}
|
||||||
</div>
|
trigger={['hover', 'focus']}
|
||||||
|
>
|
||||||
|
<div className='w-100' key={provider.id}>
|
||||||
|
<div className='mt-2 text-center text-muted fw-bold'>or</div>
|
||||||
|
<EmailLoginForm text={text} callbackUrl={callbackUrl} multiAuth={multiAuth} />
|
||||||
|
</div>
|
||||||
|
</OverlayTrigger>
|
||||||
)
|
)
|
||||||
case 'Lightning':
|
case 'Lightning':
|
||||||
case 'Slashtags':
|
case 'Slashtags':
|
||||||
|
@ -99,13 +109,22 @@ export default function Login ({ providers, callbackUrl, error, text, Header, Fo
|
||||||
)
|
)
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<LoginButton
|
<OverlayTrigger
|
||||||
className={`mt-2 ${styles.providerButton}`}
|
placement='bottom'
|
||||||
key={provider.id}
|
overlay={multiAuth ? <Tooltip>not available for account switching yet</Tooltip> : <></>}
|
||||||
type={provider.id.toLowerCase()}
|
trigger={['hover', 'focus']}
|
||||||
onClick={() => signIn(provider.id, { callbackUrl })}
|
>
|
||||||
text={`${text || 'Login'} with`}
|
<div className='w-100'>
|
||||||
/>
|
<LoginButton
|
||||||
|
className={`mt-2 ${styles.providerButton}`}
|
||||||
|
key={provider.id}
|
||||||
|
type={provider.id.toLowerCase()}
|
||||||
|
onClick={() => signIn(provider.id, { callbackUrl, multiAuth })}
|
||||||
|
text={`${text || 'Login'} with`}
|
||||||
|
disabled={multiAuth}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</OverlayTrigger>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -8,10 +8,14 @@ export const MeContext = React.createContext({
|
||||||
})
|
})
|
||||||
|
|
||||||
export function MeProvider ({ me, children }) {
|
export function MeProvider ({ me, children }) {
|
||||||
const { data } = useQuery(ME, SSR ? {} : { pollInterval: FAST_POLL_INTERVAL, nextFetchPolicy: 'cache-and-network' })
|
const { data, refetch } = useQuery(ME, SSR ? {} : { pollInterval: FAST_POLL_INTERVAL, nextFetchPolicy: 'cache-and-network' })
|
||||||
|
// this makes sure that we always use the fetched data if it's null.
|
||||||
|
// without this, we would always fallback to the `me` object
|
||||||
|
// which was passed during page load which (visually) breaks switching to anon
|
||||||
|
const futureMe = data?.me ?? (data?.me === null ? null : me)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MeContext.Provider value={data?.me || me}>
|
<MeContext.Provider value={{ me: futureMe, refreshMe: refetch }}>
|
||||||
{children}
|
{children}
|
||||||
</MeContext.Provider>
|
</MeContext.Provider>
|
||||||
)
|
)
|
||||||
|
|
|
@ -109,7 +109,7 @@ export default function MediaOrLink ({ linkFallback = true, ...props }) {
|
||||||
|
|
||||||
// determines how the media should be displayed given the params, me settings, and editor tab
|
// determines how the media should be displayed given the params, me settings, and editor tab
|
||||||
export const useMediaHelper = ({ src, srcSet: srcSetIntital, topLevel, tab }) => {
|
export const useMediaHelper = ({ src, srcSet: srcSetIntital, topLevel, tab }) => {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const trusted = useMemo(() => !!srcSetIntital || IMGPROXY_URL_REGEXP.test(src) || MEDIA_DOMAIN_REGEXP.test(src), [!!srcSetIntital, src])
|
const trusted = useMemo(() => !!srcSetIntital || IMGPROXY_URL_REGEXP.test(src) || MEDIA_DOMAIN_REGEXP.test(src), [!!srcSetIntital, src])
|
||||||
const { dimensions, video, format, ...srcSetObj } = srcSetIntital || {}
|
const { dimensions, video, format, ...srcSetObj } = srcSetIntital || {}
|
||||||
const [isImage, setIsImage] = useState(!video && trusted)
|
const [isImage, setIsImage] = useState(!video && trusted)
|
||||||
|
|
|
@ -23,6 +23,8 @@ import classNames from 'classnames'
|
||||||
import SnIcon from '@/svgs/sn.svg'
|
import SnIcon from '@/svgs/sn.svg'
|
||||||
import { useHasNewNotes } from '../use-has-new-notes'
|
import { useHasNewNotes } from '../use-has-new-notes'
|
||||||
import { useWallets } from 'wallets'
|
import { useWallets } from 'wallets'
|
||||||
|
import SwitchAccountList, { useAccounts } from '@/components/account'
|
||||||
|
import { useShowModal } from '@/components/modal'
|
||||||
|
|
||||||
export function Brand ({ className }) {
|
export function Brand ({ className }) {
|
||||||
return (
|
return (
|
||||||
|
@ -137,7 +139,7 @@ export function NavNotifications ({ className }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WalletSummary () {
|
export function WalletSummary () {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
if (!me) return null
|
if (!me) return null
|
||||||
if (me.privates?.hideWalletBalance) {
|
if (me.privates?.hideWalletBalance) {
|
||||||
return <HiddenWalletSummary abbreviate fixedWidth />
|
return <HiddenWalletSummary abbreviate fixedWidth />
|
||||||
|
@ -146,7 +148,7 @@ export function WalletSummary () {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NavWalletSummary ({ className }) {
|
export function NavWalletSummary ({ className }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const walletLimitReached = me?.privates?.sats >= msatsToSats(BALANCE_LIMIT_MSATS)
|
const walletLimitReached = me?.privates?.sats >= msatsToSats(BALANCE_LIMIT_MSATS)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -211,7 +213,7 @@ export function MeDropdown ({ me, dropNavKey }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SignUpButton ({ className = 'py-0' }) {
|
export function SignUpButton ({ className = 'py-0', width }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const handleLogin = useCallback(async pathname => await router.push({
|
const handleLogin = useCallback(async pathname => await router.push({
|
||||||
pathname,
|
pathname,
|
||||||
|
@ -220,8 +222,8 @@ export function SignUpButton ({ className = 'py-0' }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className={classNames('align-items-center ps-2 pe-3', className)}
|
className={classNames('align-items-center ps-2 py-1 pe-3', className)}
|
||||||
style={{ borderWidth: '2px', width: '112px' }}
|
style={{ borderWidth: '2px', width: width || '150px' }}
|
||||||
id='signup'
|
id='signup'
|
||||||
onClick={() => handleLogin('/signup')}
|
onClick={() => handleLogin('/signup')}
|
||||||
>
|
>
|
||||||
|
@ -234,7 +236,7 @@ export function SignUpButton ({ className = 'py-0' }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LoginButton ({ className }) {
|
export default function LoginButton () {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const handleLogin = useCallback(async pathname => await router.push({
|
const handleLogin = useCallback(async pathname => await router.push({
|
||||||
pathname,
|
pathname,
|
||||||
|
@ -243,9 +245,9 @@ export default function LoginButton ({ className }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className='align-items-center px-3 py-1 mb-2'
|
className='align-items-center px-3 py-1'
|
||||||
id='login'
|
id='login'
|
||||||
style={{ borderWidth: '2px', width: '112px' }}
|
style={{ borderWidth: '2px', width: '150px' }}
|
||||||
variant='outline-grey-darkmode'
|
variant='outline-grey-darkmode'
|
||||||
onClick={() => handleLogin('/login')}
|
onClick={() => handleLogin('/login')}
|
||||||
>
|
>
|
||||||
|
@ -254,32 +256,105 @@ export default function LoginButton ({ className }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LogoutDropdownItem () {
|
function LogoutObstacle ({ onClose }) {
|
||||||
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
|
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
|
||||||
const wallets = useWallets()
|
const wallets = useWallets()
|
||||||
|
const { multiAuthSignout } = useAccounts()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown.Item
|
<div className='d-flex m-auto flex-column w-fit-content'>
|
||||||
onClick={async () => {
|
<h4 className='mb-3'>I reckon you want to logout?</h4>
|
||||||
// order is important because we need to be logged in to delete push subscription on server
|
<div className='mt-2 d-flex justify-content-between'>
|
||||||
const pushSubscription = await swRegistration?.pushManager.getSubscription()
|
<Button
|
||||||
if (pushSubscription) {
|
className='me-2'
|
||||||
await togglePushSubscription().catch(console.error)
|
variant='grey-medium'
|
||||||
}
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
const switchSuccess = await multiAuthSignout()
|
||||||
|
// only signout if multiAuth did not find a next available account
|
||||||
|
if (switchSuccess) {
|
||||||
|
onClose()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
await wallets.resetClient().catch(console.error)
|
// order is important because we need to be logged in to delete push subscription on server
|
||||||
|
const pushSubscription = await swRegistration?.pushManager.getSubscription()
|
||||||
|
if (pushSubscription) {
|
||||||
|
await togglePushSubscription().catch(console.error)
|
||||||
|
}
|
||||||
|
|
||||||
await signOut({ callbackUrl: '/' })
|
await wallets.resetClient().catch(console.error)
|
||||||
}}
|
|
||||||
>logout
|
await signOut({ callbackUrl: '/' })
|
||||||
</Dropdown.Item>
|
}}
|
||||||
|
>
|
||||||
|
logout
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LoginButtons () {
|
export function LogoutDropdownItem ({ handleClose }) {
|
||||||
|
const showModal = useShowModal()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LoginButton />
|
<Dropdown.Item onClick={() => {
|
||||||
<SignUpButton className='py-1' />
|
handleClose?.()
|
||||||
|
showModal(onClose => <SwitchAccountList onClose={onClose} />)
|
||||||
|
}}
|
||||||
|
>switch account
|
||||||
|
</Dropdown.Item>
|
||||||
|
<Dropdown.Item
|
||||||
|
onClick={async () => {
|
||||||
|
showModal(onClose => (<LogoutObstacle onClose={onClose} />))
|
||||||
|
}}
|
||||||
|
>logout
|
||||||
|
</Dropdown.Item>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SwitchAccountButton ({ handleClose }) {
|
||||||
|
const showModal = useShowModal()
|
||||||
|
const { accounts } = useAccounts()
|
||||||
|
|
||||||
|
if (accounts.length === 0) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className='align-items-center px-3 py-1'
|
||||||
|
variant='outline-grey-darkmode'
|
||||||
|
style={{ borderWidth: '2px', width: '150px' }}
|
||||||
|
onClick={() => {
|
||||||
|
// login buttons rendered in offcanvas aren't wrapped inside <Dropdown>
|
||||||
|
// so we manually close the offcanvas in that case by passing down handleClose here
|
||||||
|
handleClose?.()
|
||||||
|
showModal(onClose => <SwitchAccountList onClose={onClose} />)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
switch account
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoginButtons ({ handleClose }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Dropdown.Item className='py-1'>
|
||||||
|
<LoginButton />
|
||||||
|
</Dropdown.Item>
|
||||||
|
<Dropdown.Item className='py-1'>
|
||||||
|
<SignUpButton />
|
||||||
|
</Dropdown.Item>
|
||||||
|
<Dropdown.Item className='py-1'>
|
||||||
|
<SwitchAccountButton handleClose={handleClose} />
|
||||||
|
</Dropdown.Item>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -299,7 +374,7 @@ export function AnonDropdown ({ path }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='position-relative'>
|
<div className='position-relative'>
|
||||||
<Dropdown className={styles.dropdown} align='end'>
|
<Dropdown className={styles.dropdown} align='end' autoClose>
|
||||||
<Dropdown.Toggle className='nav-link nav-item' id='profile' variant='custom'>
|
<Dropdown.Toggle className='nav-link nav-item' id='profile' variant='custom'>
|
||||||
<Nav.Link eventKey='anon' as='span' className='p-0 fw-normal'>
|
<Nav.Link eventKey='anon' as='span' className='p-0 fw-normal'>
|
||||||
@anon<Hat user={{ id: USER_ID.anon }} />
|
@anon<Hat user={{ id: USER_ID.anon }} />
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { AnonCorner, Back, Brand, MeCorner, NavPrice, SearchItem } from '../comm
|
||||||
import { useMe } from '../../me'
|
import { useMe } from '../../me'
|
||||||
|
|
||||||
export default function TopBar ({ prefix, sub, path, topNavKey, dropNavKey }) {
|
export default function TopBar ({ prefix, sub, path, topNavKey, dropNavKey }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
return (
|
return (
|
||||||
<Navbar>
|
<Navbar>
|
||||||
<Nav
|
<Nav
|
||||||
|
|
|
@ -30,7 +30,7 @@ function useDetectKeyboardOpen (minKeyboardHeight = 300, defaultValue) {
|
||||||
|
|
||||||
export default function BottomBar ({ sub }) {
|
export default function BottomBar ({ sub }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const isKeyboardOpen = useDetectKeyboardOpen(200, false)
|
const isKeyboardOpen = useDetectKeyboardOpen(200, false)
|
||||||
|
|
||||||
if (isKeyboardOpen) {
|
if (isKeyboardOpen) {
|
||||||
|
|
|
@ -75,10 +75,10 @@ export default function OffCanvas ({ me, dropNavKey }) {
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<Dropdown.Divider />
|
<Dropdown.Divider />
|
||||||
<LogoutDropdownItem />
|
<LogoutDropdownItem handleClose={handleClose} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
: <LoginButtons />}
|
: <LoginButtons handleClose={handleClose} />}
|
||||||
<div className={classNames(styles.footerPadding, 'mt-auto')}>
|
<div className={classNames(styles.footerPadding, 'mt-auto')}>
|
||||||
<Navbar className={classNames('container d-flex flex-row px-0 text-muted')}>
|
<Navbar className={classNames('container d-flex flex-row px-0 text-muted')}>
|
||||||
<Nav>
|
<Nav>
|
||||||
|
|
|
@ -4,7 +4,7 @@ import styles from '../../header.module.css'
|
||||||
import { useMe } from '@/components/me'
|
import { useMe } from '@/components/me'
|
||||||
|
|
||||||
export default function SecondBar (props) {
|
export default function SecondBar (props) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const { topNavKey } = props
|
const { topNavKey } = props
|
||||||
if (!hasNavSelect(props)) return null
|
if (!hasNavSelect(props)) return null
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Back, NavPrice, NavSelect, NavWalletSummary, SignUpButton, hasNavSelect
|
||||||
import { useMe } from '@/components/me'
|
import { useMe } from '@/components/me'
|
||||||
|
|
||||||
export default function TopBar ({ prefix, sub, path, pathname, topNavKey, dropNavKey }) {
|
export default function TopBar ({ prefix, sub, path, pathname, topNavKey, dropNavKey }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
return (
|
return (
|
||||||
<Navbar>
|
<Navbar>
|
||||||
<Nav
|
<Nav
|
||||||
|
@ -17,7 +17,7 @@ export default function TopBar ({ prefix, sub, path, pathname, topNavKey, dropNa
|
||||||
: (
|
: (
|
||||||
<>
|
<>
|
||||||
<NavPrice className='flex-shrink-1' />
|
<NavPrice className='flex-shrink-1' />
|
||||||
{me ? <NavWalletSummary /> : <SignUpButton />}
|
{me ? <NavWalletSummary /> : <SignUpButton width='fit-content' />}
|
||||||
</>)}
|
</>)}
|
||||||
</Nav>
|
</Nav>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
|
|
|
@ -7,7 +7,7 @@ import classNames from 'classnames'
|
||||||
|
|
||||||
export default function StickyBar ({ prefix, sub, path, topNavKey, dropNavKey }) {
|
export default function StickyBar ({ prefix, sub, path, topNavKey, dropNavKey }) {
|
||||||
const ref = useRef()
|
const ref = useRef()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const stick = () => {
|
const stick = () => {
|
||||||
|
@ -49,7 +49,7 @@ export default function StickyBar ({ prefix, sub, path, topNavKey, dropNavKey })
|
||||||
>
|
>
|
||||||
<Back />
|
<Back />
|
||||||
<NavPrice className='flex-shrink-1 flex-grow-0' />
|
<NavPrice className='flex-shrink-1 flex-grow-0' />
|
||||||
{me ? <NavWalletSummary className='px-2' /> : <SignUpButton />}
|
{me ? <NavWalletSummary className='px-2' /> : <SignUpButton width='fit-content' />}
|
||||||
</Nav>
|
</Nav>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
|
@ -64,7 +64,7 @@ function NostrExplainer ({ text }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NostrAuth ({ text, callbackUrl }) {
|
export function NostrAuth ({ text, callbackUrl, multiAuth }) {
|
||||||
const [createAuth, { data, error }] = useMutation(gql`
|
const [createAuth, { data, error }] = useMutation(gql`
|
||||||
mutation createAuth {
|
mutation createAuth {
|
||||||
createAuth {
|
createAuth {
|
||||||
|
@ -112,7 +112,8 @@ export function NostrAuth ({ text, callbackUrl }) {
|
||||||
try {
|
try {
|
||||||
await signIn('nostr', {
|
await signIn('nostr', {
|
||||||
event: JSON.stringify(event),
|
event: JSON.stringify(event),
|
||||||
callbackUrl
|
callbackUrl,
|
||||||
|
multiAuth
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error('authorization failed', e)
|
throw new Error('authorization failed', e)
|
||||||
|
@ -141,14 +142,14 @@ export function NostrAuth ({ text, callbackUrl }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NostrAuthWithExplainer ({ text, callbackUrl }) {
|
export function NostrAuthWithExplainer ({ text, callbackUrl, multiAuth }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<div className={styles.login}>
|
<div className={styles.login}>
|
||||||
<div className='w-100 mb-3 text-muted pointer' onClick={() => router.back()}><BackIcon /></div>
|
<div className='w-100 mb-3 text-muted pointer' onClick={() => router.back()}><BackIcon /></div>
|
||||||
<h3 className='w-100 pb-2'>{text || 'Login'} with Nostr</h3>
|
<h3 className='w-100 pb-2'>{text || 'Login'} with Nostr</h3>
|
||||||
<NostrAuth text={text} callbackUrl={callbackUrl} />
|
<NostrAuth text={text} callbackUrl={callbackUrl} multiAuth={multiAuth} />
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
|
|
|
@ -42,7 +42,7 @@ export const payBountyCacheMods = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PayBounty ({ children, item }) {
|
export default function PayBounty ({ children, item }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const showModal = useShowModal()
|
const showModal = useShowModal()
|
||||||
const root = useRoot()
|
const root = useRoot()
|
||||||
const strike = useLightning()
|
const strike = useLightning()
|
||||||
|
|
|
@ -201,7 +201,7 @@ export const useQrPayment = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const usePayment = () => {
|
export const usePayment = () => {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const feeButton = useFeeButton()
|
const feeButton = useFeeButton()
|
||||||
const invoice = useInvoice()
|
const invoice = useInvoice()
|
||||||
const waitForWalletPayment = useWalletPayment()
|
const waitForWalletPayment = useWalletPayment()
|
||||||
|
|
|
@ -14,7 +14,7 @@ import useItemSubmit from './use-item-submit'
|
||||||
|
|
||||||
export function PollForm ({ item, sub, editThreshold, children }) {
|
export function PollForm ({ item, sub, editThreshold, children }) {
|
||||||
const client = useApolloClient()
|
const client = useApolloClient()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const schema = pollSchema({ client, me, existingBoost: item?.boost })
|
const schema = pollSchema({ client, me, existingBoost: item?.boost })
|
||||||
|
|
||||||
const onSubmit = useItemSubmit(UPSERT_POLL, { item, sub })
|
const onSubmit = useItemSubmit(UPSERT_POLL, { item, sub })
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { usePaidMutation } from './use-paid-mutation'
|
||||||
import { POLL_VOTE, RETRY_PAID_ACTION } from '@/fragments/paidAction'
|
import { POLL_VOTE, RETRY_PAID_ACTION } from '@/fragments/paidAction'
|
||||||
|
|
||||||
export default function Poll ({ item }) {
|
export default function Poll ({ item }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const pollVote = usePollVote({ query: POLL_VOTE, itemId: item.id })
|
const pollVote = usePollVote({ query: POLL_VOTE, itemId: item.id })
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ import CancelButton from './cancel-button'
|
||||||
import { TerritoryInfo } from './territory-header'
|
import { TerritoryInfo } from './territory-header'
|
||||||
|
|
||||||
export function PostForm ({ type, sub, children }) {
|
export function PostForm ({ type, sub, children }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const [errorMessage, setErrorMessage] = useState()
|
const [errorMessage, setErrorMessage] = useState()
|
||||||
|
|
||||||
const prefix = sub?.name ? `/~${sub.name}` : ''
|
const prefix = sub?.name ? `/~${sub.name}` : ''
|
||||||
|
@ -150,7 +150,7 @@ export function PostForm ({ type, sub, children }) {
|
||||||
return (
|
return (
|
||||||
<FeeButtonProvider
|
<FeeButtonProvider
|
||||||
baseLineItems={sub ? postCommentBaseLineItems({ baseCost: sub.baseCost, me: !!me }) : undefined}
|
baseLineItems={sub ? postCommentBaseLineItems({ baseCost: sub.baseCost, me: !!me }) : undefined}
|
||||||
useRemoteLineItems={postCommentUseRemoteLineItems({ me: !!me })}
|
useRemoteLineItems={postCommentUseRemoteLineItems()}
|
||||||
>
|
>
|
||||||
<FormType sub={sub}>{children}</FormType>
|
<FormType sub={sub}>{children}</FormType>
|
||||||
</FeeButtonProvider>
|
</FeeButtonProvider>
|
||||||
|
|
|
@ -19,7 +19,7 @@ export function usePrice () {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PriceProvider ({ price, children }) {
|
export function PriceProvider ({ price, children }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const fiatCurrency = me?.privates?.fiatCurrency
|
const fiatCurrency = me?.privates?.fiatCurrency
|
||||||
const { data } = useQuery(PRICE, {
|
const { data } = useQuery(PRICE, {
|
||||||
variables: { fiatCurrency },
|
variables: { fiatCurrency },
|
||||||
|
|
|
@ -40,7 +40,7 @@ export default forwardRef(function Reply ({
|
||||||
quote
|
quote
|
||||||
}, ref) {
|
}, ref) {
|
||||||
const [reply, setReply] = useState(replyOpen || quote)
|
const [reply, setReply] = useState(replyOpen || quote)
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const parentId = item.id
|
const parentId = item.id
|
||||||
const replyInput = useRef(null)
|
const replyInput = useRef(null)
|
||||||
const showModal = useShowModal()
|
const showModal = useShowModal()
|
||||||
|
|
|
@ -11,7 +11,7 @@ export default function Search ({ sub }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [q, setQ] = useState(router.query.q || '')
|
const [q, setQ] = useState(router.query.q || '')
|
||||||
const inputRef = useRef(null)
|
const inputRef = useRef(null)
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
inputRef.current?.focus()
|
inputRef.current?.focus()
|
||||||
|
|
|
@ -38,7 +38,7 @@ async function share (title, url, toaster) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Share ({ path, title = '', className = '' }) {
|
export default function Share ({ path, title = '', className = '' }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const url = referrurl(path, me)
|
const url = referrurl(path, me)
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ export default function Share ({ path, title = '', className = '' }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CopyLinkDropdownItem ({ item }) {
|
export function CopyLinkDropdownItem ({ item }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
let url = referrurl(`/items/${item.id}`, me)
|
let url = referrurl(`/items/${item.id}`, me)
|
||||||
|
|
|
@ -18,7 +18,7 @@ import { UNARCHIVE_TERRITORY, UPSERT_SUB } from '@/fragments/paidAction'
|
||||||
export default function TerritoryForm ({ sub }) {
|
export default function TerritoryForm ({ sub }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const client = useApolloClient()
|
const client = useApolloClient()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const [upsertSub] = usePaidMutation(UPSERT_SUB)
|
const [upsertSub] = usePaidMutation(UPSERT_SUB)
|
||||||
const [unarchiveTerritory] = usePaidMutation(UNARCHIVE_TERRITORY)
|
const [unarchiveTerritory] = usePaidMutation(UNARCHIVE_TERRITORY)
|
||||||
|
|
||||||
|
|
|
@ -57,7 +57,7 @@ export function TerritoryInfo ({ sub }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TerritoryHeader ({ sub }) {
|
export default function TerritoryHeader ({ sub }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
|
|
||||||
const [toggleMuteSub] = useMutation(
|
const [toggleMuteSub] = useMutation(
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { usePaidMutation } from './use-paid-mutation'
|
||||||
import { SUB_PAY } from '@/fragments/paidAction'
|
import { SUB_PAY } from '@/fragments/paidAction'
|
||||||
|
|
||||||
export default function TerritoryPaymentDue ({ sub }) {
|
export default function TerritoryPaymentDue ({ sub }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const client = useApolloClient()
|
const client = useApolloClient()
|
||||||
const [paySub] = usePaidMutation(SUB_PAY)
|
const [paySub] = usePaidMutation(SUB_PAY)
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ export default function TerritoryPaymentDue ({ sub }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TerritoryBillingLine ({ sub }) {
|
export function TerritoryBillingLine ({ sub }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
if (!sub || sub.userId !== Number(me?.id)) return null
|
if (!sub || sub.userId !== Number(me?.id)) return null
|
||||||
|
|
||||||
const dueDate = sub.billPaidUntil && new Date(sub.billPaidUntil)
|
const dueDate = sub.billPaidUntil && new Date(sub.billPaidUntil)
|
||||||
|
|
|
@ -57,7 +57,7 @@ function TransferObstacle ({ sub, onClose, userName }) {
|
||||||
function TerritoryTransferForm ({ sub, onClose }) {
|
function TerritoryTransferForm ({ sub, onClose }) {
|
||||||
const showModal = useShowModal()
|
const showModal = useShowModal()
|
||||||
const client = useApolloClient()
|
const client = useApolloClient()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const schema = territoryTransferSchema({ me, client })
|
const schema = territoryTransferSchema({ me, client })
|
||||||
|
|
||||||
const onSubmit = useCallback(async (values) => {
|
const onSubmit = useCallback(async (values) => {
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { numWithUnits } from '@/lib/format'
|
||||||
import { Dropdown } from 'react-bootstrap'
|
import { Dropdown } from 'react-bootstrap'
|
||||||
|
|
||||||
const UpvotePopover = ({ target, show, handleClose }) => {
|
const UpvotePopover = ({ target, show, handleClose }) => {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
return (
|
return (
|
||||||
<Overlay
|
<Overlay
|
||||||
show={show}
|
show={show}
|
||||||
|
@ -107,7 +107,7 @@ export default function UpVote ({ item, className }) {
|
||||||
const [voteShow, _setVoteShow] = useState(false)
|
const [voteShow, _setVoteShow] = useState(false)
|
||||||
const [tipShow, _setTipShow] = useState(false)
|
const [tipShow, _setTipShow] = useState(false)
|
||||||
const ref = useRef()
|
const ref = useRef()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const [hover, setHover] = useState(false)
|
const [hover, setHover] = useState(false)
|
||||||
const [setWalkthrough] = useMutation(
|
const [setWalkthrough] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
|
@ -153,7 +153,7 @@ export default function UpVote ({ item, className }) {
|
||||||
[item?.mine, item?.meForward, item?.deletedAt])
|
[item?.mine, item?.meForward, item?.deletedAt])
|
||||||
|
|
||||||
const [meSats, overlayText, color, nextColor] = useMemo(() => {
|
const [meSats, overlayText, color, nextColor] = useMemo(() => {
|
||||||
const meSats = (item?.meSats || item?.meAnonSats || 0)
|
const meSats = (me ? item?.meSats : item?.meAnonSats) || 0
|
||||||
|
|
||||||
// what should our next tip be?
|
// what should our next tip be?
|
||||||
const sats = pending || nextTip(meSats, { ...me?.privates })
|
const sats = pending || nextTip(meSats, { ...me?.privates })
|
||||||
|
@ -168,7 +168,7 @@ export default function UpVote ({ item, className }) {
|
||||||
meSats, overlayTextContent,
|
meSats, overlayTextContent,
|
||||||
getColor(meSats), getColor(meSats + sats)]
|
getColor(meSats), getColor(meSats + sats)]
|
||||||
}, [
|
}, [
|
||||||
item?.meSats, item?.meAnonSats, me?.privates?.tipDefault, me?.privates?.turboDefault,
|
me, item?.meSats, item?.meAnonSats, me?.privates?.tipDefault, me?.privates?.turboDefault,
|
||||||
me?.privates?.tipRandom, me?.privates?.tipRandomMin, me?.privates?.tipRandomMax, pending])
|
me?.privates?.tipRandom, me?.privates?.tipRandomMin, me?.privates?.tipRandomMax, pending])
|
||||||
|
|
||||||
const handleModalClosed = () => {
|
const handleModalClosed = () => {
|
||||||
|
|
|
@ -175,7 +175,7 @@ function NymEdit ({ user, setEditting }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function NymView ({ user, isMe, setEditting }) {
|
function NymView ({ user, isMe, setEditting }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
return (
|
return (
|
||||||
<div className='d-flex align-items-center mb-2'>
|
<div className='d-flex align-items-center mb-2'>
|
||||||
<div className={styles.username}>@{user.name}<Hat className='' user={user} badge /></div>
|
<div className={styles.username}>@{user.name}<Hat className='' user={user} badge /></div>
|
||||||
|
@ -237,7 +237,7 @@ function SocialLink ({ name, id }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function HeaderHeader ({ user }) {
|
function HeaderHeader ({ user }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
|
|
||||||
const showModal = useShowModal()
|
const showModal = useShowModal()
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { useMe } from './me'
|
||||||
import { MEDIA_URL } from '@/lib/constants'
|
import { MEDIA_URL } from '@/lib/constants'
|
||||||
import { NymActionDropdown } from '@/components/user-header'
|
import { NymActionDropdown } from '@/components/user-header'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
import CheckCircle from '@/svgs/checkbox-circle-fill.svg'
|
||||||
|
|
||||||
// all of this nonsense is to show the stat we are sorting by first
|
// all of this nonsense is to show the stat we are sorting by first
|
||||||
const Stacked = ({ user }) => (user.optional.stacked !== null && <span>{abbrNum(user.optional.stacked)} stacked</span>)
|
const Stacked = ({ user }) => (user.optional.stacked !== null && <span>{abbrNum(user.optional.stacked)} stacked</span>)
|
||||||
|
@ -40,6 +41,34 @@ function seperate (arr, seperator) {
|
||||||
return arr.flatMap((x, i) => i < arr.length - 1 ? [x, seperator] : [x])
|
return arr.flatMap((x, i) => i < arr.length - 1 ? [x, seperator] : [x])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function UserListRow ({ user, stats, className, onNymClick, showHat = true, selected }) {
|
||||||
|
return (
|
||||||
|
<div className={`${styles.item} mb-2`} key={user.name}>
|
||||||
|
<Link href={`/${user.name}`}>
|
||||||
|
<Image
|
||||||
|
src={user.photoId ? `${MEDIA_URL}/${user.photoId}` : '/dorian400.jpg'} width='32' height='32'
|
||||||
|
className={`${userStyles.userimg} me-2`}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
<div className={`${styles.hunk} ${className}`}>
|
||||||
|
<Link
|
||||||
|
href={`/${user.name}`}
|
||||||
|
className={`d-inline-flex align-items-center text-reset ${selected ? 'fw-bold text-underline' : 'text-muted'}`}
|
||||||
|
style={{ textUnderlineOffset: '0.25em' }}
|
||||||
|
onClick={onNymClick}
|
||||||
|
>
|
||||||
|
@{user.name}{showHat && <Hat className='ms-1 fill-grey' height={14} width={14} user={user} />}{selected && <CheckCircle className='ms-3 fill-primary' height={14} width={14} />}
|
||||||
|
</Link>
|
||||||
|
{stats && (
|
||||||
|
<div className={styles.other}>
|
||||||
|
{stats.map((Comp, i) => <Comp key={i} user={user} />)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function UserBase ({ user, className, children, nymActionDropdown }) {
|
export function UserBase ({ user, className, children, nymActionDropdown }) {
|
||||||
return (
|
return (
|
||||||
<div className={classNames(styles.item, className)}>
|
<div className={classNames(styles.item, className)}>
|
||||||
|
@ -63,7 +92,7 @@ export function UserBase ({ user, className, children, nymActionDropdown }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function User ({ user, rank, statComps, className = 'mb-2', Embellish, nymActionDropdown = false }) {
|
export function User ({ user, rank, statComps, className = 'mb-2', Embellish, nymActionDropdown = false }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const showStatComps = statComps && statComps.length > 0
|
const showStatComps = statComps && statComps.length > 0
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -112,7 +112,7 @@ const initIndexedDB = async (dbName, storeName) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WalletLoggerProvider = ({ children }) => {
|
export const WalletLoggerProvider = ({ children }) => {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const [logs, setLogs] = useState([])
|
const [logs, setLogs] = useState([])
|
||||||
let dbName = 'app:storage'
|
let dbName = 'app:storage'
|
||||||
if (me) {
|
if (me) {
|
||||||
|
|
|
@ -124,7 +124,7 @@ export const SETTINGS_FIELDS = gql`
|
||||||
|
|
||||||
export const SETTINGS = gql`
|
export const SETTINGS = gql`
|
||||||
${SETTINGS_FIELDS}
|
${SETTINGS_FIELDS}
|
||||||
{
|
query Settings {
|
||||||
settings {
|
settings {
|
||||||
...SettingsFields
|
...SettingsFields
|
||||||
}
|
}
|
||||||
|
@ -320,8 +320,8 @@ export const USER_FULL = gql`
|
||||||
|
|
||||||
export const USER = gql`
|
export const USER = gql`
|
||||||
${USER_FIELDS}
|
${USER_FIELDS}
|
||||||
query User($name: String!) {
|
query User($id: ID, $name: String) {
|
||||||
user(name: $name) {
|
user(id: $id, name: $name) {
|
||||||
...UserFields
|
...UserFields
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client'
|
import { ApolloClient, InMemoryCache, HttpLink, makeVar } from '@apollo/client'
|
||||||
import { decodeCursor, LIMIT } from './cursor'
|
import { decodeCursor, LIMIT } from './cursor'
|
||||||
import { SSR } from './constants'
|
import { SSR } from './constants'
|
||||||
|
|
||||||
|
@ -25,6 +25,8 @@ export default function getApolloClient () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const meAnonSats = {}
|
||||||
|
|
||||||
function getClient (uri) {
|
function getClient (uri) {
|
||||||
return new ApolloClient({
|
return new ApolloClient({
|
||||||
link: new HttpLink({ uri }),
|
link: new HttpLink({ uri }),
|
||||||
|
@ -259,10 +261,23 @@ function getClient (uri) {
|
||||||
Item: {
|
Item: {
|
||||||
fields: {
|
fields: {
|
||||||
meAnonSats: {
|
meAnonSats: {
|
||||||
read (meAnonSats, { readField }) {
|
read (existingAmount, { readField }) {
|
||||||
if (typeof window === 'undefined') return null
|
if (SSR) return null
|
||||||
|
|
||||||
const itemId = readField('id')
|
const itemId = readField('id')
|
||||||
return meAnonSats ?? Number(window.localStorage.getItem(`TIP-item:${itemId}`) || '0')
|
|
||||||
|
// we need to use reactive variables such that updates
|
||||||
|
// to local state propagate correctly
|
||||||
|
// see https://www.apollographql.com/docs/react/local-state/reactive-variables
|
||||||
|
let reactiveVar = meAnonSats[itemId]
|
||||||
|
if (!reactiveVar) {
|
||||||
|
const storageKey = `TIP-item:${itemId}`
|
||||||
|
const existingAmount = Number(window.localStorage.getItem(storageKey) || '0')
|
||||||
|
reactiveVar = makeVar(existingAmount || 0)
|
||||||
|
meAnonSats[itemId] = reactiveVar
|
||||||
|
}
|
||||||
|
|
||||||
|
return reactiveVar()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
"canonical-json": "0.0.4",
|
"canonical-json": "0.0.4",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"clipboard-copy": "^4.0.1",
|
"clipboard-copy": "^4.0.1",
|
||||||
|
"cookie": "^0.6.0",
|
||||||
"cross-fetch": "^4.0.0",
|
"cross-fetch": "^4.0.0",
|
||||||
"csv-parser": "^3.0.0",
|
"csv-parser": "^3.0.0",
|
||||||
"domino": "^2.1.6",
|
"domino": "^2.1.6",
|
||||||
|
@ -530,6 +531,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@auth/core/node_modules/cookie": {
|
||||||
|
"version": "0.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||||
|
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@auth/prisma-adapter": {
|
"node_modules/@auth/prisma-adapter": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@auth/prisma-adapter/-/prisma-adapter-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@auth/prisma-adapter/-/prisma-adapter-1.0.3.tgz",
|
||||||
|
@ -7267,9 +7277,10 @@
|
||||||
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
|
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
|
||||||
},
|
},
|
||||||
"node_modules/cookie": {
|
"node_modules/cookie": {
|
||||||
"version": "0.5.0",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
|
@ -9006,6 +9017,15 @@
|
||||||
"node": ">= 0.10.0"
|
"node": ">= 0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/express/node_modules/cookie": {
|
||||||
|
"version": "0.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||||
|
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/express/node_modules/debug": {
|
"node_modules/express/node_modules/debug": {
|
||||||
"version": "2.6.9",
|
"version": "2.6.9",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
@ -14752,6 +14772,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/next-auth/node_modules/cookie": {
|
||||||
|
"version": "0.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||||
|
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/next-plausible": {
|
"node_modules/next-plausible": {
|
||||||
"version": "3.11.1",
|
"version": "3.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/next-plausible/-/next-plausible-3.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/next-plausible/-/next-plausible-3.11.1.tgz",
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
"canonical-json": "0.0.4",
|
"canonical-json": "0.0.4",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"clipboard-copy": "^4.0.1",
|
"clipboard-copy": "^4.0.1",
|
||||||
|
"cookie": "^0.6.0",
|
||||||
"cross-fetch": "^4.0.0",
|
"cross-fetch": "^4.0.0",
|
||||||
"csv-parser": "^3.0.0",
|
"csv-parser": "^3.0.0",
|
||||||
"domino": "^2.1.6",
|
"domino": "^2.1.6",
|
||||||
|
|
|
@ -86,7 +86,7 @@ export default function User ({ ssrData }) {
|
||||||
const [create, setCreate] = useState(false)
|
const [create, setCreate] = useState(false)
|
||||||
const [edit, setEdit] = useState(false)
|
const [edit, setEdit] = useState(false)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
|
|
||||||
const { data } = useQuery(USER_FULL, { variables: { ...router.query } })
|
const { data } = useQuery(USER_FULL, { variables: { ...router.query } })
|
||||||
if (!data && !ssrData) return <PageLoading />
|
if (!data && !ssrData) return <PageLoading />
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { ChainFeeProvider } from '@/components/chain-fee.js'
|
||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic'
|
||||||
import { HasNewNotesProvider } from '@/components/use-has-new-notes'
|
import { HasNewNotesProvider } from '@/components/use-has-new-notes'
|
||||||
import WebLnProvider from '@/wallets/webln'
|
import WebLnProvider from '@/wallets/webln'
|
||||||
|
import { AccountProvider } from '@/components/account'
|
||||||
|
|
||||||
const PWAPrompt = dynamic(() => import('react-ios-pwa-prompt'), { ssr: false })
|
const PWAPrompt = dynamic(() => import('react-ios-pwa-prompt'), { ssr: false })
|
||||||
|
|
||||||
|
@ -109,22 +110,24 @@ export default function MyApp ({ Component, pageProps: { ...props } }) {
|
||||||
<WalletLoggerProvider>
|
<WalletLoggerProvider>
|
||||||
<WebLnProvider>
|
<WebLnProvider>
|
||||||
<ServiceWorkerProvider>
|
<ServiceWorkerProvider>
|
||||||
<PriceProvider price={price}>
|
<AccountProvider>
|
||||||
<LightningProvider>
|
<PriceProvider price={price}>
|
||||||
<ToastProvider>
|
<LightningProvider>
|
||||||
<ShowModalProvider>
|
<ToastProvider>
|
||||||
<BlockHeightProvider blockHeight={blockHeight}>
|
<ShowModalProvider>
|
||||||
<ChainFeeProvider chainFee={chainFee}>
|
<BlockHeightProvider blockHeight={blockHeight}>
|
||||||
<ErrorBoundary>
|
<ChainFeeProvider chainFee={chainFee}>
|
||||||
<Component ssrData={ssrData} {...otherProps} />
|
<ErrorBoundary>
|
||||||
{!router?.query?.disablePrompt && <PWAPrompt copyBody='This website has app functionality. Add it to your home screen to use it in fullscreen and receive notifications. In Safari:' promptOnVisit={2} />}
|
<Component ssrData={ssrData} {...otherProps} />
|
||||||
</ErrorBoundary>
|
{!router?.query?.disablePrompt && <PWAPrompt copyBody='This website has app functionality. Add it to your home screen to use it in fullscreen and receive notifications. In Safari:' promptOnVisit={2} />}
|
||||||
</ChainFeeProvider>
|
</ErrorBoundary>
|
||||||
</BlockHeightProvider>
|
</ChainFeeProvider>
|
||||||
</ShowModalProvider>
|
</BlockHeightProvider>
|
||||||
</ToastProvider>
|
</ShowModalProvider>
|
||||||
</LightningProvider>
|
</ToastProvider>
|
||||||
</PriceProvider>
|
</LightningProvider>
|
||||||
|
</PriceProvider>
|
||||||
|
</AccountProvider>
|
||||||
</ServiceWorkerProvider>
|
</ServiceWorkerProvider>
|
||||||
</WebLnProvider>
|
</WebLnProvider>
|
||||||
</WalletLoggerProvider>
|
</WalletLoggerProvider>
|
||||||
|
|
|
@ -7,11 +7,13 @@ import EmailProvider from 'next-auth/providers/email'
|
||||||
import prisma from '@/api/models'
|
import prisma from '@/api/models'
|
||||||
import nodemailer from 'nodemailer'
|
import nodemailer from 'nodemailer'
|
||||||
import { PrismaAdapter } from '@auth/prisma-adapter'
|
import { PrismaAdapter } from '@auth/prisma-adapter'
|
||||||
import { getToken } from 'next-auth/jwt'
|
import { NodeNextRequest, NodeNextResponse } from 'next/dist/server/base-http/node'
|
||||||
import { NodeNextRequest } from 'next/dist/server/base-http/node'
|
import { getToken, encode as encodeJWT } from 'next-auth/jwt'
|
||||||
|
import { datePivot } from '@/lib/time'
|
||||||
import { schnorr } from '@noble/curves/secp256k1'
|
import { schnorr } from '@noble/curves/secp256k1'
|
||||||
import { notifyReferral } from '@/lib/webPush'
|
import { notifyReferral } from '@/lib/webPush'
|
||||||
import { hashEmail } from '@/lib/crypto'
|
import { hashEmail } from '@/lib/crypto'
|
||||||
|
import cookie from 'cookie'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores userIds in user table
|
* Stores userIds in user table
|
||||||
|
@ -53,7 +55,7 @@ async function getReferrerId (referrer) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @returns {Partial<import('next-auth').CallbacksOptions>} */
|
/** @returns {Partial<import('next-auth').CallbacksOptions>} */
|
||||||
function getCallbacks (req) {
|
function getCallbacks (req, res) {
|
||||||
return {
|
return {
|
||||||
/**
|
/**
|
||||||
* @param {object} token Decrypted JSON Web Token
|
* @param {object} token Decrypted JSON Web Token
|
||||||
|
@ -88,6 +90,16 @@ function getCallbacks (req) {
|
||||||
token.sub = Number(token.id)
|
token.sub = Number(token.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// response is only defined during signup/login
|
||||||
|
if (req && res) {
|
||||||
|
req = new NodeNextRequest(req)
|
||||||
|
res = new NodeNextResponse(res)
|
||||||
|
const secret = process.env.NEXTAUTH_SECRET
|
||||||
|
const jwt = await encodeJWT({ token, secret })
|
||||||
|
const me = await prisma.user.findUnique({ where: { id: token.id } })
|
||||||
|
setMultiAuthCookies(req, res, { ...me, jwt })
|
||||||
|
}
|
||||||
|
|
||||||
return token
|
return token
|
||||||
},
|
},
|
||||||
async session ({ session, token }) {
|
async session ({ session, token }) {
|
||||||
|
@ -100,23 +112,78 @@ function getCallbacks (req) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pubkeyAuth (credentials, req, pubkeyColumnName) {
|
function setMultiAuthCookies (req, res, { id, jwt, name, photoId }) {
|
||||||
|
const b64Encode = obj => Buffer.from(JSON.stringify(obj)).toString('base64')
|
||||||
|
const b64Decode = s => JSON.parse(Buffer.from(s, 'base64'))
|
||||||
|
|
||||||
|
// default expiration for next-auth JWTs is in 1 month
|
||||||
|
const expiresAt = datePivot(new Date(), { months: 1 })
|
||||||
|
const cookieOptions = {
|
||||||
|
path: '/',
|
||||||
|
httpOnly: true,
|
||||||
|
secure: req.secure,
|
||||||
|
sameSite: 'lax',
|
||||||
|
expires: expiresAt
|
||||||
|
}
|
||||||
|
|
||||||
|
// add JWT to **httpOnly** cookie
|
||||||
|
res.appendHeader('Set-Cookie', cookie.serialize(`multi_auth.${id}`, jwt, cookieOptions))
|
||||||
|
|
||||||
|
let newMultiAuth = [{ id, name, photoId }]
|
||||||
|
if (req.cookies.multi_auth) {
|
||||||
|
const oldMultiAuth = b64Decode(req.cookies.multi_auth)
|
||||||
|
// make sure we don't add duplicates
|
||||||
|
if (oldMultiAuth.some(({ id: id_ }) => id_ === id)) return
|
||||||
|
newMultiAuth = [...oldMultiAuth, ...newMultiAuth]
|
||||||
|
}
|
||||||
|
res.appendHeader('Set-Cookie', cookie.serialize('multi_auth', b64Encode(newMultiAuth), { ...cookieOptions, httpOnly: false }))
|
||||||
|
|
||||||
|
// switch to user we just added
|
||||||
|
res.appendHeader('Set-Cookie', cookie.serialize('multi_auth.user-id', id, { ...cookieOptions, httpOnly: false }))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pubkeyAuth (credentials, req, res, pubkeyColumnName) {
|
||||||
const { k1, pubkey } = credentials
|
const { k1, pubkey } = credentials
|
||||||
|
|
||||||
|
// are we trying to add a new account for switching between?
|
||||||
|
const { body } = req.body
|
||||||
|
const multiAuth = typeof body.multiAuth === 'string' ? body.multiAuth === 'true' : !!body.multiAuth
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// does the given challenge (k1) exist in our db?
|
||||||
const lnauth = await prisma.lnAuth.findUnique({ where: { k1 } })
|
const lnauth = await prisma.lnAuth.findUnique({ where: { k1 } })
|
||||||
|
|
||||||
|
// delete challenge to prevent replay attacks
|
||||||
await prisma.lnAuth.delete({ where: { k1 } })
|
await prisma.lnAuth.delete({ where: { k1 } })
|
||||||
|
|
||||||
|
// does the given pubkey match the one for which we verified the signature?
|
||||||
if (lnauth.pubkey === pubkey) {
|
if (lnauth.pubkey === pubkey) {
|
||||||
|
// does the pubkey already exist in our db?
|
||||||
let user = await prisma.user.findUnique({ where: { [pubkeyColumnName]: pubkey } })
|
let user = await prisma.user.findUnique({ where: { [pubkeyColumnName]: pubkey } })
|
||||||
|
|
||||||
|
// get token if it exists
|
||||||
const token = await getToken({ req })
|
const token = await getToken({ req })
|
||||||
if (!user) {
|
if (!user) {
|
||||||
// if we are logged in, update rather than create
|
// we have not seen this pubkey before
|
||||||
if (token?.id) {
|
|
||||||
|
// only update our pubkey if we're not currently trying to add a new account
|
||||||
|
if (token?.id && !multiAuth) {
|
||||||
user = await prisma.user.update({ where: { id: token.id }, data: { [pubkeyColumnName]: pubkey } })
|
user = await prisma.user.update({ where: { id: token.id }, data: { [pubkeyColumnName]: pubkey } })
|
||||||
} else {
|
} else {
|
||||||
|
// we're not logged in: create new user with that pubkey
|
||||||
user = await prisma.user.create({ data: { name: pubkey.slice(0, 10), [pubkeyColumnName]: pubkey } })
|
user = await prisma.user.create({ data: { name: pubkey.slice(0, 10), [pubkeyColumnName]: pubkey } })
|
||||||
}
|
}
|
||||||
} else if (token && token?.id !== user.id) {
|
}
|
||||||
return null
|
|
||||||
|
if (token && token?.id !== user.id && multiAuth) {
|
||||||
|
// we're logged in as a different user than the one we're authenticating as
|
||||||
|
// and we want to add a new account. this means we want to add this account
|
||||||
|
// to our list of accounts for switching between so we issue a new JWT and
|
||||||
|
// update the cookies for multi-authentication.
|
||||||
|
const secret = process.env.NEXTAUTH_SECRET
|
||||||
|
const userJWT = await encodeJWT({ token: { id: user.id, name: user.name, email: user.email }, secret })
|
||||||
|
setMultiAuthCookies(req, res, { ...user, jwt: userJWT })
|
||||||
|
return token
|
||||||
}
|
}
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
@ -160,7 +227,7 @@ async function nostrEventAuth (event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {import('next-auth/providers').Provider[]} */
|
/** @type {import('next-auth/providers').Provider[]} */
|
||||||
const providers = [
|
const getProviders = res => [
|
||||||
CredentialsProvider({
|
CredentialsProvider({
|
||||||
id: 'lightning',
|
id: 'lightning',
|
||||||
name: 'Lightning',
|
name: 'Lightning',
|
||||||
|
@ -168,7 +235,9 @@ const providers = [
|
||||||
pubkey: { label: 'publickey', type: 'text' },
|
pubkey: { label: 'publickey', type: 'text' },
|
||||||
k1: { label: 'k1', type: 'text' }
|
k1: { label: 'k1', type: 'text' }
|
||||||
},
|
},
|
||||||
authorize: async (credentials, req) => await pubkeyAuth(credentials, new NodeNextRequest(req), 'pubkey')
|
authorize: async (credentials, req) => {
|
||||||
|
return await pubkeyAuth(credentials, new NodeNextRequest(req), new NodeNextResponse(res), 'pubkey')
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
CredentialsProvider({
|
CredentialsProvider({
|
||||||
id: 'nostr',
|
id: 'nostr',
|
||||||
|
@ -178,7 +247,7 @@ const providers = [
|
||||||
},
|
},
|
||||||
authorize: async ({ event }, req) => {
|
authorize: async ({ event }, req) => {
|
||||||
const credentials = await nostrEventAuth(event)
|
const credentials = await nostrEventAuth(event)
|
||||||
return await pubkeyAuth(credentials, new NodeNextRequest(req), 'nostrAuthPubkey')
|
return await pubkeyAuth(credentials, new NodeNextRequest(req), new NodeNextResponse(res), 'nostrAuthPubkey')
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
GitHubProvider({
|
GitHubProvider({
|
||||||
|
@ -213,9 +282,9 @@ const providers = [
|
||||||
]
|
]
|
||||||
|
|
||||||
/** @returns {import('next-auth').AuthOptions} */
|
/** @returns {import('next-auth').AuthOptions} */
|
||||||
export const getAuthOptions = req => ({
|
export const getAuthOptions = (req, res) => ({
|
||||||
callbacks: getCallbacks(req),
|
callbacks: getCallbacks(req, res),
|
||||||
providers,
|
providers: getProviders(res),
|
||||||
adapter: {
|
adapter: {
|
||||||
...PrismaAdapter(prisma),
|
...PrismaAdapter(prisma),
|
||||||
createUser: data => {
|
createUser: data => {
|
||||||
|
@ -299,7 +368,7 @@ async function enrollInNewsletter ({ email }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
await NextAuth(req, res, getAuthOptions(req))
|
await NextAuth(req, res, getAuthOptions(req, res))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendVerificationRequest ({
|
async function sendVerificationRequest ({
|
||||||
|
|
|
@ -66,6 +66,7 @@ export default startServerAndCreateNextHandler(apolloServer, {
|
||||||
session = { user: { ...sessionFields, apiKey: true } }
|
session = { user: { ...sessionFields, apiKey: true } }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
req = multiAuthMiddleware(req)
|
||||||
session = await getServerSession(req, res, getAuthOptions(req))
|
session = await getServerSession(req, res, getAuthOptions(req))
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
@ -79,3 +80,41 @@ export default startServerAndCreateNextHandler(apolloServer, {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function multiAuthMiddleware (request) {
|
||||||
|
// switch next-auth session cookie with multi_auth cookie if cookie pointer present
|
||||||
|
|
||||||
|
// is there a cookie pointer?
|
||||||
|
const cookiePointerName = 'multi_auth.user-id'
|
||||||
|
const hasCookiePointer = !!request.cookies[cookiePointerName]
|
||||||
|
|
||||||
|
// is there a session?
|
||||||
|
const sessionCookieName = request.secure ? '__Secure-next-auth.session-token' : 'next-auth.session-token'
|
||||||
|
const hasSession = !!request.cookies[sessionCookieName]
|
||||||
|
|
||||||
|
if (!hasCookiePointer || !hasSession) {
|
||||||
|
// no session or no cookie pointer. do nothing.
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = request.cookies[cookiePointerName]
|
||||||
|
if (userId === 'anonymous') {
|
||||||
|
// user switched to anon. only delete session cookie.
|
||||||
|
delete request.cookies[sessionCookieName]
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
const userJWT = request.cookies[`multi_auth.${userId}`]
|
||||||
|
if (!userJWT) {
|
||||||
|
// no JWT for account switching found
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userJWT) {
|
||||||
|
// use JWT found in cookie pointed to by cookie pointer
|
||||||
|
request.cookies[sessionCookieName] = userJWT
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
import cookie from 'cookie'
|
||||||
|
import { datePivot } from '../../lib/time'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {NextApiRequest} req
|
||||||
|
* @param {NextApiResponse} res
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
export default (req, res) => {
|
||||||
|
// is there a cookie pointer?
|
||||||
|
const cookiePointerName = 'multi_auth.user-id'
|
||||||
|
const userId = req.cookies[cookiePointerName]
|
||||||
|
|
||||||
|
// is there a session?
|
||||||
|
const sessionCookieName = req.secure ? '__Secure-next-auth.session-token' : 'next-auth.session-token'
|
||||||
|
const sessionJWT = req.cookies[sessionCookieName]
|
||||||
|
|
||||||
|
if (!userId && !sessionJWT) {
|
||||||
|
// no cookie pointer and no session cookie present. nothing to do.
|
||||||
|
res.status(404).end()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookies = []
|
||||||
|
|
||||||
|
const cookieOptions = {
|
||||||
|
path: '/',
|
||||||
|
secure: req.secure,
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: 'lax',
|
||||||
|
expires: datePivot(new Date(), { months: 1 })
|
||||||
|
}
|
||||||
|
// remove JWT pointed to by cookie pointer
|
||||||
|
cookies.push(cookie.serialize(`multi_auth.${userId}`, '', { ...cookieOptions, expires: 0, maxAge: 0 }))
|
||||||
|
|
||||||
|
// update multi_auth cookie and check if there are more accounts available
|
||||||
|
const oldMultiAuth = b64Decode(req.cookies.multi_auth)
|
||||||
|
const newMultiAuth = oldMultiAuth.filter(({ id }) => id !== Number(userId))
|
||||||
|
if (newMultiAuth.length === 0) {
|
||||||
|
// no next account available. cleanup: remove multi_auth + pointer cookie
|
||||||
|
cookies.push(cookie.serialize('multi_auth', '', { ...cookieOptions, httpOnly: false, expires: 0, maxAge: 0 }))
|
||||||
|
cookies.push(cookie.serialize('multi_auth.user-id', '', { ...cookieOptions, httpOnly: false, expires: 0, maxAge: 0 }))
|
||||||
|
res.setHeader('Set-Cookie', cookies)
|
||||||
|
res.status(204).end()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cookies.push(cookie.serialize('multi_auth', b64Encode(newMultiAuth), { ...cookieOptions, httpOnly: false }))
|
||||||
|
|
||||||
|
const newUserId = newMultiAuth[0].id
|
||||||
|
const newUserJWT = req.cookies[`multi_auth.${newUserId}`]
|
||||||
|
res.setHeader('Set-Cookie', [
|
||||||
|
...cookies,
|
||||||
|
cookie.serialize(cookiePointerName, newUserId, { ...cookieOptions, httpOnly: false }),
|
||||||
|
cookie.serialize(sessionCookieName, newUserJWT, cookieOptions)
|
||||||
|
])
|
||||||
|
|
||||||
|
res.status(302).end()
|
||||||
|
}
|
||||||
|
|
||||||
|
const b64Encode = obj => Buffer.from(JSON.stringify(obj)).toString('base64')
|
||||||
|
const b64Decode = s => JSON.parse(Buffer.from(s, 'base64'))
|
|
@ -6,8 +6,15 @@ import { StaticLayout } from '@/components/layout'
|
||||||
import Login from '@/components/login'
|
import Login from '@/components/login'
|
||||||
import { isExternal } from '@/lib/url'
|
import { isExternal } from '@/lib/url'
|
||||||
|
|
||||||
export async function getServerSideProps ({ req, res, query: { callbackUrl, error = null } }) {
|
export async function getServerSideProps ({ req, res, query: { callbackUrl, multiAuth = false, error = null } }) {
|
||||||
const session = await getServerSession(req, res, getAuthOptions(req))
|
let session = await getServerSession(req, res, getAuthOptions(req))
|
||||||
|
|
||||||
|
// required to prevent infinite redirect loops if we switch to anon
|
||||||
|
// but are on a page that would redirect us to /signup.
|
||||||
|
// without this code, /signup would redirect us back to the callbackUrl.
|
||||||
|
if (req.cookies['multi_auth.user-id'] === 'anonymous') {
|
||||||
|
session = null
|
||||||
|
}
|
||||||
|
|
||||||
// prevent open redirects. See https://github.com/stackernews/stacker.news/issues/264
|
// prevent open redirects. See https://github.com/stackernews/stacker.news/issues/264
|
||||||
// let undefined urls through without redirect ... otherwise this interferes with multiple auth linking
|
// let undefined urls through without redirect ... otherwise this interferes with multiple auth linking
|
||||||
|
@ -22,9 +29,9 @@ export async function getServerSideProps ({ req, res, query: { callbackUrl, erro
|
||||||
callbackUrl = '/'
|
callbackUrl = '/'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session && callbackUrl) {
|
if (session && callbackUrl && !multiAuth) {
|
||||||
// in the cause of auth linking we want to pass the error back to
|
// in the case of auth linking we want to pass the error back to settings
|
||||||
// settings
|
// in the case of multi auth, don't redirect if there is already a session
|
||||||
if (error) {
|
if (error) {
|
||||||
const url = new URL(callbackUrl, process.env.NEXT_PUBLIC_URL)
|
const url = new URL(callbackUrl, process.env.NEXT_PUBLIC_URL)
|
||||||
url.searchParams.set('error', error)
|
url.searchParams.set('error', error)
|
||||||
|
@ -39,11 +46,14 @@ export async function getServerSideProps ({ req, res, query: { callbackUrl, erro
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const providers = await getProviders()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
providers: await getProviders(),
|
providers,
|
||||||
callbackUrl,
|
callbackUrl,
|
||||||
error
|
error,
|
||||||
|
multiAuth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ export const getServerSideProps = getGetServerSideProps({ query: REFERRALS, auth
|
||||||
|
|
||||||
export default function Referrals ({ ssrData }) {
|
export default function Referrals ({ ssrData }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
|
|
||||||
const select = async values => {
|
const select = async values => {
|
||||||
const { when, ...query } = values
|
const { when, ...query } = values
|
||||||
|
|
|
@ -84,7 +84,7 @@ export function SettingsHeader () {
|
||||||
|
|
||||||
export default function Settings ({ ssrData }) {
|
export default function Settings ({ ssrData }) {
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const [setSettings] = useMutation(SET_SETTINGS, {
|
const [setSettings] = useMutation(SET_SETTINGS, {
|
||||||
update (cache, { data: { setSettings } }) {
|
update (cache, { data: { setSettings } }) {
|
||||||
cache.modify({
|
cache.modify({
|
||||||
|
@ -96,13 +96,14 @@ export default function Settings ({ ssrData }) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
)
|
|
||||||
const logger = useServiceWorkerLogger()
|
const logger = useServiceWorkerLogger()
|
||||||
|
|
||||||
const { data } = useQuery(SETTINGS)
|
const { data } = useQuery(SETTINGS)
|
||||||
const { settings: { privates: settings } } = useMemo(() => data ?? ssrData, [data, ssrData])
|
const { settings: { privates: settings } } = useMemo(() => data ?? ssrData, [data, ssrData])
|
||||||
if (!data && !ssrData) return <PageLoading />
|
|
||||||
|
// if we switched to anon, me is null before the page is reloaded
|
||||||
|
if ((!data && !ssrData) || !me) return <PageLoading />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
|
@ -110,6 +111,7 @@ export default function Settings ({ ssrData }) {
|
||||||
<SettingsHeader />
|
<SettingsHeader />
|
||||||
{hasOnlyOneAuthMethod(settings?.authMethods) && <AuthBanner />}
|
{hasOnlyOneAuthMethod(settings?.authMethods) && <AuthBanner />}
|
||||||
<Form
|
<Form
|
||||||
|
enableReinitialize
|
||||||
initial={{
|
initial={{
|
||||||
tipDefault: settings?.tipDefault || 21,
|
tipDefault: settings?.tipDefault || 21,
|
||||||
tipRandom: settings?.tipRandom,
|
tipRandom: settings?.tipRandom,
|
||||||
|
@ -870,7 +872,7 @@ export function EmailLinkForm ({ callbackUrl }) {
|
||||||
|
|
||||||
function ApiKey ({ enabled, apiKey }) {
|
function ApiKey ({ enabled, apiKey }) {
|
||||||
const showModal = useShowModal()
|
const showModal = useShowModal()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const [generateApiKey] = useMutation(
|
const [generateApiKey] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
mutation generateApiKey($id: ID!) {
|
mutation generateApiKey($id: ID!) {
|
||||||
|
@ -996,7 +998,7 @@ function ApiKeyModal ({ apiKey }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function ApiKeyDeleteObstacle ({ onClose }) {
|
function ApiKeyDeleteObstacle ({ onClose }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const [deleteApiKey] = useMutation(
|
const [deleteApiKey] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
mutation deleteApiKey($id: ID!) {
|
mutation deleteApiKey($id: ID!) {
|
||||||
|
|
|
@ -61,7 +61,7 @@ export default function Wallet () {
|
||||||
}
|
}
|
||||||
|
|
||||||
function YouHaveSats () {
|
function YouHaveSats () {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const limitReached = me?.privates?.sats >= msatsToSats(BALANCE_LIMIT_MSATS)
|
const limitReached = me?.privates?.sats >= msatsToSats(BALANCE_LIMIT_MSATS)
|
||||||
return (
|
return (
|
||||||
<h2 className={`${me ? 'visible' : 'invisible'} ${limitReached ? 'text-warning' : 'text-success'}`}>
|
<h2 className={`${me ? 'visible' : 'invisible'} ${limitReached ? 'text-warning' : 'text-success'}`}>
|
||||||
|
@ -108,7 +108,7 @@ export function WalletForm () {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FundForm () {
|
export function FundForm () {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const [showAlert, setShowAlert] = useState(true)
|
const [showAlert, setShowAlert] = useState(true)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [createInvoice, { called, error }] = useMutation(gql`
|
const [createInvoice, { called, error }] = useMutation(gql`
|
||||||
|
@ -211,7 +211,7 @@ export function SelectedWithdrawalForm () {
|
||||||
|
|
||||||
export function InvWithdrawal () {
|
export function InvWithdrawal () {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
|
|
||||||
const [createWithdrawl, { called, error }] = useMutation(CREATE_WITHDRAWL)
|
const [createWithdrawl, { called, error }] = useMutation(CREATE_WITHDRAWL)
|
||||||
|
|
||||||
|
@ -358,7 +358,7 @@ export function LnWithdrawal () {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LnAddrWithdrawal () {
|
export function LnAddrWithdrawal () {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [sendToLnAddr, { called, error }] = useMutation(SEND_TO_LNADDR)
|
const [sendToLnAddr, { called, error }] = useMutation(SEND_TO_LNADDR)
|
||||||
const defaultOptions = { min: 1 }
|
const defaultOptions = { min: 1 }
|
||||||
|
|
|
@ -123,7 +123,7 @@ function LoadWithdrawl () {
|
||||||
function PrivacyOption ({ wd }) {
|
function PrivacyOption ({ wd }) {
|
||||||
if (!wd.bolt11) return
|
if (!wd.bolt11) return
|
||||||
|
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const keepUntil = datePivot(new Date(wd.createdAt), { days: INVOICE_RETENTION_DAYS })
|
const keepUntil = datePivot(new Date(wd.createdAt), { days: INVOICE_RETENTION_DAYS })
|
||||||
const oldEnough = new Date() >= keepUntil
|
const oldEnough = new Date() >= keepUntil
|
||||||
if (!oldEnough) {
|
if (!oldEnough) {
|
||||||
|
|
|
@ -241,6 +241,10 @@ $zindex-sticky: 900;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.w-fit-content {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
@media (display-mode: standalone) {
|
@media (display-mode: standalone) {
|
||||||
.standalone {
|
.standalone {
|
||||||
display: flex
|
display: flex
|
||||||
|
@ -251,6 +255,14 @@ $zindex-sticky: 900;
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: var(--bs-body-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill-primary {
|
||||||
|
fill: var(--bs-primary);
|
||||||
|
}
|
||||||
|
|
||||||
.text-primary svg {
|
.text-primary svg {
|
||||||
fill: var(--bs-primary);
|
fill: var(--bs-primary);
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ export const Status = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useWallet (name) {
|
export function useWallet (name) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const showModal = useShowModal()
|
const showModal = useShowModal()
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const [disableFreebies] = useMutation(gql`mutation { disableFreebies }`)
|
const [disableFreebies] = useMutation(gql`mutation { disableFreebies }`)
|
||||||
|
@ -151,7 +151,7 @@ function extractServerConfig (fields, config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function useConfig (wallet) {
|
function useConfig (wallet) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
|
|
||||||
const storageKey = getStorageKey(wallet?.name, me)
|
const storageKey = getStorageKey(wallet?.name, me)
|
||||||
const [clientConfig, setClientConfig, clearClientConfig] = useClientConfig(storageKey, {})
|
const [clientConfig, setClientConfig, clearClientConfig] = useClientConfig(storageKey, {})
|
||||||
|
@ -268,7 +268,7 @@ function isConfigured ({ fields, config }) {
|
||||||
|
|
||||||
function useServerConfig (wallet) {
|
function useServerConfig (wallet) {
|
||||||
const client = useApolloClient()
|
const client = useApolloClient()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
|
|
||||||
const { data, refetch: refetchConfig } = useQuery(WALLET_BY_TYPE, { variables: { type: wallet?.walletType }, skip: !wallet?.walletType })
|
const { data, refetch: refetchConfig } = useQuery(WALLET_BY_TYPE, { variables: { type: wallet?.walletType }, skip: !wallet?.walletType })
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue