diff --git a/src/app/(mobile-ui)/home/page.tsx b/src/app/(mobile-ui)/home/page.tsx index ab611456a..2975e3eaf 100644 --- a/src/app/(mobile-ui)/home/page.tsx +++ b/src/app/(mobile-ui)/home/page.tsx @@ -41,6 +41,8 @@ import { useDeviceType, DeviceType } from '@/hooks/useGetDeviceType' import useKycStatus from '@/hooks/useKycStatus' import HomeBanners from '@/components/Home/HomeBanners' import InvitesIcon from '@/components/Home/InvitesIcon' +import NoMoreJailModal from '@/components/Global/NoMoreJailModal' +import EarlyUserModal from '@/components/Global/EarlyUserModal' const BALANCE_WARNING_THRESHOLD = parseInt(process.env.NEXT_PUBLIC_BALANCE_WARNING_THRESHOLD ?? '500') const BALANCE_WARNING_EXPIRY = parseInt(process.env.NEXT_PUBLIC_BALANCE_WARNING_EXPIRY ?? '1814400') // 21 days in seconds @@ -66,7 +68,7 @@ export default function Home() { const [showAddMoneyPromptModal, setShowAddMoneyPromptModal] = useState(false) const [showBalanceWarningModal, setShowBalanceWarningModal] = useState(false) // const [showReferralCampaignModal, setShowReferralCampaignModal] = useState(false) - const [isPostSignupActionModalVisible, setIsPostSignupActionModalVisible] = useState(false) + const [showPostSignupActionModal, setShowPostSignupActionModal] = useState(false) const userFullName = useMemo(() => { if (!user) return @@ -127,7 +129,7 @@ export default function Home() { !isStandalone && !hasSeenModalThisSession && !user?.hasPwaInstalled && - !isPostSignupActionModalVisible && + !showPostSignupActionModal && !redirectUrl ) { setShowIOSPWAInstallModal(true) @@ -136,7 +138,7 @@ export default function Home() { setShowIOSPWAInstallModal(false) } } - }, [user?.hasPwaInstalled, isPostSignupActionModalVisible, deviceType]) + }, [user?.hasPwaInstalled, showPostSignupActionModal, deviceType]) // effect for showing balance warning modal useEffect(() => { @@ -155,7 +157,7 @@ export default function Home() { !hasSeenBalanceWarning && !showIOSPWAInstallModal && !showAddMoneyPromptModal && - !isPostSignupActionModalVisible + !showPostSignupActionModal ) { setShowBalanceWarningModal(true) } @@ -189,12 +191,15 @@ export default function Home() { useEffect(() => { if (typeof window !== 'undefined' && !isFetchingBalance) { const hasSeenAddMoneyPromptThisSession = sessionStorage.getItem('hasSeenAddMoneyPromptThisSession') + const showNoMoreJailModal = sessionStorage.getItem('showNoMoreJailModal') // show if: // 1. balance is zero. // 2. user hasn't seen this prompt in the current session. // 3. the iOS PWA install modal is not currently active. // 4. the balance warning modal is not currently active. + // 5. the early user modal is not currently active. + // 6. the no more jail modal is not currently active. // this allows the modal on any device (iOS/Android) and in any display mode (PWA/browser), // as long as the PWA modal (which is iOS & browser-specific) isn't taking precedence. if ( @@ -202,7 +207,9 @@ export default function Home() { !hasSeenAddMoneyPromptThisSession && !showIOSPWAInstallModal && !showBalanceWarningModal && - !isPostSignupActionModalVisible + !showPostSignupActionModal && + showNoMoreJailModal !== 'true' && + !user?.showEarlyUserModal // Give Early User and No more jail modal precedence, showing two modals together isn't ideal and it messes up their functionality ) { setShowAddMoneyPromptModal(true) sessionStorage.setItem('hasSeenAddMoneyPromptThisSession', 'true') @@ -268,6 +275,10 @@ export default function Home() { {/* Add Money Prompt Modal */} setShowAddMoneyPromptModal(false)} /> + + + + {/* Balance Warning Modal */} setShowReferralCampaignModal(true)} /> */} {/* Post Signup Action Modal */} - + ) } diff --git a/src/components/Global/EarlyUserModal/index.tsx b/src/components/Global/EarlyUserModal/index.tsx new file mode 100644 index 000000000..509692da8 --- /dev/null +++ b/src/components/Global/EarlyUserModal/index.tsx @@ -0,0 +1,52 @@ +'use client' +import { useEffect, useState } from 'react' +import ActionModal from '../ActionModal' +import ShareButton from '../ShareButton' +import { generateInviteCodeLink, generateInvitesShareText } from '@/utils' +import { useAuth } from '@/context/authContext' +import { updateUserById } from '@/app/actions/users' + +const EarlyUserModal = () => { + const { user } = useAuth() + const inviteLink = generateInviteCodeLink(user?.user.username ?? '').inviteLink + const [showModal, setShowModal] = useState(false) + + useEffect(() => { + if (user && user.showEarlyUserModal) { + setShowModal(true) + } + }, [user]) + + const handleCloseModal = () => { + setShowModal(false) + updateUserById({ userId: user?.user.userId, hasSeenEarlyUserModal: true }) + } + + return ( + +

