From 74890e251dee3e0f195583431cb48b9f3a58ecc9 Mon Sep 17 00:00:00 2001 From: Max Nanis Date: Mon, 9 Jun 2025 16:05:52 +0700 Subject: Cashout Methods page: adding walletSlice and cashoutmethodsSlice so they're in the stored state. Iterating with fix vs variable filters. Pulling old validators from old code and setting up the wallet fetch. --- src/Widget.tsx | 22 ++++- src/components/app-sidebar.tsx | 31 ++++-- src/models/app.ts | 2 +- src/models/cashoutMethodSlice.ts | 78 +++++++++++++++ src/models/walletSlice.ts | 23 +++++ src/pages/CashoutMethods.tsx | 207 ++++++++++++++++++++++++++++++++++----- src/pages/Offerwall.tsx | 89 +++++++++-------- src/store.ts | 7 +- 8 files changed, 383 insertions(+), 76 deletions(-) create mode 100644 src/models/cashoutMethodSlice.ts create mode 100644 src/models/walletSlice.ts diff --git a/src/Widget.tsx b/src/Widget.tsx index 3f7902c..f355194 100644 --- a/src/Widget.tsx +++ b/src/Widget.tsx @@ -6,9 +6,11 @@ import {Offerwall} from "@/pages/Offerwall.tsx" import {QuestionsPage} from "@/pages/Questions.tsx"; import {useAppDispatch, useAppSelector} from "@/hooks.ts"; -import {OfferwallApi, ProfilingQuestionsApi} from "@/api"; +import {CashoutMethodOut, OfferwallApi, ProfilingQuestionsApi, UserWalletBalance, WalletApi} from "@/api"; import {ProfileQuestion, setQuestions} from "@/models/questionSlice.ts"; import {setBuckets} from "@/models/bucketSlice.ts"; +import {setCashoutMethods} from "@/models/cashoutMethodSlice.ts"; +import {setWallet} from "@/models/walletSlice.ts" import {CashoutMethodsPage} from "@/pages/CashoutMethods.tsx"; import {setAvailabilityCount, setOfferwallId} from "@/models/appSlice.ts" @@ -24,7 +26,6 @@ const Widget = () => { // https://fsb.generalresearch.com/{product_id}/offerwall/37d1da64/?country new OfferwallApi().offerwallSoftpairProductIdOfferwall37d1da64Get(app.bpid, app.bpuid, "104.9.125.144") .then(res => { - // We want to set these questions first, because the Bucket Component views may do // some redux lookups const objects: ProfileQuestion[] = Object.values(res.data.offerwall.question_info) as ProfileQuestion[] @@ -41,6 +42,21 @@ const Widget = () => { dispatch(setQuestions(res.data.questions as ProfileQuestion[])) }) .catch(err => console.log(err)); + + new WalletApi().getCashoutMethodsProductIdCashoutMethodsGet(app.bpid, app.bpuid) + .then(res => { + dispatch(setCashoutMethods(res.data.cashout_methods as CashoutMethodOut[])) + }) + .catch(err => console.log(err)) + + new WalletApi().getUserWalletBalanceProductIdWalletGet(app.bpid, app.bpuid) + .then(res => { + dispatch(setWallet(res.data.wallet as UserWalletBalance)) + }) + .catch(err => { + // TODO: Wallet mode is likely off + }) + }, []); // ← empty array means "run once" @@ -56,7 +72,7 @@ const Widget = () => {
{app.currentPage === 'offerwall' && } {app.currentPage === 'questions' && } - {app.currentPage === 'cashouts' && } + {app.currentPage === 'cashout_methods' && }
diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx index e8cbd4c..f314c9f 100644 --- a/src/components/app-sidebar.tsx +++ b/src/components/app-sidebar.tsx @@ -1,13 +1,12 @@ "use client" import * as React from "react" -import {CircleDollarSign, MessageCircle, SquareStack} from "lucide-react" +import {CircleDollarSign, SquareStack} from "lucide-react" import {NavMain} from "@/components/nav-main" import { Sidebar, SidebarContent, - SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, @@ -17,10 +16,16 @@ import { SidebarMenuItem, useSidebar, } from "@/components/ui/sidebar" -import {useAppSelector} from "@/hooks.ts"; +import {useAppDispatch, useAppSelector} from "@/hooks.ts"; +import {setPage} from "@/models/appSlice.ts"; +import {Badge} from "@/components/ui/badge.tsx"; +import {useSelector} from "react-redux"; +import {selectCashoutMethods} from "@/models/cashoutMethodSlice.ts"; export function AppSidebar({...props}: React.ComponentProps) { const app = useAppSelector(state => state.app) + const dispatch = useAppDispatch() + const cashoutMethods = useSelector(selectCashoutMethods) const {isMobile} = useSidebar() @@ -50,12 +55,24 @@ export function AppSidebar({...props}: React.ComponentProps) { - + - + dispatch(setPage("cashout_methods"))} + > - Methods - + + Methods {cashoutMethods.length.toLocaleString()} + + + diff --git a/src/models/app.ts b/src/models/app.ts index 62da7bb..25b2412 100644 --- a/src/models/app.ts +++ b/src/models/app.ts @@ -1,4 +1,4 @@ -export type Page = 'offerwall' | 'questions' | 'cashouts'; +export type Page = 'offerwall' | 'questions' | 'cashout_methods'; export interface App { targetId: string, diff --git a/src/models/cashoutMethodSlice.ts b/src/models/cashoutMethodSlice.ts new file mode 100644 index 0000000..a6d0211 --- /dev/null +++ b/src/models/cashoutMethodSlice.ts @@ -0,0 +1,78 @@ +import {createSlice, PayloadAction} from '@reduxjs/toolkit' +import type {RootState} from '@/store' +import {CashoutMethodOut} from "@/api"; + + +const initialState: CashoutMethodOut[] = [] + +const cashoutMethodSlice = createSlice({ + name: 'cashoutMethods', + initialState, + reducers: { + setCashoutMethods(state, action: PayloadAction) { + return action.payload; + }, + redeem: { + // let res = {'status': false, 'msg': ''}; + // let cashout_method = this.collection.getCashoutMethod(); + // let req_amt = +this.ui.amount.val(); + // + // // Generic checks + // if (!cashout_method) { + // res['msg'] = "Cashout method not selected"; + // return res + // } + // + // if (isNaN(req_amt)) { + // res['msg'] = "Invalid amount (numbers only)"; + // return res; + // } + // + // if (!this.WALLET) { + // res['msg'] = "Unknown wallet balance"; + // return res + // } + // + // let balance: number = this.WALLET.get("redeemable_amount"); + // + // // Limit checks + // if (balance < cashout_method.get("min_value")) { + // res["msg"] = "Wallet balance not large enough"; + // return res; + // } + // + // let req_amount = this.getIntCentsValue() + // + // if (req_amount < cashout_method.get("min_value")) { + // res["msg"] = "Requested amount not large enough"; + // return res; + // } + // + // if (req_amount > cashout_method.get("max_value")) { + // res["msg"] = "Amount too large for payout method"; + // return res; + // } + // + // if (req_amount > balance) { + // res["msg"] = "Amount is more than wallet balance"; + // return res; + // } + // + // res["status"] = true; + // return res; + // }, + } + } +}) + +export const { + setCashoutMethods, +} = cashoutMethodSlice.actions; +export default cashoutMethodSlice.reducer + +export const selectCashoutMethods = (state: RootState) => state.cashoutMethods + +export const selectFixedCashoutMethods = (state: RootState) => + state.cashoutMethods.filter(cm => cm.data.value_type === "fixed") +export const selectVariableCashoutMethods = (state: RootState) => + state.cashoutMethods.filter(cm => cm.data.value_type === "variable") diff --git a/src/models/walletSlice.ts b/src/models/walletSlice.ts new file mode 100644 index 0000000..83eda62 --- /dev/null +++ b/src/models/walletSlice.ts @@ -0,0 +1,23 @@ +import {createSlice, PayloadAction} from '@reduxjs/toolkit' + +import {UserWalletBalance} from "@/api"; + + +const initialState: UserWalletBalance = {}; + + +const walletSlice = createSlice({ + name: 'wallet', + initialState, + reducers: { + setWallet(state, action: PayloadAction) { + return action.payload; + } + } +}) + +export const { + setWallet, +} = walletSlice.actions; +export default walletSlice.reducer + diff --git a/src/pages/CashoutMethods.tsx b/src/pages/CashoutMethods.tsx index 9656087..fca6af0 100644 --- a/src/pages/CashoutMethods.tsx +++ b/src/pages/CashoutMethods.tsx @@ -1,45 +1,202 @@ -import React, {useEffect, useState} from 'react' +import React, {useState} from 'react' +import {Card, CardContent, CardHeader,} from "@/components/ui/card" +import {Tabs, TabsContent, TabsList, TabsTrigger,} from "@/components/ui/tabs" +import {Badge} from "@/components/ui/badge" + +import {selectFixedCashoutMethods, selectVariableCashoutMethods} from "@/models/cashoutMethodSlice.ts"; +import {CashoutMethodOut, UserWalletBalance} from "@/api" +import {formatCentsToUSD} from "@/lib/utils.ts"; +import {useSelector} from 'react-redux' +import {motion} from "framer-motion"; +import {Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle,} from "@/components/ui/drawer" +import {useAppSelector} from "@/hooks.ts"; + +const CashoutAcknowledgement = () => { + return ( + <> +

Your request has been successfully submitted!

+
+ + + Request Status: + Pending +
+

Your redemption link will be on the history page.

+ + ) +} + +const CashoutReview: React.FC<{ cashout_method: CashoutMethodOut }> = ({cashout_method}) => { + return ( + <> +

You are about to redeem to a card.

+ ... +

{cashout_method.name}

+ {/*

{data.terms}

*/} + + +

+ + ) +} + +const VariableCashoutMethodPreview: React.FC<{ cashout_method: CashoutMethodOut }> = ({cashout_method}) => { + return ( + <> + + {cashout_method.name} + + + + {formatCentsToUSD(cashout_method.min_value)} – {formatCentsToUSD(cashout_method.max_value)} + + + ) +} + +const FixedCashoutMethodPreview: React.FC<{ cashout_method: CashoutMethodOut }> = ({cashout_method}) => { + return ( + <> + + {cashout_method.name} + + + ) +} -import {Card, CardContent, CardHeader} from "@/components/ui/card.tsx"; -import {CashoutMethodOut, CashoutMethodsResponse, WalletApi} from "@/api" const CashoutMethodPreview: React.FC<{ cashout_method: CashoutMethodOut }> = ({cashout_method}) => { + const [open, setOpen] = useState(false) + + const renderContent = () => { + switch (cashout_method.data.value_type) { + case 'fixed': + return + case 'variable': + return + } + }; + return ( - + - {cashout_method.name} + {renderContent()} - + Cashout method + + setOpen(true)} + >Details + + + + + + {cashout_method.name} Details + +
+ + + + + ) } -const CashoutMethodsPage: React.FC = ({settings}) => { - const [cashoutMethods, setCashoutMethods] = useState([]); - useEffect(() => { - const x = new WalletApi(); - x.getCashoutMethodsProductIdCashoutMethodsGet(settings.bpid, settings.bpuid) - .then(res => { - const data: CashoutMethodsResponse = res.data; - setCashoutMethods(data.cashout_methods); - }) - .catch(err => console.log(err)); - }, []); // ← empty array means "run once" +const CashoutMethodsPage = () => { + const variableCashoutMethods = useSelector(selectVariableCashoutMethods) + const fixedCashoutMethods = useSelector(selectFixedCashoutMethods) + const wallet: UserWalletBalance = useAppSelector(state => state.wallet) + return ( -
- { - cashoutMethods.map((m, index) => { - return ; - }) - } -
- ); + <> +

Your balance is {formatCentsToUSD(wallet.amount)}.

+

You can redeem {formatCentsToUSD(wallet.redeemable_amount)} now.

+

(a portion of each survey is delayed by 30 days)

+ + + + Variable + Fixed + + + +
+ { + variableCashoutMethods.map((m, index) => { + return ; + }) + } +
+
+ +
+ { + fixedCashoutMethods.map((m, index) => { + return ; + }) + } +
+
+
+ + ) } + export {CashoutMethodsPage} \ No newline at end of file diff --git a/src/pages/Offerwall.tsx b/src/pages/Offerwall.tsx index 0d0534e..19c4515 100644 --- a/src/pages/Offerwall.tsx +++ b/src/pages/Offerwall.tsx @@ -1,5 +1,5 @@ import React, {useState} from 'react' -import { motion } from "framer-motion"; +import {motion} from "framer-motion"; import {Badge} from "@/components/ui/badge" import {Link} from '@mui/material'; import {Tabs, TabsContent} from "@/components/ui/tabs" @@ -18,14 +18,13 @@ import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from "@/ import {useSelector} from "react-redux"; import {Switch} from "@/components/ui/switch" import {Card, CardContent, CardFooter} from "@/components/ui/card.tsx"; -import {makeSelectQuestionsByIds, ProfileQuestion} from "@/models/questionSlice.ts" +import {makeSelectQuestionsByIds} from "@/models/questionSlice.ts" import {CheckIcon, MessageCircleQuestionIcon, XIcon} from "lucide-react" import {useAppDispatch, useAppSelector} from '@/hooks' import {Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger,} from "@/components/ui/sheet" import {ProfileQuestionFull} from "@/pages/Questions.tsx" import {Answer, selectAnswerForQuestion, submitAnswer} from "@/models/answerSlice.ts"; import {assert, formatCentsToUSD, formatSeconds} from "@/lib/utils.ts"; -import {IQRBoxPlot} from "@/lib/snippets.tsx" import {setAvailabilityCount, setOfferwallId} from "@/models/appSlice.ts"; import {setBuckets} from "@/models/bucketSlice.ts"; @@ -54,12 +53,12 @@ const ContentsGrid: React.FC = ({bucket}) => { { accessorKey: "loi", header: "Length", - cell: ({ getValue }) => formatSeconds(getValue() as number), + cell: ({getValue}) => formatSeconds(getValue() as number), }, { accessorKey: "payout", header: "Payout", - cell: ({ getValue}) => formatCentsToUSD(getValue() as number) + cell: ({getValue}) => formatCentsToUSD(getValue() as number) }, ] @@ -73,7 +72,7 @@ const ContentsGrid: React.FC = ({bucket}) => { return (
- +
{table.getHeaderGroups().map((headerGroup) => ( @@ -123,6 +122,7 @@ const ConditionalQuestions: React.FC = ({bucket}) => { const app = useAppSelector(state => state.app) console.log("Conditional bucket:", questions, question, answer) + const [open, setOpen] = useState(false) const submitEvt = () => { dispatch(submitAnswer({question: question})) @@ -153,32 +153,41 @@ const ConditionalQuestions: React.FC = ({bucket}) => { } return ( - - Open - - - - Bucket Questions - - This survey has some unanswered questions. Answer these to determine if you're - eligible for the Survey Bucket - - - - { - questions.map(q => { - return - }) - } + <> + + + + + + + Bucket Questions + + This survey has some unanswered questions. Answer these to determine if you're + eligible for the Survey Bucket + + - - + { + questions.map(q => { + return + }) + } + + + + ) } @@ -201,22 +210,24 @@ const CallToAction: React.FC = ({bucket}) => { ) case "conditional": - // return + return ( +
+ +
+ ) + + case "ineligible": return (
) - - case "ineligible": - return ; } } diff --git a/src/store.ts b/src/store.ts index 4e56b39..cfa64ec 100644 --- a/src/store.ts +++ b/src/store.ts @@ -4,6 +4,8 @@ import bucketReducers from "@/models/bucketSlice.ts" import questionReducers from "@/models/questionSlice.ts" import appReducers from "@/models/appSlice.ts" import answerReducers from "@/models/answerSlice.ts" +import cashoutMethodReducers from "@/models/cashoutMethodSlice.ts" +import walletReducers from "@/models/walletSlice.ts" export const store = configureStore({ reducer: { @@ -16,7 +18,10 @@ export const store = configureStore({ // - Read Write // -- This stores user engagement (eg: answering any questions) - answers: answerReducers + answers: answerReducers, + + cashoutMethods: cashoutMethodReducers, + wallet: walletReducers } }) -- cgit v1.2.3