mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-23 15:09:33 +01:00
web: allow users to view their billing history
This commit is contained in:
committed by
Abdullah Atta
parent
7f873f240e
commit
2bd0f1a304
@@ -211,7 +211,8 @@ export function showError(title: string, message: string) {
|
|||||||
export function showMultiDeleteConfirmation(length: number) {
|
export function showMultiDeleteConfirmation(length: number) {
|
||||||
return confirm({
|
return confirm({
|
||||||
title: `Delete ${length} items?`,
|
title: `Delete ${length} items?`,
|
||||||
message: `These items will be **kept in your Trash for ${db.settings?.getTrashCleanupInterval() || 7
|
message: `These items will be **kept in your Trash for ${
|
||||||
|
db.settings?.getTrashCleanupInterval() || 7
|
||||||
} days** after which they will be permanently deleted.`,
|
} days** after which they will be permanently deleted.`,
|
||||||
positiveButtonText: "Yes",
|
positiveButtonText: "Yes",
|
||||||
negativeButtonText: "No"
|
negativeButtonText: "No"
|
||||||
@@ -331,6 +332,12 @@ export function showMoveNoteDialog(noteIds: string[]) {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function showBillingHistoryDialog() {
|
||||||
|
return showDialog("BillingHistoryDialog", (Dialog, perform) => (
|
||||||
|
<Dialog onClose={(res: boolean) => perform(res)} />
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
function getDialogData(type: string) {
|
function getDialogData(type: string) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "create_vault":
|
case "create_vault":
|
||||||
|
|||||||
145
apps/web/src/components/dialogs/billing-history-dialog.tsx
Normal file
145
apps/web/src/components/dialogs/billing-history-dialog.tsx
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
/*
|
||||||
|
This file is part of the Notesnook project (https://notesnook.com/)
|
||||||
|
|
||||||
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Perform } from "../../common/dialog-controller";
|
||||||
|
import Dialog from "./dialog";
|
||||||
|
import { db } from "../../common/db";
|
||||||
|
import { Loading } from "../icons";
|
||||||
|
import { Flex, Link, Text } from "@theme-ui/components";
|
||||||
|
import { formatDate } from "@notesnook/core/utils/date";
|
||||||
|
|
||||||
|
type Transaction = {
|
||||||
|
order_id: string;
|
||||||
|
checkout_id: string;
|
||||||
|
amount: string;
|
||||||
|
currency: string;
|
||||||
|
status: keyof typeof TransactionStatusToText;
|
||||||
|
created_at: Date;
|
||||||
|
passthrough: null;
|
||||||
|
product_id: number;
|
||||||
|
is_subscription: boolean;
|
||||||
|
is_one_off: boolean;
|
||||||
|
subscription: Subscription;
|
||||||
|
user: User;
|
||||||
|
receipt_url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Subscription = {
|
||||||
|
subscription_id: number;
|
||||||
|
status: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type User = {
|
||||||
|
user_id: number;
|
||||||
|
email: string;
|
||||||
|
marketing_consent: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TransactionStatusToText = {
|
||||||
|
completed: "Completed",
|
||||||
|
refunded: "Refunded",
|
||||||
|
partially_refunded: "Partially refunded",
|
||||||
|
disputed: "Disputed"
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BillingHistoryDialogProps = {
|
||||||
|
onClose: Perform;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function BillingHistoryDialog(props: BillingHistoryDialogProps) {
|
||||||
|
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<Error | undefined>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async function () {
|
||||||
|
try {
|
||||||
|
setError(undefined);
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const transactions = await db.subscriptions?.transactions();
|
||||||
|
if (!transactions) return;
|
||||||
|
setTransactions(transactions);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) setError(e);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
isOpen={true}
|
||||||
|
title={"Billing history"}
|
||||||
|
description={"View all the transactions you have made with Notesnook."}
|
||||||
|
onClose={() => props.onClose(false)}
|
||||||
|
negativeButton={{ text: "Close", onClick: () => props.onClose(false) }}
|
||||||
|
width={600}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<Loading sx={{ mt: 2 }} />
|
||||||
|
) : error ? (
|
||||||
|
<Flex sx={{ bg: "errorBg", p: 1, borderRadius: "default" }}>
|
||||||
|
<Text variant="error">
|
||||||
|
{error.message}
|
||||||
|
<br />
|
||||||
|
{error.stack}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
) : (
|
||||||
|
<Flex sx={{ flexDirection: "column", gap: 1, mt: 1 }}>
|
||||||
|
{transactions.map((transaction) => (
|
||||||
|
<Flex
|
||||||
|
key={transaction.order_id}
|
||||||
|
sx={{
|
||||||
|
justifyContent: "space-between",
|
||||||
|
borderBottom: "1px solid var(--border)",
|
||||||
|
pb: 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex sx={{ flexDirection: "column" }}>
|
||||||
|
<Text variant="subtitle">Order #{transaction.order_id}</Text>
|
||||||
|
<Text variant="body" sx={{ color: "fontTertiary" }}>
|
||||||
|
{formatDate(new Date(transaction.created_at).getTime())} •{" "}
|
||||||
|
{TransactionStatusToText[transaction.status]}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex sx={{ flexDirection: "column", alignItems: "end" }}>
|
||||||
|
<Text variant="body">
|
||||||
|
{transaction.amount} {transaction.currency}
|
||||||
|
</Text>
|
||||||
|
<Link
|
||||||
|
href={transaction.receipt_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferer nofollow"
|
||||||
|
variant="text.subBody"
|
||||||
|
sx={{ color: "primary" }}
|
||||||
|
>
|
||||||
|
View receipt
|
||||||
|
</Link>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -57,6 +57,9 @@ const EmailChangeDialog = React.lazy(() => import("./email-change-dialog"));
|
|||||||
const LanguageSelectorDialog = React.lazy(
|
const LanguageSelectorDialog = React.lazy(
|
||||||
() => import("./language-selector-dialog")
|
() => import("./language-selector-dialog")
|
||||||
);
|
);
|
||||||
|
const BillingHistoryDialog = React.lazy(
|
||||||
|
() => import("./billing-history-dialog")
|
||||||
|
);
|
||||||
|
|
||||||
export const Dialogs = {
|
export const Dialogs = {
|
||||||
AddNotebookDialog,
|
AddNotebookDialog,
|
||||||
@@ -85,5 +88,6 @@ export const Dialogs = {
|
|||||||
AddReminderDialog,
|
AddReminderDialog,
|
||||||
ReminderPreviewDialog,
|
ReminderPreviewDialog,
|
||||||
EmailChangeDialog,
|
EmailChangeDialog,
|
||||||
LanguageSelectorDialog
|
LanguageSelectorDialog,
|
||||||
|
BillingHistoryDialog
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ import {
|
|||||||
showPromptDialog,
|
showPromptDialog,
|
||||||
showEmailChangeDialog,
|
showEmailChangeDialog,
|
||||||
showLanguageSelectorDialog,
|
showLanguageSelectorDialog,
|
||||||
confirm
|
confirm,
|
||||||
|
showBillingHistoryDialog
|
||||||
} from "../common/dialog-controller";
|
} from "../common/dialog-controller";
|
||||||
import { TaskManager } from "../common/task-manager";
|
import { TaskManager } from "../common/task-manager";
|
||||||
import { SUBSCRIPTION_STATUS } from "../common/constants";
|
import { SUBSCRIPTION_STATUS } from "../common/constants";
|
||||||
@@ -1391,6 +1392,12 @@ function AccountStatus(props) {
|
|||||||
tip="If you are eligible for a refund, your account will be immediately downgraded to Basic and your funds will be transferred to your account within 24 hours."
|
tip="If you are eligible for a refund, your account will be immediately downgraded to Basic and your funds will be transferred to your account within 24 hours."
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button variant="list" onClick={() => showBillingHistoryDialog()}>
|
||||||
|
<Tip
|
||||||
|
text="Billing history"
|
||||||
|
tip="View all the transactions you have made with Notesnook."
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="list"
|
variant="list"
|
||||||
sx={{ ":hover": { borderColor: "error" } }}
|
sx={{ ":hover": { borderColor: "error" } }}
|
||||||
@@ -1413,11 +1420,8 @@ function AccountStatus(props) {
|
|||||||
});
|
});
|
||||||
showToast(
|
showToast(
|
||||||
"success",
|
"success",
|
||||||
"Your subscription has been cancelled."
|
"Your subscription has been canceled."
|
||||||
);
|
);
|
||||||
setTimeout(() => {
|
|
||||||
window.location.reload();
|
|
||||||
}, 5000);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast("error", e.message);
|
showToast("error", e.message);
|
||||||
|
|||||||
@@ -44,11 +44,20 @@ export default class Subscriptions {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async transactions() {
|
||||||
|
const token = await this._tokenManager.getAccessToken();
|
||||||
|
if (!token) return;
|
||||||
|
return await http.get(
|
||||||
|
`${hosts.SUBSCRIPTIONS_HOST}/subscriptions/transactions`,
|
||||||
|
token
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async updateUrl() {
|
async updateUrl() {
|
||||||
const token = await this._tokenManager.getAccessToken();
|
const token = await this._tokenManager.getAccessToken();
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
return await http.get(
|
return await http.get(
|
||||||
`${hosts.SUBSCRIPTIONS_HOST}/subscriptions/update_url`,
|
`${hosts.SUBSCRIPTIONS_HOST}/subscriptions/update`,
|
||||||
token
|
token
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user