Feat: integrate deposit (#363)
* create deposit payment * display recent deposits & improve UI * support Mixin pay
This commit is contained in:
@@ -297,4 +297,28 @@ export class Client {
|
||||
unstarStory(storyId: string): Promise<{ starred: boolean }> {
|
||||
return this.api.delete(`/api/mine/stories/${storyId}`);
|
||||
}
|
||||
|
||||
createPayment(params: {
|
||||
amount: number;
|
||||
processor: string;
|
||||
paymentType: string;
|
||||
}): Promise<PaymentType> {
|
||||
return this.api.post("/api/payments", decamelizeKeys(params));
|
||||
}
|
||||
|
||||
payments(params?: {
|
||||
paymentType?: string;
|
||||
page?: number;
|
||||
items?: number;
|
||||
}): Promise<
|
||||
{
|
||||
payments: PaymentType[];
|
||||
} & PagyResponseType
|
||||
> {
|
||||
return this.api.get("/api/payments", { params: decamelizeKeys(params) });
|
||||
}
|
||||
|
||||
payment(id: string): Promise<PaymentType> {
|
||||
return this.api.get(`/api/payments/${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,6 +309,15 @@
|
||||
"language": "Language",
|
||||
"balance": "Balance",
|
||||
"deposit": "Deposit",
|
||||
"depositDescription": "Deposit to your account",
|
||||
"depositDisclaimer": "After deposit, your balance is only used for paid services. It is not refundable.",
|
||||
"pay": "Pay",
|
||||
"pleaseCompletePaymentInPopupWindow": "Please complete payment in the popup window",
|
||||
"processor": "Processor",
|
||||
"amount": "Amount",
|
||||
"date": "Date",
|
||||
"status": "Status",
|
||||
"recentDeposits": "Recent deposits",
|
||||
"notAvailableYet": "Not available yet",
|
||||
"whisperModel": "Whisper Model",
|
||||
"sttAiService": "STT AI service",
|
||||
|
||||
@@ -308,6 +308,15 @@
|
||||
"language": "语言",
|
||||
"balance": "余额",
|
||||
"deposit": "充值",
|
||||
"depositDescription": "充值至您的账户,用于购买付费服务",
|
||||
"depositDisclaimer": "充值金额将直接存入您的账户,仅用于 App 内购买付费服务,不可退款",
|
||||
"pay": "支付",
|
||||
"pleaseCompletePaymentInPopupWindow": "请在弹出窗口中完成支付",
|
||||
"processor": "渠道",
|
||||
"amount": "金额",
|
||||
"date": "日期",
|
||||
"status": "状态",
|
||||
"recentDeposits": "最近充值记录",
|
||||
"notAvailableYet": "暂未开放",
|
||||
"whisperModel": "Whisper 模型",
|
||||
"sttAiService": "语音转文本服务",
|
||||
|
||||
@@ -20,7 +20,7 @@ import { WEB_API_URL, REPO_URL } from "@/constants";
|
||||
import { AudibleProvider, TedProvider } from "@main/providers";
|
||||
import Ffmpeg from "@main/ffmpeg";
|
||||
import { Waveform } from "./waveform";
|
||||
import url from 'url';
|
||||
import url from "url";
|
||||
|
||||
const __filename = url.fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@@ -442,14 +442,8 @@ ${log}
|
||||
},
|
||||
});
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||
if (url.startsWith("http")) {
|
||||
logger.info(`Opening ${url}`);
|
||||
shell.openExternal(url);
|
||||
return { action: "deny" };
|
||||
} else {
|
||||
return { action: "allow" };
|
||||
}
|
||||
mainWindow.webContents.setWindowOpenHandler(() => {
|
||||
return { action: "allow" };
|
||||
});
|
||||
|
||||
// and load the index.html of the app.
|
||||
|
||||
@@ -1,16 +1,82 @@
|
||||
import { t } from "i18next";
|
||||
import { AppSettingsProviderContext } from "@renderer/context";
|
||||
import { useContext, useState, useEffect } from "react";
|
||||
import { Button } from "@renderer/components/ui";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
toast,
|
||||
} from "@renderer/components/ui";
|
||||
import { LoaderSpin } from "@renderer/components";
|
||||
import { LoaderIcon } from "lucide-react";
|
||||
import { formatDateTime } from "@/renderer/lib/utils";
|
||||
|
||||
export const BalanceSettings = () => {
|
||||
const { webApi } = useContext(AppSettingsProviderContext);
|
||||
const { webApi, user } = useContext(AppSettingsProviderContext);
|
||||
const [balance, setBalance] = useState<number>(0);
|
||||
const [depositAmount, setDepositAmount] = useState<number>(1);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [paymentCreated, setPaymentCreated] = useState<boolean>(false);
|
||||
const [payments, setPayments] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const refreshPayments = () => {
|
||||
webApi
|
||||
.payments({
|
||||
paymentType: "deposit",
|
||||
})
|
||||
.then(({ payments }) => {
|
||||
setPayments(payments);
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
};
|
||||
|
||||
const refreshBalance = () => {
|
||||
webApi.me().then((user) => {
|
||||
setBalance(user.balance);
|
||||
});
|
||||
};
|
||||
|
||||
const createDepositPayment = (processor = "stripe") => {
|
||||
if (loading) return;
|
||||
|
||||
setLoading(true);
|
||||
webApi
|
||||
.createPayment({
|
||||
amount: depositAmount,
|
||||
paymentType: "deposit",
|
||||
processor,
|
||||
})
|
||||
.then((payment) => {
|
||||
if (payment?.payUrl) {
|
||||
setPaymentCreated(true);
|
||||
window.open(payment.payUrl, "model");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error.message);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refreshBalance();
|
||||
}, []);
|
||||
|
||||
if (!balance) return null;
|
||||
@@ -20,15 +86,134 @@ export const BalanceSettings = () => {
|
||||
<div className="mb-2">{t("balance")}</div>
|
||||
<div className="text-sm text-muted-foreground mb-2">${balance}</div>
|
||||
</div>
|
||||
<Button
|
||||
data-tooltip-id="preferences-tooltip"
|
||||
data-tooltip-content={t("notAvailableYet")}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="cursor-not-allowed"
|
||||
|
||||
<Dialog
|
||||
onOpenChange={(value) => {
|
||||
if (value) {
|
||||
refreshPayments();
|
||||
} else {
|
||||
setPaymentCreated(false);
|
||||
refreshBalance();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("deposit")}
|
||||
</Button>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
onClick={() => setPaymentCreated(false)}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className=""
|
||||
>
|
||||
{t("deposit")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("deposit")}</DialogTitle>
|
||||
<DialogDescription>{t("depositDescription")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{paymentCreated ? (
|
||||
<>
|
||||
<LoaderSpin />
|
||||
<div className="text-center">
|
||||
{t("pleaseCompletePaymentInPopupWindow")}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{[1, 2, 5, 10].map((amount) => (
|
||||
<div
|
||||
className={`text-xl w-full h-20 border rounded-md flex items-center justify-center cursor-pointer shadow hover:bg-gray-100 transition-colors duration-200 ease-in-out ${
|
||||
amount == depositAmount ? "bg-gray-100" : ""
|
||||
}`}
|
||||
key={`deposit-amount-${amount}`}
|
||||
onClick={() => setDepositAmount(amount)}
|
||||
>
|
||||
${amount}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-sm">{t("depositDisclaimer")}</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="secondary">
|
||||
{paymentCreated ? t("finish") : t("cancel")}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
{paymentCreated ? null : (
|
||||
<>
|
||||
{user.hasMixin && (
|
||||
<Button
|
||||
variant="default"
|
||||
disabled={loading}
|
||||
className="bg-blue-500 hover:bg-blue-600 transition-colors duration-200 ease-in-out"
|
||||
onClick={() => createDepositPayment("mixin")}
|
||||
>
|
||||
{loading && (
|
||||
<LoaderIcon className="w-4 h-4 mr-2 animate-spin" />
|
||||
)}
|
||||
<span>Mixin {t('pay')}</span>
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="default"
|
||||
disabled={loading}
|
||||
onClick={() => createDepositPayment()}
|
||||
>
|
||||
{loading && (
|
||||
<LoaderIcon className="w-4 h-4 mr-2 animate-spin" />
|
||||
)}
|
||||
<span>{t("pay")}</span>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</DialogFooter>
|
||||
|
||||
{payments.length > 0 && (
|
||||
<div className="">
|
||||
<Table>
|
||||
<TableCaption>{t("recentDeposits")}</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>ID</TableHead>
|
||||
<TableHead className="capitalize">{t("amount")}</TableHead>
|
||||
<TableHead className="capitalize">{t("status")}</TableHead>
|
||||
<TableHead className="capitalize">
|
||||
{t("processor")}
|
||||
</TableHead>
|
||||
<TableHead className="capitalize">{t("date")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{payments.map((payment) => (
|
||||
<TableRow key={payment.id}>
|
||||
<TableCell>
|
||||
<span className="text-xs bg-muted font-mono p-0.5 rounded select-text">
|
||||
{payment.id.split("-").shift()}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>${payment.amount}</TableCell>
|
||||
<TableCell className="">{payment.status}</TableCell>
|
||||
<TableCell className="capitalize">
|
||||
{payment.processor}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{formatDateTime(payment.createdAt)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -150,7 +150,7 @@ export const AppSettingsProvider = ({
|
||||
|
||||
client.me().then((user) => {
|
||||
if (user?.id) {
|
||||
login(currentUser);
|
||||
login(Object.assign({}, currentUser, user));
|
||||
} else {
|
||||
logout();
|
||||
}
|
||||
|
||||
14
enjoy/src/types/payment.d.ts
vendored
Normal file
14
enjoy/src/types/payment.d.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
type PaymentType = {
|
||||
id: string;
|
||||
amount: number;
|
||||
status: 'succeeded' | 'expired' | 'pending';
|
||||
paymentType: string;
|
||||
processor: string;
|
||||
traceId?: string;
|
||||
snapshotId?: string;
|
||||
memo?: string;
|
||||
payUrl?: string;
|
||||
receiptUrl?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
1
enjoy/src/types/user.d.ts
vendored
1
enjoy/src/types/user.d.ts
vendored
@@ -6,5 +6,6 @@ type UserType = {
|
||||
accessToken?: string;
|
||||
recordingsCount?: number;
|
||||
recordingsDuration?: number;
|
||||
hasMixin?: boolean;
|
||||
createdAt?: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user