{
>
-
- {/* (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;
+ },
+ },
},
},
--
cgit v1.2.3