Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancements to Steam Linking, Premium Checkout, and UI Updates #94

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 50 additions & 2 deletions app/auth/authenticator.server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @/app/auth/authenticator.server.ts
import { commitSession, getSession } from './storage.server'

const apiBase = 'https://api.imperfectgamers.org'
export const apiBase = 'https://api.imperfectgamers.org'

type User = {
email: string
Expand Down Expand Up @@ -116,8 +116,20 @@ export async function login(request: Request) {
session.set('email', user.data.email)

const hasSteamAccount = await checkSteamAccount(user.data?.userToken)

if (hasSteamAccount.hasSteam) {
session.set('steamId', hasSteamAccount.steamId ?? undefined)

// Check if the user is a premium member
const isPremium = await checkPremiumStatus(
user.data.userToken,
user.data.uid,
) // Assuming this function exists
session.set('isPremium', isPremium)

if (isPremium) {
// Additional logic for premium users can be handled here
}
}

const onboardingDetails = await checkOnboarded(user.data?.userToken)
Expand All @@ -139,6 +151,42 @@ export async function login(request: Request) {
}
}

/**
* Checks if the user with the given token is a premium member.
* @param token - The user's authentication token.
* @param uid - The user's ID.
* @returns A promise that resolves to a boolean indicating whether the user is premium.
*/
export async function checkPremiumStatus(
token: string,
uid: number,
): Promise<boolean> {
try {
const response = await fetch(`${apiBase}/premium/status/${uid}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: token,
},
})

if (!response.ok) {
// The API sends a message in the response on error
const errorData = await response.json()
throw new Error(errorData.message || 'Error checking premium status.')
}

const data = await response.json()
return data.is_premium
} catch (error) {
console.error(
'Error checking premium status:',
error || 'Error checking premium status.',
)
return false // Default to non-premium if there's an error
}
}

/**
* Registers a user by sending a POST request to the API with the provided email and password.
* @param email - The email of the user to register.
Expand Down Expand Up @@ -200,7 +248,7 @@ export async function logout(token: string) {
*/
async function checkSteamAccount(
token: string,
): Promise<{ status: string; hasSteam: boolean; steamId: number | null }> {
): Promise<{ status: string; hasSteam: boolean; steamId: string | null }> {
try {
// Send the request to API
const response = await fetch(`${apiBase}/user/verifySteam`, {
Expand Down
3 changes: 2 additions & 1 deletion app/auth/storage.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import { z } from 'zod'
type SessionData = {
uid?: number
userToken?: string
steamId?: number
steamId?: string
email?: string
username?: string
isPremium?: boolean // Track premium status
}

export const sessionStorage = createCookieSessionStorage<SessionData>({
Expand Down
16 changes: 13 additions & 3 deletions app/components/atoms/PriceLabel/PriceLabel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Atoms/PriceLabel/PriceLabel.tsx
import { useLoaderData } from '@remix-run/react'
import { useEffect, useState } from 'react'
import type { LoaderData } from '~/routes/_index'
import price from './PriceLabel.module.css'

/**
Expand All @@ -13,16 +15,20 @@ import price from './PriceLabel.module.css'
* @returns {JSX.Element} The rendered price label component.
*/
export const PriceLabel = ({ isYearly }: { isYearly: boolean }) => {
const { isPremium, isAuthenticated, isSteamLinked, username } =
useLoaderData<LoaderData>()

const isMember = isAuthenticated && username && isSteamLinked && isPremium
const [animationClass, setAnimationClass] = useState('')
const [priceText, setPriceText] = useState(
isYearly ? '$200/year - Coming soon!' : '$12/month',
isYearly ? '$120/year - Coming soon!' : '$12/month',
)

useEffect(() => {
setAnimationClass(price.label_change) // Trigger the animation
const textTimer = setTimeout(() => {
// Update the text in the middle of the animation
setPriceText(isYearly ? '$200/year - Coming soon!' : '$12/month')
setPriceText(isYearly ? '$120/year - Coming soon!' : '$12/month')
}, 300) // Half the duration of your CSS animation
const animationTimer = setTimeout(() => {
setAnimationClass('') // Reset animation class after it completes
Expand All @@ -33,5 +39,9 @@ export const PriceLabel = ({ isYearly }: { isYearly: boolean }) => {
}
}, [isYearly])

return <p className={`${price.label} mt-2 ${animationClass}`}>{priceText}</p>
return (
<p className={`${price.label} mt-2 ${animationClass}`}>
{isMember ? username : priceText}
</p>
)
}
35 changes: 26 additions & 9 deletions app/components/molecules/AuthorizeForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import { useEffect, useRef, useState } from 'react'
import { wait } from 'remix-utils/timers'
import { CloseInterceptReason } from '../organism/ModalWrapper/ModalWrapper'
import MessageContainer from '../pending/MessageContainer'
import { useDispatch, useDispatchState } from '../pending/ProcessProvider'

interface AuthorizeFormProps {
setCloseInterceptReason?: (reason: CloseInterceptReason) => void
Expand All @@ -28,6 +30,12 @@
const timerRef = useRef<NodeJS.Timeout | null>(null)
const transitionRef = useRef<HTMLDivElement | null>(null)

const dispatch = useDispatch()
const state = useDispatchState()

const currentDispatch = state.find(dispatch => dispatch.inProgress)
const errorMessage = currentDispatch?.message

useEffect(() => {
if (visible) {
timerRef.current = setTimeout(() => {
Expand Down Expand Up @@ -68,6 +76,15 @@
}
}

const resetForm = () => {
setVisible(false)
setSteamPopup(null)
setSteamPopupOpened(false)
setPopupWindow?.(null)
setShowFallback(false)
setCloseInterceptReason?.(CloseInterceptReason.None)
}

useEffect(() => {
if (setCloseInterceptReason) {
setCloseInterceptReason(CloseInterceptReason.None)
Expand All @@ -86,12 +103,11 @@
event.data.message,
)
console.log('[AuthorizeForm.tsx] Steam Authentication Flow: Popup')
// Handle error and reset form
dispatch.send(event.data.message)
resetForm()
// TODO: implement toast in future during ui/ux enhancement related tasks
// alert(`Steam authentication error: ${event.data.message}`)
setSteamPopup(null)
setSteamPopupOpened(false)

setCloseInterceptReason?.(CloseInterceptReason.None)
} else {
if (event.data.type) {
console.log('[AuthorizeForm.tsx] Event was unknown:', event.data.type) // To see exactly which unknown event was received
Expand All @@ -112,15 +128,13 @@
console.error(
'[AuthorizeForm.tsx] Error during Steam authentication from redirect.',
)
console.log('[AuthorizeForm.tsx] Steam Authentication Flow: Popup')

console.log('[AuthorizeForm.tsx] Steam Authentication Flow: Redirect')
// console.error('[AuthorizeForm.tsx] Error during Steam authentication:', event.data.message)
// TODO: implement toast in future during ui/ux enhancement related tasks
// TODO: Update redirect flash to include error message from callback
// alert(`Steam authentication error: ${event.data.message}`)
setSteamPopup(null)
setSteamPopupOpened(false)
setCloseInterceptReason?.(CloseInterceptReason.None)
dispatch.send('Error during Steam authentication from redirect.')
resetForm()
} else {
console.log('[AuthorizeForm.tsx] Event was unknown:', event.type) // To see exactly which unknown event was received
}
Expand All @@ -133,7 +147,7 @@
window.removeEventListener('message', handleMessage)
window.removeEventListener('event', handleEvent)
}
}, [])

Check warning on line 150 in app/components/molecules/AuthorizeForm.tsx

View workflow job for this annotation

GitHub Actions / Check Lint and Format

React Hook useEffect has missing dependencies: 'callback', 'dispatch', 'resetForm', and 'setCloseInterceptReason'. Either include them or remove the dependency array. If 'setCloseInterceptReason' changes too often, find the parent component that defines it and wrap that definition in useCallback

const initiateSteamLinking = async () => {
setVisible(true)
Expand Down Expand Up @@ -198,7 +212,7 @@
clearInterval(interval)
}
}
}, [steamPopupOpened, steamPopup, setCloseInterceptReason, setPopupWindow])

Check warning on line 215 in app/components/molecules/AuthorizeForm.tsx

View workflow job for this annotation

GitHub Actions / Check Lint and Format

React Hook useEffect has a missing dependency: 'handleCancel'. Either include it or remove the dependency array

const handleFallbackClick = (
e:
Expand All @@ -222,6 +236,9 @@

return (
<div>
{errorMessage && !steamPopupOpened && !visible ? (
<MessageContainer message={errorMessage} />
) : null}
{!steamPopupOpened && !visible ? (
<div className="cta-container">
<div className="cta-text">Link your Steam account to continue?</div>
Expand Down
9 changes: 8 additions & 1 deletion app/components/molecules/PriceToggle/PriceToggle.module.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/** ~/app/components/molecules/PriceToggle/PriceToggle.module.css **/

.toggle {
display: flex;
align-items: center;
Expand All @@ -6,7 +8,12 @@
}

.toggle__label {
color: #fff;
color: #808080; /* Dark gray */
font-size: 0.9rem;
margin: 0 10px;
transition: color 0.3s ease-in-out; /* Add transition effect */
}

.toggle__label--active {
color: #ff7f7f; /* Light red */
}
14 changes: 11 additions & 3 deletions app/components/molecules/PriceToggle/PriceToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,18 @@ export const PriceToggle = ({
onToggle: () => void
}) => {
return (
<div className={price.toggle}>
<span className={price.toggle__label}>Monthly</span>
<div className={`${price.toggle} select-none`}>
<span
className={`${price.toggle__label} ${!isYearly ? price['toggle__label--active'] : ''}`}
>
Monthly
</span>
<ToggleSwitch isYearly={isYearly} onToggle={onToggle} />
<span className={price.toggle__label}>Annually</span>
<span
className={`${price.toggle__label} ${isYearly ? price['toggle__label--active'] : ''}`}
>
Annually
</span>
</div>
)
}
39 changes: 26 additions & 13 deletions app/components/organism/AuthForms/AuthForms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,14 @@ enum PageTitle {
}

const AuthForms: React.FC = () => {
const { isAuthenticated, isSteamLinked, username, flashSuccess, flashError } =
useLoaderData<LoaderData>()
const {
isPremium,
isAuthenticated,
isSteamLinked,
username,
flashSuccess,
flashError,
} = useLoaderData<LoaderData>()
const [shouldOpenModal, setShouldOpenModal] = useState(false)
const revalidator = useRevalidator()
const [shouldRenderCheckoutProcess, setShouldRenderCheckoutProcess] =
Expand Down Expand Up @@ -290,6 +296,10 @@ const AuthForms: React.FC = () => {
<UsernameForm />
) : !isSteamLinked ? (
<AuthorizeForm />
) : isPremium ? (
<div className="premium-banner-#TODO">
Welcome, Premium Member!
</div>
) : (
<CheckoutProcess />
)
Expand All @@ -301,6 +311,7 @@ const AuthForms: React.FC = () => {
username={username || undefined}
isInitial={isInitial}
isLoginForm={isLoginForm}
isPremium={isPremium}
/>
}
isResponsive={
Expand All @@ -311,39 +322,41 @@ const AuthForms: React.FC = () => {
: true
} // Set true only if showing WelcomeScreen
>
<Button>Join Now</Button>
<Button>{isPremium ? 'Manage Pass' : 'Join Now'}</Button>
</ModalWrapper>
</ProcessProvider>
</>
)
}

// Define FooterProps Interface
interface FooterProps {
isAuthenticated: boolean
username?: string
isLoginForm: boolean
isInitial: boolean
isPremium: boolean // New property to indicate premium status
}

const Footer: React.FC<FooterProps> = ({
isAuthenticated,
username,
isLoginForm,
isInitial,
isPremium, // Use the new isPremium prop
}) => (
<div className="mx-auto mt-4 flex flex-col text-sm text-white">
<div>
{isAuthenticated && username ? (
<>You are currently signed in{username ? ' as ' + username : ''}.</>
) : !isInitial && !isAuthenticated && isLoginForm ? (
{isAuthenticated && isPremium ? (
<>
{/* commented out until design decision is finalized */}
{/* Don&apos;t have an account?{' '}
<button onClick={handleNewUser} className="ml-2 underline">
Sign up
</button> */}
<p className="premium-footer">
Thank you for being a Premium Member!
</p>
<p className="premium-footer">Enjoy your exclusive benefits.</p>
</>
) : isAuthenticated && username ? (
<>You are currently signed in{username ? ' as ' + username : ''}.</>
) : !isInitial && !isAuthenticated && isLoginForm ? (
<>{/* Placeholder for future content */}</>
) : !isInitial && !isAuthenticated ? (
<>
<p className="mt-4 select-none text-xs text-stone-500">
Expand Down Expand Up @@ -375,7 +388,7 @@ const Footer: React.FC<FooterProps> = ({
Imprint
</a>
.
</p>{' '}
</p>
</>
) : isInitial && !isAuthenticated ? (
<p className="flex select-none justify-center text-xs text-stone-500">
Expand Down
Loading
Loading