{
>
-
- {/* (Click to {showRedemption ? "hide" : "show"}) */}
- Details within 24hr.
+
+ (Click to {showRedemption ? "hide" : "show"})
diff --git a/jb-ui/src/components/UserAccount.tsx b/jb-ui/src/components/UserAccount.tsx
new file mode 100644
index 0000000..5aef031
--- /dev/null
+++ b/jb-ui/src/components/UserAccount.tsx
@@ -0,0 +1,25 @@
+import { useGetMeQuery } from "@/models/authAPI";
+
+const UserAccount = () => {
+ const { data: user } = useGetMeQuery();
+
+ if (!user?.product_user_id) return null;
+
+ return (
+
+
+
Email:
+
{user.email}
+
Name:
+
{user.display_name ?? "___"}
{" "}
+
+
+ );
+};
+
+export default UserAccount;
diff --git a/jb-ui/src/lib/utils.ts b/jb-ui/src/lib/utils.ts
index 1aa59c6..d20703b 100644
--- a/jb-ui/src/lib/utils.ts
+++ b/jb-ui/src/lib/utils.ts
@@ -117,3 +117,7 @@ export function titleCase(str: string): string {
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
}
+
+export function stripPriceFromTitle(title: string): string {
+ return title.replace(/\s*\$\d+(\.\d{1,2})?\s*$/, "").trim();
+}
diff --git a/jb-ui/src/models/app.ts b/jb-ui/src/models/app.ts
index db54e0d..508afa6 100644
--- a/jb-ui/src/models/app.ts
+++ b/jb-ui/src/models/app.ts
@@ -7,6 +7,7 @@ import {
OfferwallReason,
UserLedgerWallet,
UserDisplayedWalletBalance,
+ CashoutMethodOut,
} from "@/api_fsb";
import { PaginationState } from "@tanstack/react-table";
@@ -41,7 +42,16 @@ export interface App {
userAttemptCreditWallet?: UserLedgerWallet;
// ---
+ // -- Transaction Component ---
txPagination: PaginationState;
txTotalItems?: number;
txTotalPages?: number;
+
+ // -- CashoutMethods Component ---
+ // cmPagination: PaginationState;
+ // cmTotalItems?: number;
+ // cmTotalPages?: number;
+
+ cmSelected?: CashoutMethodOut;
+ cmShowHistory?: boolean;
}
diff --git a/jb-ui/src/models/appSlice.ts b/jb-ui/src/models/appSlice.ts
index 42a776e..6cb7fc0 100644
--- a/jb-ui/src/models/appSlice.ts
+++ b/jb-ui/src/models/appSlice.ts
@@ -7,6 +7,7 @@ import {
UserDisplayedWalletBalance,
UserLedgerWallet,
OfferwallReason,
+ CashoutMethodOut,
} from "@/api_fsb";
import { App } from "@/models/app";
import { RootState } from "@/store";
@@ -37,6 +38,12 @@ const initialState: App = {
txPagination: { pageIndex: 0, pageSize: 10 },
txTotalItems: undefined,
txTotalPages: undefined,
+
+ // cmPagination: { pageIndex: 0, pageSize: 10 },
+ // cmTotalItems: undefined,
+ // cmTotalPages: undefined,
+ cmSelected: undefined,
+ cmShowHistory: false,
} as App;
const appSlice = createSlice({
@@ -144,6 +151,27 @@ const appSlice = createSlice({
setTxTotalPages(state, action: PayloadAction
) {
state.txTotalPages = action.payload;
},
+
+ // setCmPagination(state, action: PayloadAction) {
+ // console.log("setCmPagination:payload=", action.payload);
+ // state.cmPagination = action.payload;
+ // },
+
+ // setCmTotalItems(state, action: PayloadAction) {
+ // state.cmTotalItems = action.payload;
+ // },
+
+ // setCmTotalPages(state, action: PayloadAction) {
+ // state.cmTotalPages = action.payload;
+ // },
+
+ setCmSelected(state, action: PayloadAction) {
+ state.cmSelected = action.payload;
+ },
+
+ setCmShowHistory(state, action: PayloadAction) {
+ state.cmShowHistory = action.payload;
+ },
},
});
@@ -171,6 +199,12 @@ export const {
setTxPagination,
setTxTotalItems,
setTxTotalPages,
+
+ // setCmPagination,
+ // setCmTotalItems,
+ // setCmTotalPages,
+ setCmSelected,
+ setCmShowHistory,
} = appSlice.actions;
export default appSlice.reducer;
diff --git a/jb-ui/src/models/authAPI.ts b/jb-ui/src/models/authAPI.ts
index 2964d55..a3efbcb 100644
--- a/jb-ui/src/models/authAPI.ts
+++ b/jb-ui/src/models/authAPI.ts
@@ -1,5 +1,13 @@
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
import type { User } from "@/types/authApi";
+export const bpuid = import.meta.env.VITE_BPUID;
+
+// const DEV_USER: User = {
+// blocked: false,
+// display_name: null,
+// email: "demo@jamesbillings67.com",
+// product_user_id: import.meta.env.VITE_BPUID,
+// };
export const authApi = createApi({
reducerPath: "authApi",
@@ -10,7 +18,16 @@ export const authApi = createApi({
tagTypes: ["User"],
endpoints: (builder) => ({
getMe: builder.query({
- query: () => "/session/",
+ queryFn: async (_arg, _api, _extra, baseQuery) => {
+ // if (import.meta.env.DEV) {
+ // return { data: DEV_USER };
+ // }
+ const result = await baseQuery("/session/");
+ return result.data
+ ? { data: result.data as User }
+ : { error: result.error! };
+ },
+
providesTags: ["User"],
}),
// logout: builder.mutation({
@@ -22,3 +39,5 @@ export const authApi = createApi({
// export const { useGetMeQuery, useLogoutMutation } = authApi;
export const { useGetMeQuery } = authApi;
+
+//
diff --git a/jb-ui/src/models/cashoutMethodsSlice.ts b/jb-ui/src/models/cashoutMethodsSlice.ts
index 92c03da..080cb04 100644
--- a/jb-ui/src/models/cashoutMethodsSlice.ts
+++ b/jb-ui/src/models/cashoutMethodsSlice.ts
@@ -1,23 +1,69 @@
-import { CashoutMethodOut } from "@/api_fsb";
+import { WalletApi, CashoutMethodOut } from "@/api_fsb";
import type { RootState } from "@/store";
-import { createSlice, createEntityAdapter } from "@reduxjs/toolkit";
+import {
+ PayloadAction,
+ createSlice,
+ createEntityAdapter,
+} from "@reduxjs/toolkit";
+import { bpid } from "@/lib/utils";
+import { createAsyncThunk } from "@reduxjs/toolkit";
// Entity adapter for normalized storage
const cashoutMethodsAdapter = createEntityAdapter({
selectId: (model: CashoutMethodOut) => model.id!,
sortComparer: (a, b) => {
- return (b.min_value ?? 0) - (a.min_value ?? 0);
+ return (a.min_value ?? 0) - (b.min_value ?? 0);
},
});
+export const fetchCashoutMethods = createAsyncThunk(
+ "cashoutMethodsSlice/fetchCashoutMethods",
+ async (bpuid: string) => {
+ console.log("cashoutMethodsSlice/fetchCashoutMethods");
+ const res =
+ await new WalletApi().getCashoutMethodsProductIdCashoutMethodsGet(
+ bpid,
+ bpuid,
+ );
+ return res.data.cashout_methods as CashoutMethodOut[];
+ },
+);
+
+interface CashoutMethodClientState {
+ is_active?: boolean;
+ is_expanded?: boolean;
+}
+
+const withClientDefaults = (method: CashoutMethodOut): CashoutMethodEntity => ({
+ ...method,
+ is_active: false,
+ is_expanded: false,
+});
+
+type CashoutMethodEntity = CashoutMethodOut & CashoutMethodClientState;
+
const cashoutMethodsSlice = createSlice({
name: "cashoutMethods",
initialState: cashoutMethodsAdapter.getInitialState(),
reducers: {
- addCashoutMethod: cashoutMethodsAdapter.addOne,
- addCashoutMethods: cashoutMethodsAdapter.addMany,
- upsertCashoutMethod: cashoutMethodsAdapter.upsertOne, // Add or Update
+ addCashoutMethod: (state, action: PayloadAction) => {
+ cashoutMethodsAdapter.addOne(state, withClientDefaults(action.payload));
+ },
+
+ addCashoutMethods: (state, action: PayloadAction) => {
+ cashoutMethodsAdapter.addMany(
+ state,
+ action.payload.map(withClientDefaults),
+ );
+ },
+
+ upsertCashoutMethod: (state, action: PayloadAction) => {
+ cashoutMethodsAdapter.upsertOne(
+ state,
+ withClientDefaults(action.payload),
+ );
+ },
},
});
diff --git a/jb-ui/src/models/cashoutRequests.ts b/jb-ui/src/models/cashoutRequests.ts
new file mode 100644
index 0000000..985b949
--- /dev/null
+++ b/jb-ui/src/models/cashoutRequests.ts
@@ -0,0 +1,32 @@
+import { createApi, fakeBaseQuery } from "@reduxjs/toolkit/query/react";
+import { WalletApi } from "@/api_fsb";
+import type { CashoutRequestSummary } from "@/api_fsb";
+
+const walletApi = new WalletApi();
+
+// //cashout/
+
+export const cashoutRequests = createApi({
+ reducerPath: "cashoutRequests",
+ baseQuery: fakeBaseQuery(),
+ endpoints: (builder) => ({
+ getUserCashouts: builder.query<
+ CashoutRequestSummary[],
+ { bpid: string; productUserId: string }
+ >({
+ queryFn: async ({ bpid, productUserId }) => {
+ try {
+ const res = await walletApi.filterUserCashoutsProductIdCashoutGet(
+ bpid,
+ productUserId,
+ );
+ return { data: res.data.cashouts as CashoutRequestSummary[] };
+ } catch (e) {
+ return { error: e instanceof Error ? e.message : "Request failed" };
+ }
+ },
+ }),
+ }),
+});
+
+export const { useGetUserCashoutsQuery } = cashoutRequests;
diff --git a/jb-ui/src/pages/MagicLink.tsx b/jb-ui/src/pages/MagicLink.tsx
index 48d7dc5..e85bac3 100644
--- a/jb-ui/src/pages/MagicLink.tsx
+++ b/jb-ui/src/pages/MagicLink.tsx
@@ -5,6 +5,7 @@ type ExchangeState = "working" | "failed" | "missing";
const MagicLink = function () {
const started = useRef(false);
const [state, setState] = useState("working");
+ const [errorMsg, setErrorMesg] = useState("");
useEffect(() => {
// React Strict Mode runs effects twice in development. Never redeem twice.
@@ -31,14 +32,19 @@ const MagicLink = function () {
if (!response.ok) throw new Error("Magic-link exchange failed");
window.location.replace("/");
})
- .catch(() => setState("failed"));
+ .catch((err) => {
+ const response = err?.response?.data;
+ setErrorMesg(response?.detail ?? err.message);
+ setState("failed");
+ });
}, []);
if (state === "missing") {
return This login link is missing its token.
;
}
if (state === "failed") {
- return This login link is invalid or has expired.
;
+ return {errorMsg}
;
+ // return This login link is invalid or has expired.
;
}
return Signing you in…
;
};
diff --git a/jb-ui/src/pages/MagicWallet.tsx b/jb-ui/src/pages/MagicWallet.tsx
new file mode 100644
index 0000000..21331f9
--- /dev/null
+++ b/jb-ui/src/pages/MagicWallet.tsx
@@ -0,0 +1,58 @@
+import {
+ WalletApi as JBApi,
+ CashoutConfirmation,
+ Configuration,
+} from "@/api_jb";
+import { useEffect, useRef, useState } from "react";
+
+type ExchangeState = "working" | "failed" | "missing";
+
+const MagicWallet = function () {
+ const started = useRef(false);
+ const [state, setState] = useState("working");
+ const [errorMsg, setErrorMesg] = useState("");
+
+ useEffect(() => {
+ // React Strict Mode runs effects twice in development. Never redeem twice.
+ if (started.current) return;
+ started.current = true;
+
+ const params = new URLSearchParams(window.location.search);
+ const token = params.get("token");
+ if (!token) {
+ setState("missing");
+ return;
+ }
+
+ // Keep the credential out of browser history and subsequent Referer headers.
+ window.history.replaceState({}, "", window.location.pathname);
+
+ const cc = {
+ token: token,
+ } as CashoutConfirmation;
+
+ const config = new Configuration({ basePath: "http://localhost:5174" });
+ new JBApi(config)
+ .confirmCashoutWalletCashoutConfirmPost(cc)
+ .then((res) => {
+ console.log("confirmCashoutWalletCashoutConfirmPost", res.data);
+ // if (!res.ok) throw new Error("Magic-link exchange failed");
+ window.location.replace("/");
+ })
+ .catch((err) => {
+ const response = err?.response?.data;
+ setErrorMesg(response?.detail ?? err.message);
+ setState("failed");
+ });
+ }, []);
+
+ if (state === "missing") {
+ return This login link is missing its token.
;
+ }
+ if (state === "failed") {
+ return {errorMsg}
;
+ }
+ return Signing you in…
;
+};
+
+export default MagicWallet;
diff --git a/jb-ui/src/pages/Work.tsx b/jb-ui/src/pages/Work.tsx
index 6d0b1c1..c039436 100644
--- a/jb-ui/src/pages/Work.tsx
+++ b/jb-ui/src/pages/Work.tsx
@@ -782,10 +782,13 @@ const SurveyEnter = () => {
-
- {bucket.eligibility_explanation}
-
+
+ {bucket.eligibility_explanation}
>
);
diff --git a/jb-ui/src/store.ts b/jb-ui/src/store.ts
index 081de9f..c143999 100644
--- a/jb-ui/src/store.ts
+++ b/jb-ui/src/store.ts
@@ -7,6 +7,7 @@ import profilingQuestionsReducers from "@/models/profilingQuestionsSlice";
import cashoutMethodReducers from "@/models/cashoutMethodsSlice";
import type { Action, ThunkAction } from "@reduxjs/toolkit";
import { configureStore } from "@reduxjs/toolkit";
+import { cashoutRequests } from "@/models/cashoutRequests";
export const store = configureStore({
reducer: {
@@ -17,8 +18,10 @@ export const store = configureStore({
events: grlEventsReducers,
profilingQuestions: profilingQuestionsReducers,
cashoutMethods: cashoutMethodReducers,
+ cashoutRequests: cashoutRequests.reducer,
},
- middleware: (getDefault) => getDefault().concat(authApi.middleware),
+ middleware: (getDefault) =>
+ getDefault().concat(authApi.middleware).concat(cashoutRequests.middleware),
});
// Infer the type of `store`
diff --git a/jb-ui/src/vite-env.d.ts b/jb-ui/src/vite-env.d.ts
index 93dc3cc..2e257f0 100644
--- a/jb-ui/src/vite-env.d.ts
+++ b/jb-ui/src/vite-env.d.ts
@@ -9,6 +9,7 @@ interface ViteTypeOptions {
interface ImportMetaEnv {
readonly VITE_BASENAME: string;
readonly VITE_BPID: string;
+ readonly VITE_BPUID: string;
readonly VITE_TAG_MANAGER: string;
}
diff --git a/jb-ui/vite.config.ts b/jb-ui/vite.config.ts
index 41f1d9e..b8bc20d 100644
--- a/jb-ui/vite.config.ts
+++ b/jb-ui/vite.config.ts
@@ -28,10 +28,23 @@ export default defineConfig({
// This forwards requests to the FastAPI development server
// that must also be running
proxy: {
- "^/(auth|status|statuses|report|survey)(/|$)": {
+ "^/(wallet/cashout/request|auth|status|statuses|report|survey)(/|$)": {
target: "http://localhost:8000",
changeOrigin: true,
},
+
+ // GET -> React, everything else -> FastAPI
+ "^/wallet/cashout/confirm(/|$)": {
+ target: "http://localhost:8000",
+ changeOrigin: true,
+ bypass(req) {
+ if (req.method === "GET") {
+ return "/index.html";
+ }
+
+ return undefined;
+ },
+ },
},
},
diff --git a/scripts/__init__.py b/scripts/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/scripts/unreject.py b/scripts/unreject.py
deleted file mode 100644
index 298561f..0000000
--- a/scripts/unreject.py
+++ /dev/null
@@ -1,26 +0,0 @@
-from jb.decorators import AMTM, AM
-from jb.flow.assignment_tasks import approve_assignment
-from jb.managers.amt import APPROVAL_MESSAGE
-
-res = AM.pg_config.execute_sql_query(
- """
-SELECT amt_assignment_id, submit_time, h.amt_hit_id, ht.amt_hit_type_id
-FROM mtwerk_assignment AS a
-LEFT JOIN mtwerk_hit AS h ON h.id = a.hit_id
-LEFT JOIN mtwerk_hittype AS ht ON h.hit_type_id = ht.id
-WHERE amt_worker_id='A1FMSKDAIK80PQ'
-and a.status = 2
-AND a.created_at>'2026-04-21 00:20:54.709322'
-ORDER BY a.created_at;
-"""
-)
-
-for x in res:
- approve_assignment(
- amtm=AMTM,
- am=AM,
- amt_assignment_id=x['amt_assignment_id'],
- msg=APPROVAL_MESSAGE,
- amt_hit_type_id=x['amt_hit_type_id'],
- override_rejection=True
- )
diff --git a/scripts/wallet.ipynb b/scripts/wallet.ipynb
deleted file mode 100644
index 12a90be..0000000
--- a/scripts/wallet.ipynb
+++ /dev/null
@@ -1,153 +0,0 @@
-{
- "cells": [
- {
- "metadata": {},
- "cell_type": "code",
- "source": [
- "import uuid\n",
- "\n",
- "import requests\n",
- "from collections import Counter\n",
- "\n",
- "FSB_BASE_URL = \"https://fsb.generalresearch.com/\"\n",
- "JB_BASE_URL = \"https://jamesbillings67.com/\"\n",
- "product_id = \"888dbc589987425fa846d6e2a8daed04\"\n",
- "bpuid = uuid.uuid4().hex"
- ],
- "id": "initial_id",
- "outputs": [],
- "execution_count": null
- },
- {
- "metadata": {},
- "cell_type": "code",
- "source": [
- "# Check their balance\n",
- "url = FSB_BASE_URL + f\"{product_id}/wallets/?bpuid={bpuid}\"\n",
- "print(f\"{url=}\")\n",
- "res = requests.get(url).json()\n",
- "print(res['displayed_balances'][0])"
- ],
- "id": "74a8d7f3452c2372",
- "outputs": [],
- "execution_count": null
- },
- {
- "metadata": {},
- "cell_type": "code",
- "source": [
- "# Get user cashout methods\n",
- "url = FSB_BASE_URL + f\"{product_id}/cashout_methods/?bpuid={bpuid}\"\n",
- "print(f\"{url=}\")\n",
- "res = requests.get(url).json()\n",
- "cms = res['cashout_methods']\n",
- "print(dict(Counter(x['type'] for x in cms)))\n",
- "\n",
- "# Get the paypal method\n",
- "paypal_cm = next(filter(lambda x: x['type'] == 'PAYPAL', cms))\n",
- "print(paypal_cm['id'])"
- ],
- "id": "e5c4e61b4727912d",
- "outputs": [],
- "execution_count": null
- },
- {
- "metadata": {},
- "cell_type": "code",
- "source": [
- "# Log in and retain the jb_session cookie for later requests\n",
- "from urllib.parse import parse_qs, urlparse\n",
- "\n",
- "session = requests.Session()\n",
- "email = 'your-email@gmail.com'\n",
- "\n",
- "res = session.post(\n",
- " JB_BASE_URL + 'auth/magic-link/request/',\n",
- " json={'email': email},\n",
- ")\n",
- "res.raise_for_status()\n",
- "print(res.json())"
- ],
- "id": "38c7d77b3abf9948",
- "outputs": [],
- "execution_count": null
- },
- {
- "metadata": {},
- "cell_type": "code",
- "source": [
- "# Paste the link from the login email. Exchanging it sets jb_session on `session`.\n",
- "magic_link = \"https://jamesbillings67.com/auth/magic-link/?token=XXXX\"\n",
- "token = parse_qs(urlparse(magic_link).query)['token'][0]\n",
- "res = session.post(\n",
- " JB_BASE_URL + 'auth/magic-link/exchange/',\n",
- " json={'token': token},\n",
- ")\n",
- "res.raise_for_status()\n",
- "\n",
- "session_token = session.cookies['jb_session']\n",
- "print(f'Logged in; captured jb_session ({len(session_token)} characters)')"
- ],
- "id": "db3caa6e440619fe",
- "outputs": [],
- "execution_count": null
- },
- {
- "metadata": {},
- "cell_type": "code",
- "source": [
- "# Request a PayPal cashout using the authenticated session.\n",
- "# The response confirms that the cashout confirmation email was sent;\n",
- "# the payout event is created only after that email link is confirmed.\n",
- "res = session.post(JB_BASE_URL + 'wallet/cashout/request/', json={\n",
- " 'amount': 10_00,\n",
- " 'cashout_method_id': paypal_cm['id'],\n",
- "})\n",
- "res.raise_for_status()\n",
- "print(res.json())"
- ],
- "id": "774a00c08ab54d4f",
- "outputs": [],
- "execution_count": null
- },
- {
- "metadata": {},
- "cell_type": "code",
- "source": [
- "# Paste link from email\n",
- "magic_link = \"https://jamesbillings67.com/wallet/cashout/confirm/?token=XXXX\"\n",
- "token = parse_qs(urlparse(magic_link).query)['token'][0]\n",
- "res = session.post(\n",
- " JB_BASE_URL + 'wallet/cashout/confirm/',\n",
- " json={'token': token},\n",
- ")\n",
- "print(res.json())\n",
- "res.raise_for_status()"
- ],
- "id": "894887ddc479eb1a",
- "outputs": [],
- "execution_count": null
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python 3",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 2
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython2",
- "version": "2.7.6"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 5
-}
--
cgit v1.2.3