+ Peanut is now invite-only. +
+ As an early user, you keep full access and you get the power to invite friends. +
+ Each invite earns you points and perks. +

+ Promise.resolve(generateInvitesShareText(inviteLink))} + title="Share your invite link" + > + Share Invite link + + + } + /> + ) +} + +export default EarlyUserModal diff --git a/src/components/Global/Icons/Icon.tsx b/src/components/Global/Icons/Icon.tsx index 647e135bd..5da9f3b1f 100644 --- a/src/components/Global/Icons/Icon.tsx +++ b/src/components/Global/Icons/Icon.tsx @@ -61,6 +61,7 @@ import { QuestionMarkIcon } from './question-mark' import { ShieldIcon } from './shield' import { TrophyIcon } from './trophy' import { InviteHeartIcon } from './invite-heart' +import { LockIcon } from './lock' // available icon names export type IconName = @@ -126,6 +127,7 @@ export type IconName = | 'question-mark' | 'trophy' | 'invite-heart' + | 'lock' export interface IconProps extends SVGProps { name: IconName @@ -196,6 +198,7 @@ const iconComponents: Record>> = shield: ShieldIcon, trophy: TrophyIcon, 'invite-heart': InviteHeartIcon, + lock: LockIcon, } export const Icon: FC = ({ name, size = 24, width, height, ...props }) => { diff --git a/src/components/Global/Icons/lock.tsx b/src/components/Global/Icons/lock.tsx new file mode 100644 index 000000000..5467d8b67 --- /dev/null +++ b/src/components/Global/Icons/lock.tsx @@ -0,0 +1,19 @@ +import { FC, SVGProps } from 'react' + +export const LockIcon: FC> = (props) => { + return ( + + + + ) +} diff --git a/src/components/Global/NoMoreJailModal/index.tsx b/src/components/Global/NoMoreJailModal/index.tsx new file mode 100644 index 000000000..70a9cb1f8 --- /dev/null +++ b/src/components/Global/NoMoreJailModal/index.tsx @@ -0,0 +1,71 @@ +'use client' +import React, { FC, useEffect, useState } from 'react' +import Image from 'next/image' +import { PEANUT_LOGO_BLACK, PEANUTMAN_LOGO } from '@/assets' +import Modal from '../Modal' +import { Button } from '@/components/0_Bruddle' +import chillPeanutAnim from '@/animations/GIF_ALPHA_BACKGORUND/512X512_ALPHA_GIF_konradurban_01.gif' + +const NoMoreJailModal = () => { + const [isOpen, setisOpen] = useState(false) + + const onClose = () => { + setisOpen(false) + sessionStorage.removeItem('showNoMoreJailModal') + } + + useEffect(() => { + const showNoMoreJailModal = sessionStorage.getItem('showNoMoreJailModal') + if (showNoMoreJailModal === 'true') { + setisOpen(true) + } + }, []) + + return ( + + {/* Main content container */} +
+
+
+
+

+ No more Peanut jail! +

+ +
+

+ You’re now part of Peanut! +
+ Explore, pay, and invite your friends. +

