Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/components/menu/account-view/account-info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const AccountInfo = () => {
token
);
if (rep.status === 401) {
showErrorToast(t('Login status expired'));
showErrorToast(t('Login status expired.'));
dispatch(logout());
return;
}
Expand All @@ -68,7 +68,7 @@ const AccountInfo = () => {
token
);
if (rep.status === 401) {
showErrorToast(t('Login status expired'));
showErrorToast(t('Login status expired.'));
dispatch(logout());
return;
}
Expand Down
12 changes: 6 additions & 6 deletions src/components/menu/account-view/saves-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const SavesSection = () => {
token
);
if (rep.status === 401) {
showErrorToast(t('Login status expired'));
showErrorToast(t('Login status expired.'));
dispatch(logout());
return;
}
Expand All @@ -94,7 +94,7 @@ const SavesSection = () => {
// fetch cloud save metadata
const savesRep = await dispatch(fetchSaveList());
if (savesRep.meta.requestStatus !== 'fulfilled') {
showErrorToast(t('Login status expired')); // TODO: also might be !200 response
showErrorToast(t('Login status expired.')); // TODO: also might be !200 response
setSyncButtonIsLoading(undefined);
return;
}
Expand Down Expand Up @@ -135,7 +135,7 @@ const SavesSection = () => {
token
);
if (rep.status === 401) {
showErrorToast(t('Login status expired'));
showErrorToast(t('Login status expired.'));
setSyncButtonIsLoading(undefined);
dispatch(logout());
return;
Expand All @@ -151,7 +151,7 @@ const SavesSection = () => {
setSyncButtonIsLoading(saveId);
const rep = await apiFetch(API_ENDPOINT.SAVES + '/' + saveId, {}, token);
if (rep.status === 401) {
showErrorToast(t('Login status expired'));
showErrorToast(t('Login status expired.'));
setSyncButtonIsLoading(undefined);
dispatch(logout());
return;
Expand All @@ -174,7 +174,7 @@ const SavesSection = () => {
setDeleteButtonIsLoading(saveId);
const rep = await apiFetch(API_ENDPOINT.SAVES + '/' + currentSaveId, { method: 'DELETE' }, token);
if (rep.status === 401) {
showErrorToast(t('Login status expired'));
showErrorToast(t('Login status expired.'));
setDeleteButtonIsLoading(undefined);
dispatch(logout());
return;
Expand All @@ -195,7 +195,7 @@ const SavesSection = () => {
token
);
if (rep.status === 401) {
showErrorToast(t('Login status expired'));
showErrorToast(t('Login status expired.'));
dispatch(logout());
return;
}
Expand Down
141 changes: 65 additions & 76 deletions src/components/menu/account-view/subscription-section.tsx
Original file line number Diff line number Diff line change
@@ -1,70 +1,20 @@
import { Button, Card, List, Stack, Text, Title, Tooltip } from '@mantine/core';
import { RMSection, RMSectionBody, RMSectionHeader } from '@railmapgen/mantine-components';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useRootDispatch, useRootSelector } from '../../../redux';
import {
ActiveSubscriptions,
defaultActiveSubscriptions,
logout,
setActiveSubscriptions,
} from '../../../redux/account/account-slice';
import { apiFetch } from '../../../util/api';
import { API_ENDPOINT } from '../../../util/constants';
import { useRootSelector } from '../../../redux';
import RedeemModal from '../../modal/redeem-modal';
import { RMSection, RMSectionBody, RMSectionHeader } from '@railmapgen/mantine-components';
import { Button, Card, List, Stack, Text, Title } from '@mantine/core';
import { addNotification } from '../../../redux/notification/notification-slice';

interface APISubscription {
type: 'RMP' | 'RMP_CLOUD' | 'RMP_EXPORT';
expires: string;
}
const DAYS_TO_REMIND_RENEW = 90;
const MILLISECONDS_TO_REMIND_RENEW = DAYS_TO_REMIND_RENEW * 24 * 60 * 60 * 1000;

const SubscriptionSection = () => {
const { t } = useTranslation();
const { isLoggedIn, token } = useRootSelector(state => state.account);
const dispatch = useRootDispatch();

const [subscriptions, setSubscriptions] = React.useState([] as APISubscription[]);
const { activeSubscriptions } = useRootSelector(state => state.account);
const [isRedeemModalOpen, setIsRedeemModalOpen] = React.useState(false);

const showErrorToast = (msg: string) =>
dispatch(
addNotification({
title: t('Unable to retrieve your subscriptions'),
message: msg,
type: 'error',
duration: 9000,
})
);

const getSubscriptions = async () => {
if (!isLoggedIn) return;
const rep = await apiFetch(API_ENDPOINT.SUBSCRIPTION, {}, token);
if (rep.status === 401) {
showErrorToast(t('Login status expired'));
dispatch(logout());
return;
}
if (rep.status !== 200) {
showErrorToast(await rep.text());
return;
}
const subscriptions = (await rep.json()).subscriptions as APISubscription[];
if (!subscriptions.map(_ => _.type).includes('RMP_CLOUD')) return;
setSubscriptions([{ type: 'RMP', expires: subscriptions[0].expires }]);

const activeSubscriptions = structuredClone(defaultActiveSubscriptions);
for (const subscription of subscriptions) {
const type = subscription.type;
if (type in activeSubscriptions) {
activeSubscriptions[type as keyof ActiveSubscriptions] = true;
}
}
dispatch(setActiveSubscriptions(activeSubscriptions));
};
React.useEffect(() => {
getSubscriptions();
}, []);
const noneSubscription = !activeSubscriptions.RMP_CLOUD && !activeSubscriptions.RMP_EXPORT;

return (
<RMSection>
Expand All @@ -78,12 +28,10 @@ const SubscriptionSection = () => {
</RMSectionHeader>

<RMSectionBody direction="column" gap="xs">
{subscriptions.length === 0 && (
{noneSubscription && (
<Card withBorder shadow="sm">
<Card.Section p="xs">
<Title order={4} size="h3">
{t('Rail Map Painter')}
</Title>
<Text fw="bold">{t('Rail Map Painter')}</Text>
</Card.Section>
<Stack gap="xs">
<Text>{t('With this subscription, the following features are unlocked:')}</Text>
Expand All @@ -92,6 +40,7 @@ const SubscriptionSection = () => {
<List.Item>{t('Sync 9 more saves')}</List.Item>
<List.Item>{t('Unlimited master nodes')}</List.Item>
<List.Item>{t('Unlimited parallel lines')}</List.Item>
<List.Item>{t('Random station names')}</List.Item>
</List>

<Text>
Expand All @@ -101,30 +50,70 @@ const SubscriptionSection = () => {
</Card>
)}

{subscriptions.map(_ => (
<Card key={_.type} withBorder shadow="sm">
{!noneSubscription && (
<Card withBorder shadow="sm">
<Card.Section p="xs">
<Title order={4} size="h3">
{t(_.type === 'RMP' ? 'Rail Map Painter' : _.type)}
</Title>
<Text fw="bold">{t('Rail Map Painter')}</Text>
</Card.Section>
<Stack gap="xs">
<Text>
{t('Expires at:')} {new Date(_.expires).toLocaleString()}
{t('Expires at:')} {new Date(activeSubscriptions.RMP_CLOUD!).toLocaleString()}
</Text>
<Button color="blue" onClick={() => setIsRedeemModalOpen(true)}>
{t('Renew')}
</Button>
{new Date(activeSubscriptions.RMP_CLOUD!).getTime() - new Date().getTime() <
MILLISECONDS_TO_REMIND_RENEW ? (
<Tooltip
opened
label={t('Renew now and get an extra 45 days!')}
position="bottom"
withArrow
>
<Button color="blue" onClick={() => setIsRedeemModalOpen(true)}>
{t('Renew')}
</Button>
</Tooltip>
) : (
<Button color="blue" onClick={() => setIsRedeemModalOpen(true)}>
{t('Renew')}
</Button>
)}
</Stack>
</Card>
))}
)}

{/* {Object.entries(activeSubscriptions)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.filter(([_, expires]) => expires)
.map(([type, expires]) => (
<Card key={type} withBorder shadow="sm">
<Card.Section p="xs">
<Text fw="bold">{t(type)}</Text>
</Card.Section>
<Stack gap="xs">
<Text>
{t('Expires at:')} {new Date(expires).toLocaleString()}
</Text>
{new Date(expires).getTime() - new Date().getTime() < MILLISECONDS_TO_REMIND_RENEW ? (
<Tooltip
opened
label={t('Renew now and get an extra 45 days!')}
position="bottom"
withArrow
>
<Button color="blue" onClick={() => setIsRedeemModalOpen(true)}>
{t('Renew')}
</Button>
</Tooltip>
) : (
<Button color="blue" onClick={() => setIsRedeemModalOpen(true)}>
{t('Renew')}
</Button>
)}
</Stack>
</Card>
))} */}
</RMSectionBody>

<RedeemModal
opened={isRedeemModalOpen}
onClose={() => setIsRedeemModalOpen(false)}
getSubscriptions={getSubscriptions}
/>
<RedeemModal opened={isRedeemModalOpen} onClose={() => setIsRedeemModalOpen(false)} />
</RMSection>
);
};
Expand Down
14 changes: 7 additions & 7 deletions src/components/modal/redeem-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { Anchor, Button, Group, List, Modal, Text, TextInput } from '@mantine/core';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { MdOpenInNew } from 'react-icons/md';
import { useRootDispatch, useRootSelector } from '../../redux';
import { logout } from '../../redux/account/account-slice';
import { fetchSubscription, logout } from '../../redux/account/account-slice';
import { addNotification } from '../../redux/notification/notification-slice';
import { apiFetch } from '../../util/api';
import { API_ENDPOINT } from '../../util/constants';
import { Anchor, Button, Group, List, Modal, Text, TextInput } from '@mantine/core';
import { addNotification } from '../../redux/notification/notification-slice';

const RedeemModal = (props: { opened: boolean; onClose: () => void; getSubscriptions: () => Promise<void> }) => {
const RedeemModal = (props: { opened: boolean; onClose: () => void }) => {
const { t } = useTranslation();
const { opened, onClose, getSubscriptions } = props;
const { opened, onClose } = props;
const { isLoggedIn, token } = useRootSelector(state => state.account);
const dispatch = useRootDispatch();

Expand All @@ -34,7 +34,7 @@ const RedeemModal = (props: { opened: boolean; onClose: () => void; getSubscript
token
);
if (rep.status === 401) {
showErrorToast(t('Login status expired'));
showErrorToast(t('Login status expired.'));
dispatch(logout());
return;
}
Expand All @@ -43,7 +43,7 @@ const RedeemModal = (props: { opened: boolean; onClose: () => void; getSubscript
showErrorToast(t(msg));
return;
}
await getSubscriptions();
dispatch(fetchSubscription());
// TODO: let RMP refresh its subscription status
onClose();
};
Expand Down
5 changes: 4 additions & 1 deletion src/i18n/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,8 @@
"text2": " from local storage. This item is created and used by ",
"text3": ". Deleting it may cause unrecoverable data loss or unexpected behaviour to the applications.",
"text4": "Please close this modal unless you're certain to do so or are supervised by our developers."
}
},

"RMP_CLOUD": "Rail Map Painter・Tianshu",
"RMP_EXPORT": "Rail Map Painter・Yuheng"
}
Loading