Skip to content
2 changes: 1 addition & 1 deletion src/app/(mobile-ui)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
}

// Show waitlist page if user doesn't have app access
if (!isFetchingUser && user && !user?.user.hasAppAccess) {
if (!isFetchingUser && user && !user?.user.hasAppAccess && !isPublicPath) {
return <JoinWaitlistPage />
}

Expand Down
43 changes: 42 additions & 1 deletion src/components/Claim/Link/Initial.view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ import { GuestVerificationModal } from '@/components/Global/GuestVerificationMod
import useKycStatus from '@/hooks/useKycStatus'
import MantecaFlowManager from './MantecaFlowManager'
import ErrorAlert from '@/components/Global/ErrorAlert'
import { invitesApi } from '@/services/invites'
import { EInviteType } from '@/services/services.types'

export const InitialClaimLinkView = (props: IClaimScreenProps) => {
const {
Expand Down Expand Up @@ -104,7 +106,7 @@ export const InitialClaimLinkView = (props: IClaimScreenProps) => {
const { claimLink, claimLinkXchain, removeParamStep } = useClaimLink()
const { isConnected: isPeanutWallet, address, fetchBalance } = useWallet()
const router = useRouter()
const { user } = useAuth()
const { user, fetchUser } = useAuth()
const queryClient = useQueryClient()
const searchParams = useSearchParams()
const prevRecipientType = useRef<string | null>(null)
Expand Down Expand Up @@ -164,6 +166,45 @@ export const InitialClaimLinkView = (props: IClaimScreenProps) => {

if (recipient.address === '') return

// If the user doesn't have app access, accept the invite before claiming the link
if (!user?.user.hasAppAccess) {
try {
const inviterUsername = claimLinkData.sender?.username
if (!inviterUsername) {
setErrorState({
showError: true,
errorMessage: 'Unable to accept invite: missing inviter. Please contact support.',
})
setLoadingState('Idle')
return
}
const inviteCode = `${inviterUsername}INVITESYOU`
const result = await invitesApi.acceptInvite(inviteCode, EInviteType.PAYMENT_LINK)
if (!result.success) {
console.error('Failed to accept invite')
setErrorState({
showError: true,
errorMessage: 'Something went wrong. Please try again or contact support.',
})
setLoadingState('Idle')
return
}

// fetch user so that we have the latest state and user can access the app.
// We dont need to wait for this, can happen in background.
fetchUser()
} catch (error) {
Sentry.captureException(error)
console.error('Failed to accept invite', error)
setErrorState({
showError: true,
errorMessage: 'Something went wrong. Please try again or contact support.',
})
setLoadingState('Idle')
return
}
}

try {
setLoadingState('Executing transaction')
if (isPeanutWallet) {
Expand Down
4 changes: 2 additions & 2 deletions src/components/Common/ActionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ export default function ActionList({
const inviteCode = `${username}INVITESYOU`
dispatch(setupActions.setInviteCode(inviteCode))
dispatch(setupActions.setInviteType(EInviteType.PAYMENT_LINK))
router.push(`/setup?step=signup&redirect_uri=${redirectUri}`)
router.push(`/invite?code=${inviteCode}&redirect_uri=${redirectUri}`)
} else {
router.push(`/setup?redirect_uri=${redirectUri}`)
}
Expand Down Expand Up @@ -233,7 +233,7 @@ export default function ActionList({
</Button>
)}
{isInviteLink && !userHasAppAccess && username && (
<div className="!mt-6 flex w-full items-center justify-between">
<div className="!mt-6 flex w-full items-center justify-center gap-1 md:gap-2">
<Image src={starStraightImage.src} alt="star" width={20} height={20} />{' '}
<p className="text-center text-sm">Invited by {username}, you have early access!</p>
<Image src={starStraightImage.src} alt="star" width={20} height={20} />
Expand Down
14 changes: 9 additions & 5 deletions src/components/Global/EarlyUserModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,18 @@ const EarlyUserModal = () => {
<>
<p className="text-sm text-grey-1">
<span className="block">
Peanut is now <b>invite-only</b> and you're in!
Peanut is now <b>invite-only.</b>
</span>
<span className="mt-2 block">
<b>Friends you invite </b>→ you earn a cut of their fees
<span>
Share your link to earn a share of fees from your invitees and a smaller share when their
friends join.
</span>
<span className="block">
<b> Their invites </b> you earn a cut of the cut
{/* <span className="mt-2 block">
<b>Friends you invite: </b> you earn a share of their fees.
</span>
<span className="block">
<b> Their invites: </b> you earn a smaller share, too.
</span> */}
</p>
<ShareButton
generateText={() => Promise.resolve(generateInvitesShareText(inviteLink))}
Expand Down
8 changes: 7 additions & 1 deletion src/components/Global/GuestLoginCta/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,13 @@ const GuestLoginCta = ({ hideConnectWallet = false, view }: GuestLoginCtaProps)
const redirect_uri = searchParams.get('redirect_uri')
if (redirect_uri) {
const sanitizedRedirectUrl = sanitizeRedirectURL(redirect_uri)
router.push(sanitizedRedirectUrl)
// Only redirect if the URL is safe (same-origin)
if (sanitizedRedirectUrl) {
router.push(sanitizedRedirectUrl)
} else {
// If redirect_uri was invalid, stay on current page
Sentry.captureException(`Invalid redirect URL ${redirect_uri}`)
}
}
} catch (e) {
toast.error('Error logging in')
Expand Down
28 changes: 15 additions & 13 deletions src/components/Invites/InvitesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@ import { setupActions } from '@/redux/slices/setup-slice'
import { useAuth } from '@/context/authContext'
import { EInviteType } from '@/services/services.types'
import { saveToCookie } from '@/utils'
import { useLogin } from '@/hooks/useLogin'

function InvitePageContent() {
const searchParams = useSearchParams()
const inviteCode = searchParams.get('code')
const { logoutUser, isLoggingOut, user } = useAuth()
const redirectUri = searchParams.get('redirect_uri')
const { user } = useAuth()

const dispatch = useAppDispatch()
const router = useRouter()
const { handleLoginClick, isLoggingIn } = useLogin()

const {
data: inviteCodeData,
Expand All @@ -38,15 +41,12 @@ function InvitePageContent() {
dispatch(setupActions.setInviteCode(inviteCode))
dispatch(setupActions.setInviteType(EInviteType.PAYMENT_LINK))
saveToCookie('inviteCode', inviteCode) // Save to cookies as well, so that if user installs PWA, they can still use the invite code
router.push('/setup?step=signup')
}
}

const handleLogout = () => {
if (user) {
logoutUser()
} else {
router.push('/setup')
if (redirectUri) {
const encodedRedirectUri = encodeURIComponent(redirectUri)
router.push('/setup?step=signup&redirect_uri=' + encodedRedirectUri)
} else {
router.push('/setup?step=signup')
}
}
}

Expand Down Expand Up @@ -86,9 +86,11 @@ function InvitePageContent() {
Claim your spot
</Button>

<button disabled={isLoggingOut} onClick={handleLogout} className="text-sm underline">
{isLoggingOut ? 'Please wait...' : 'Already have an account? Log in!'}
</button>
{!user?.user && (
<button disabled={isLoggingIn} onClick={handleLoginClick} className="text-sm underline">
{isLoggingIn ? 'Please wait...' : 'Already have an account? Log in!'}
</button>
)}
</div>
</div>
</div>
Expand Down
48 changes: 45 additions & 3 deletions src/components/Payment/PaymentForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import { useUserInteractions } from '@/hooks/useUserInteractions'
import { useUserByUsername } from '@/hooks/useUserByUsername'
import { PaymentFlow } from '@/app/[...recipient]/client'
import MantecaFulfillment from '../Views/MantecaFulfillment.view'
import { invitesApi } from '@/services/invites'
import { EInviteType } from '@/services/services.types'

export type PaymentFlowProps = {
isExternalWalletFlow?: boolean
Expand Down Expand Up @@ -66,7 +68,7 @@ export const PaymentForm = ({
}: PaymentFormProps) => {
const dispatch = useAppDispatch()
const router = useRouter()
const { user } = useAuth()
const { user, fetchUser } = useAuth()
const { requestDetails, chargeDetails, daimoError, error: paymentStoreError, attachmentOptions } = usePaymentStore()
const {
setShowExternalWalletFulfillMethods,
Expand All @@ -91,6 +93,8 @@ export const PaymentForm = ({
const [inputTokenAmount, setInputTokenAmount] = useState<string>(
chargeDetails?.tokenAmount || requestDetails?.tokenAmount || amount || ''
)
const [isAcceptingInvite, setIsAcceptingInvite] = useState(false)
const [inviteError, setInviteError] = useState(false)

// states
const [disconnectWagmiModal, setDisconnectWagmiModal] = useState<boolean>(false)
Expand All @@ -108,8 +112,9 @@ export const PaymentForm = ({
const error = useMemo(() => {
if (paymentStoreError) return ErrorHandler(paymentStoreError)
if (initiatorError) return ErrorHandler(initiatorError)
if (inviteError) return 'Something went wrong. Please try again or contact support.'
return null
}, [paymentStoreError, initiatorError])
}, [paymentStoreError, initiatorError, inviteError])

const {
selectedTokenPrice,
Expand Down Expand Up @@ -314,8 +319,43 @@ export const PaymentForm = ({
isActivePeanutWallet,
])

const handleAcceptInvite = async () => {
try {
setIsAcceptingInvite(true)
const inviteCode = `${recipient?.identifier}INVITESYOU`
const result = await invitesApi.acceptInvite(inviteCode, EInviteType.PAYMENT_LINK)

if (!result.success) {
console.error('Failed to accept invite')
setInviteError(true)
setIsAcceptingInvite(false)
return false
}

// fetch user so that we have the latest state and user can access the app.
// We dont need to wait for this, can happen in background.
await fetchUser()
setIsAcceptingInvite(false)
return true
} catch (error) {
console.error('Failed to accept invite', error)
setInviteError(true)
setIsAcceptingInvite(false)
return false
}
}

const handleInitiatePayment = useCallback(async () => {
// clear invite error
if (inviteError) {
setInviteError(false)
}
if (isActivePeanutWallet && isInsufficientBalanceError && !isExternalWalletFlow) {
// If the user doesn't have app access, accept the invite before claiming the link
if (recipient.recipientType === 'USERNAME' && !user?.user.hasAppAccess) {
const isAccepted = await handleAcceptInvite()
if (!isAccepted) return
}
router.push('/add-money')
return
}
Expand Down Expand Up @@ -416,6 +456,8 @@ export const PaymentForm = ({
selectedChainID,
inputUsdValue,
requestedTokenPrice,
inviteError,
handleAcceptInvite,
])

const getButtonText = () => {
Expand Down Expand Up @@ -654,7 +696,7 @@ export const PaymentForm = ({
{isPeanutWalletConnected && (!error || isInsufficientBalanceError) && (
<Button
variant="purple"
loading={isProcessing}
loading={isAcceptingInvite || isProcessing}
shadowSize="4"
onClick={handleInitiatePayment}
disabled={isButtonDisabled}
Expand Down
6 changes: 1 addition & 5 deletions src/components/Profile/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,7 @@ export const Profile = () => {
label="Identity Verification"
href="/profile/identity-verification"
onClick={() => {
if (isUserKycApproved) {
setIsKycApprovedModalOpen(true)
} else {
setShowInitiateKycModal(true)
}
setShowInitiateKycModal(true)
}}
position="middle"
endIcon={isUserKycApproved ? 'check' : undefined}
Expand Down
33 changes: 3 additions & 30 deletions src/components/Setup/Views/JoinWaitlist.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@ import { useSetupFlow } from '@/hooks/useSetupFlow'
import { useAppDispatch } from '@/redux/hooks'
import { setupActions } from '@/redux/slices/setup-slice'
import { invitesApi } from '@/services/invites'
import { useRouter, useSearchParams } from 'next/navigation'
import { getFromLocalStorage, sanitizeRedirectURL } from '@/utils'
import ErrorAlert from '@/components/Global/ErrorAlert'
import { useAuth } from '@/context/authContext'
import { useLogin } from '@/hooks/useLogin'

const JoinWaitlist = () => {
const [inviteCode, setInviteCode] = useState('')
Expand All @@ -23,13 +21,10 @@ const JoinWaitlist = () => {
const [isLoading, setisLoading] = useState(false)
const [error, setError] = useState('')

const { handleLogin, isLoggingIn } = useZeroDev()
const toast = useToast()
const { handleNext } = useSetupFlow()
const dispatch = useAppDispatch()
const router = useRouter()
const searchParams = useSearchParams()
const { user } = useAuth()
const { handleLoginClick, isLoggingIn } = useLogin()

const validateInviteCode = async (inviteCode: string): Promise<boolean> => {
try {
Expand Down Expand Up @@ -62,34 +57,12 @@ const JoinWaitlist = () => {

const onLoginClick = async () => {
try {
await handleLogin()
await handleLoginClick()
} catch (e) {
handleError(e)
}
}

// Wait for user to be fetched, then redirect
useEffect(() => {
if (user) {
const localStorageRedirect = getFromLocalStorage('redirect')
const redirect_uri = searchParams.get('redirect_uri')
if (redirect_uri) {
let decodedRedirect = redirect_uri
try {
decodedRedirect = decodeURIComponent(redirect_uri)
} catch {}
const sanitizedRedirectUrl = sanitizeRedirectURL(decodedRedirect)
router.push(sanitizedRedirectUrl)
} else if (localStorageRedirect) {
localStorage.removeItem('redirect')
const sanitizedLocalRedirect = sanitizeRedirectURL(String(localStorageRedirect))
router.push(sanitizedLocalRedirect)
} else {
router.push('/home')
}
}
}, [user, router, searchParams])

return (
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2">
Expand Down
11 changes: 7 additions & 4 deletions src/components/Setup/Views/SetupPasskey.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import * as Sentry from '@sentry/nextjs'
import { WalletProviderType, AccountType } from '@/interfaces'
import { WebAuthnError } from '@simplewebauthn/browser'
import Link from 'next/link'
import { getFromCookie, getFromLocalStorage, sanitizeRedirectURL } from '@/utils'
import { getFromCookie, getFromLocalStorage, getValidRedirectUrl, sanitizeRedirectURL } from '@/utils'
import { POST_SIGNUP_ACTIONS } from '@/components/Global/PostSignupActionManager/post-signup-action.consts'

const SetupPasskey = () => {
Expand Down Expand Up @@ -47,9 +47,11 @@ const SetupPasskey = () => {

const redirect_uri = searchParams.get('redirect_uri')
if (redirect_uri) {
const sanitizedRedirectUrl = sanitizeRedirectURL(redirect_uri)
router.push(sanitizedRedirectUrl)
const validRedirectUrl = getValidRedirectUrl(redirect_uri, '/home')
// Only redirect if the URL is safe (same-origin)
router.push(validRedirectUrl)
return
// If redirect_uri was invalid, fall through to other redirect logic
}

const localStorageRedirect = getFromLocalStorage('redirect')
Expand All @@ -62,7 +64,8 @@ const SetupPasskey = () => {
router.push('/home')
} else {
localStorage.removeItem('redirect')
router.push(localStorageRedirect)
const validRedirectUrl = getValidRedirectUrl(localStorageRedirect, '/home')
router.push(validRedirectUrl)
}
} else {
router.push('/home')
Expand Down
Loading
Loading