+
+
+
+ + +
+
+ + {/* Peanutman animation */} +
+
+ Peanut Man +
+
+
+ ) +} + +export default NoMoreJailModal diff --git a/src/components/Home/InvitesIcon.tsx b/src/components/Home/InvitesIcon.tsx index f0586d213..7706b194e 100644 --- a/src/components/Home/InvitesIcon.tsx +++ b/src/components/Home/InvitesIcon.tsx @@ -1,35 +1,8 @@ import STAR_STRAIGHT_ICON from '@/assets/icons/starStraight.svg' import Image from 'next/image' -import { motion } from 'framer-motion' const InvitesIcon = () => { - return ( - - star - - ) + return star } export default InvitesIcon diff --git a/src/components/Invites/JoinWaitlistPage.tsx b/src/components/Invites/JoinWaitlistPage.tsx index 94a1ca8b0..68854660c 100644 --- a/src/components/Invites/JoinWaitlistPage.tsx +++ b/src/components/Invites/JoinWaitlistPage.tsx @@ -43,6 +43,7 @@ const JoinWaitlistPage = () => { try { const res = await invitesApi.acceptInvite(inviteCode, inviteType) if (res.success) { + sessionStorage.setItem('showNoMoreJailModal', 'true') fetchUser() } else { setError('Something went wrong. Please try again or contact support.') diff --git a/src/components/Profile/index.tsx b/src/components/Profile/index.tsx index 9ba045e83..2372ff10a 100644 --- a/src/components/Profile/index.tsx +++ b/src/components/Profile/index.tsx @@ -7,7 +7,7 @@ import NavHeader from '../Global/NavHeader' import ProfileHeader from './components/ProfileHeader' import ProfileMenuItem from './components/ProfileMenuItem' import { useRouter } from 'next/navigation' -import { checkIfInternalNavigation, generateInvitesShareText } from '@/utils' +import { checkIfInternalNavigation, generateInviteCodeLink, generateInvitesShareText } from '@/utils' import ActionModal from '../Global/ActionModal' import { useState } from 'react' import useKycStatus from '@/hooks/useKycStatus' @@ -31,8 +31,7 @@ export const Profile = () => { const fullName = user?.user.fullName || user?.user?.username || 'Anonymous User' const username = user?.user.username || 'anonymous' - const inviteCode = `${user?.user.username?.toUpperCase()}INVITESYOU` - const inviteLink = `${process.env.NEXT_PUBLIC_BASE_URL}/invite?code=${inviteCode}` + const inviteData = generateInviteCodeLink(user?.user.username ?? '') return (
@@ -162,13 +161,13 @@ export const Profile = () => { <>
-

{`${inviteCode}`}

+

{`${inviteData.inviteCode}`}

- +
Promise.resolve(generateInvitesShareText(inviteLink))} + generateText={() => Promise.resolve(generateInvitesShareText(inviteData.inviteLink))} title="Share your invite link" > Share Invite link diff --git a/src/components/Setup/Views/JoinWaitlist.tsx b/src/components/Setup/Views/JoinWaitlist.tsx index 5c6bf5ef2..8a8ed3ccb 100644 --- a/src/components/Setup/Views/JoinWaitlist.tsx +++ b/src/components/Setup/Views/JoinWaitlist.tsx @@ -4,7 +4,7 @@ import { Button } from '@/components/0_Bruddle' import { useToast } from '@/components/0_Bruddle/Toast' import ValidatedInput from '@/components/Global/ValidatedInput' import { useZeroDev } from '@/hooks/useZeroDev' -import { useState } from 'react' +import { useEffect, useState } from 'react' import { twMerge } from 'tailwind-merge' import * as Sentry from '@sentry/nextjs' import { useSetupFlow } from '@/hooks/useSetupFlow' @@ -14,6 +14,7 @@ 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' const JoinWaitlist = () => { const [inviteCode, setInviteCode] = useState('') @@ -28,6 +29,7 @@ const JoinWaitlist = () => { const dispatch = useAppDispatch() const router = useRouter() const searchParams = useSearchParams() + const { user } = useAuth() const validateInviteCode = async (inviteCode: string): Promise => { try { @@ -61,6 +63,14 @@ const JoinWaitlist = () => { const onLoginClick = async () => { try { await handleLogin() + } 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) { @@ -77,10 +87,8 @@ const JoinWaitlist = () => { } else { router.push('/home') } - } catch (e) { - handleError(e) } - } + }, [user, router, searchParams]) return (
diff --git a/src/interfaces/interfaces.ts b/src/interfaces/interfaces.ts index 2401b49d6..9be06ac66 100644 --- a/src/interfaces/interfaces.ts +++ b/src/interfaces/interfaces.ts @@ -326,6 +326,7 @@ export interface IUserProfile { }> totalReferralPoints: number invitesSent: userInvites[] + showEarlyUserModal: boolean } interface Contact { diff --git a/src/utils/general.utils.ts b/src/utils/general.utils.ts index cd3366add..97242166f 100644 --- a/src/utils/general.utils.ts +++ b/src/utils/general.utils.ts @@ -1323,3 +1323,9 @@ export function slugify(text: string): string { export const generateInvitesShareText = (inviteLink: string) => { return `I’m using Peanut, an invite-only app for easy payments. With it you can pay friends, use merchants, and move money in and out of your bank, even cross-border. Here’s my invite: ${inviteLink}` } + +export const generateInviteCodeLink = (username: string) => { + const inviteCode = `${username.toUpperCase()}INVITESYOU` + const inviteLink = `${consts.BASE_URL}/invite?code=${inviteCode}` + return { inviteLink, inviteCode } +} diff --git a/tailwind.config.js b/tailwind.config.js index 9c52f8e83..d251d8126 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -174,6 +174,12 @@ module.exports = { '50%': { transform: 'scale(1.05)', opacity: '1' }, '100%': { transform: 'scale(0.95)', opacity: '0.8' }, }, + pulsateDeep: { + '0%': { transform: 'scale(1)', opacity: '1' }, + '30%': { transform: 'scale(0.7)', opacity: '1' }, + '60%': { transform: 'scale(1)', opacity: '1' }, + '100%': { transform: 'scale(1)', opacity: '1' }, + }, 'pulse-strong': { '0%': { opacity: '1' }, '50%': { opacity: '0.3' }, @@ -184,6 +190,7 @@ module.exports = { colorPulse: 'colorPulse 2.5s cubic-bezier(0.4, 0, 0.6, 1) infinite', fadeIn: 'fadeIn 0.3s ease-in-out', pulsate: 'pulsate 1.5s ease-in-out infinite', + 'pulsate-slow': 'pulsateDeep 4s ease-in-out infinite', 'pulse-strong': 'pulse-strong 1s ease-in-out infinite', }, opacity: {