diff --git a/api/ApiHelper.tsx b/api/ApiHelper.tsx index dc03c9ac..f923e3ff 100644 --- a/api/ApiHelper.tsx +++ b/api/ApiHelper.tsx @@ -30,6 +30,7 @@ import { parsePlayer, parsePopularSearch, parsePremiumProducts, + parsePremiumSubscription, parsePrivacySettings, parseProfitableCrafts, parseRecentAuction, @@ -2568,6 +2569,86 @@ export function initAPI(returnSSRResponse: boolean = false): API { }) } + let purchasePremiumSubscription = (productSlug: string, googleToken: string): Promise => { + return new Promise((resolve, reject) => { + httpApi.sendApiRequest({ + type: RequestType.PURCHASE_PREMIUM_SUBSCRIPTION, + customRequestURL: `${getApiEndpoint()}/premium/subscription/${productSlug}`, + requestMethod: 'POST', + data: '', + requestHeader: { + GoogleToken: googleToken, + 'Content-Type': 'application/json' + }, + resolve: data => { + resolve(parsePaymentResponse(data)) + }, + reject: (error: any) => { + apiErrorHandler(RequestType.PURCHASE_PREMIUM_SUBSCRIPTION, error) + reject(error) + } + }) + }) + } + + let getPremiumSubscriptions = (): Promise => { + return new Promise((resolve, reject) => { + let googleId = sessionStorage.getItem('googleId') + if (!googleId) { + toast.error('You need to be logged in to create a premium subscription.') + reject() + return + } + + httpApi.sendApiRequest({ + type: RequestType.CREATE_PREMIUM_SUBSCRIPTION, + customRequestURL: `${getApiEndpoint()}/premium/subscription`, + requestMethod: 'GET', + data: '', + requestHeader: { + GoogleToken: googleId, + 'Content-Type': 'application/json' + }, + resolve: (subscriptions: any = []) => { + resolve(subscriptions.map(parsePremiumSubscription)) + }, + reject: (error: any) => { + apiErrorHandler(RequestType.CREATE_PREMIUM_SUBSCRIPTION, error) + reject(error) + } + }) + }) + } + + let cancelPremiumSubscription = (id: string): Promise => { + return new Promise((resolve, reject) => { + let googleId = sessionStorage.getItem('googleId') + if (!googleId) { + toast.error('You need to be logged in to cancel a premium subscription.') + reject() + return + } + + httpApi.sendApiRequest({ + type: RequestType.DELETE_PREMIUM_SUBSCRIPTION, + customRequestURL: `${getApiEndpoint()}/premium/subscription/${id}`, + requestMethod: 'DELETE', + data: '', + requestHeader: { + GoogleToken: googleId, + 'Content-Type': 'application/json' + }, + resolve: () => { + resolve() + }, + reject: (error: any) => { + apiErrorHandler(RequestType.DELETE_PREMIUM_SUBSCRIPTION, error) + reject(error) + } + }) + }) + } + return { search, trackSearch, @@ -2660,7 +2741,10 @@ export function initAPI(returnSSRResponse: boolean = false): API { updateConfig, requestArchivedAuctions, exportArchivedAuctionsData, - getLinkvertiseLink + getLinkvertiseLink, + getPremiumSubscriptions, + cancelPremiumSubscription, + purchasePremiumSubscription } } diff --git a/api/ApiTypes.d.tsx b/api/ApiTypes.d.tsx index 5f673fbd..38b4f926 100644 --- a/api/ApiTypes.d.tsx +++ b/api/ApiTypes.d.tsx @@ -93,7 +93,10 @@ export enum RequestType { UPDATE_CONFIG = 'updateConfig', ARCHIVED_AUCTIONS = 'archivedAuctions', EXPORT_ARCHIVED_AUCTIONS = 'exportArchivedAuctions', - GET_LINKVERTISE_LINK = 'getLinkvertiseLink' + GET_LINKVERTISE_LINK = 'getLinkvertiseLink', + CREATE_PREMIUM_SUBSCRIPTION = 'createPremiumSubscription', + DELETE_PREMIUM_SUBSCRIPTION = 'deletePremiumSubscription', + PURCHASE_PREMIUM_SUBSCRIPTION = 'purchasePremiumSubscription' } export enum SubscriptionType { diff --git a/components/AccountDetails/AccountDetails.tsx b/components/AccountDetails/AccountDetails.tsx index 709930bf..3ee142a9 100644 --- a/components/AccountDetails/AccountDetails.tsx +++ b/components/AccountDetails/AccountDetails.tsx @@ -26,6 +26,7 @@ function AccountDetails() { let [isLoading, setIsLoading] = useState(true) let [rerenderGoogleSignIn, setRerenderGoogleSignIn] = useState(0) let [products, setProducts] = useState([]) + let [premiumSubscriptions, setPremiumSubscriptions] = useState([]) let [showSendcoflcoins, setShowSendCoflcoins] = useState(false) let coflCoins = useCoflCoins() let { pushInstruction } = useMatomo() @@ -54,7 +55,13 @@ function AccountDetails() { return api.refreshLoadPremiumProducts(products => { products = products.filter(product => product.expires.getTime() > new Date().getTime()) setProducts(products) - setIsLoading(false) + }) + } + + function loadPremiumSubscriptions(): Promise { + return api.getPremiumSubscriptions().then(subscriptions => { + subscriptions = subscriptions.filter(subscription => !subscription.endsAt || subscription.endsAt.getTime() > new Date().getTime()) + setPremiumSubscriptions([...subscriptions]) }) } @@ -75,7 +82,9 @@ function AccountDetails() { let googleId = sessionStorage.getItem('googleId') setIsLoading(true) if (googleId) { - loadPremiumProducts() + Promise.all([loadPremiumProducts(), loadPremiumSubscriptions()]).then(() => { + setIsLoading(false) + }) setIsLoggedIn(true) } } @@ -116,6 +125,13 @@ function AccountDetails() { setRerenderGoogleSignIn(rerenderGoogleSignIn + 1) } + function onSubscriptionCancel(subscription: PremiumSubscription) { + api.cancelPremiumSubscription(subscription.externalId).then(() => { + toast.success('Subscription cancelled') + loadPremiumSubscriptions() + }) + } + return ( <>

@@ -127,7 +143,12 @@ function AccountDetails() {

Account: {getAccountElement()}

- +

CoflCoins: + + + + + + ) +} + +export default CancelSubscriptionConfirmDialog diff --git a/components/Premium/Premium.tsx b/components/Premium/Premium.tsx index 858fc389..95532510 100644 --- a/components/Premium/Premium.tsx +++ b/components/Premium/Premium.tsx @@ -13,12 +13,14 @@ import TransferCoflCoins from '../TransferCoflCoins/TransferCoflCoins' import { CANCELLATION_RIGHT_CONFIRMED } from '../../utils/SettingsUtils' import { getHighestPriorityPremiumProduct } from '../../utils/PremiumTypeUtils' import PremiumStatus from './PremiumStatus/PremiumStatus' +import { toast } from 'react-toastify' function Premium() { let [isLoggedIn, setIsLoggedIn] = useState(false) let [hasPremium, setHasPremium] = useState() let [activePremiumProduct, setActivePremiumProduct] = useState() let [products, setProducts] = useState([]) + let [premiumSubscriptions, setPremiumSubscriptions] = useState([]) let [isLoading, setIsLoading] = useState(false) let [showSendCoflCoins, setShowSendCoflCoins] = useState(false) let [cancellationRightLossConfirmed, setCancellationRightLossConfirmed] = useState(false) @@ -42,7 +44,20 @@ function Premium() { setHasPremium(true) setActivePremiumProduct(activePremiumProduct) } - setIsLoading(false) + }) + } + + function loadPremiumSubscriptions(): Promise { + return api.getPremiumSubscriptions().then(subscriptions => { + subscriptions = subscriptions.filter(subscription => !subscription.endsAt || subscription.endsAt.getTime() > new Date().getTime()) + setPremiumSubscriptions(subscriptions) + }) + } + + function onSubscriptionCancel(subscription: PremiumSubscription) { + api.cancelPremiumSubscription(subscription.externalId).then(() => { + loadPremiumSubscriptions() + toast.success('Subscription cancelled') }) } @@ -51,7 +66,9 @@ function Premium() { if (googleId) { setIsLoading(true) setIsLoggedIn(true) - loadPremiumProducts() + Promise.all([loadPremiumProducts(), loadPremiumSubscriptions()]).then(() => { + setIsLoading(false) + }) } } @@ -87,13 +104,17 @@ function Premium() { ) : null}


- {isLoggedIn ? : null} + {isLoggedIn ? : null}
{isLoading ? getLoadingElement() : ''}
{isLoggedIn ? (
- +
) : null} {isLoggedIn ? ( diff --git a/components/Premium/PremiumStatus/PremiumStatus.tsx b/components/Premium/PremiumStatus/PremiumStatus.tsx index 24920afb..8372d2f2 100644 --- a/components/Premium/PremiumStatus/PremiumStatus.tsx +++ b/components/Premium/PremiumStatus/PremiumStatus.tsx @@ -2,18 +2,23 @@ import moment from 'moment' import React, { useEffect, useState } from 'react' import { getLocalDateAndTime } from '../../../utils/Formatter' -import { getHighestPriorityPremiumProduct, getPremiumType } from '../../../utils/PremiumTypeUtils' +import { getHighestPriorityPremiumProduct, getPremiumLabelForSubscription, getPremiumType } from '../../../utils/PremiumTypeUtils' import Tooltip from '../../Tooltip/Tooltip' import styles from './PremiumStatus.module.css' +import { CancelOutlined } from '@mui/icons-material' +import CancelSubscriptionConfirmDialog from '../CancelSubscriptionConfirmDialog/CancelSubscriptionConfirmDialog' interface Props { products: PremiumProduct[] + subscriptions: PremiumSubscription[] labelStyle?: React.CSSProperties + onSubscriptionCancel(subscription: PremiumSubscription): void } function PremiumStatus(props: Props) { let [highestPriorityProduct, setHighestPriorityProduct] = useState() let [productsToShow, setProductsToShow] = useState() + let [showCancelSubscriptionDialogSubscription, setShowCancelSubscriptionDialogSubscription] = useState() useEffect(() => { let products = props.products.map(product => { @@ -59,16 +64,57 @@ function PremiumStatus(props: Props) { ) } + let numberOfEntriesToShow = (productsToShow?.length || 0) + (props.subscriptions?.length || 0) + return ( <>
- {productsToShow && productsToShow.length > 1 ? ( + {numberOfEntriesToShow > 1 ? (
Premium Status:
    - {productsToShow.map(product => ( + {props.subscriptions.map(subscription => ( +
  • + {' '} + + {getPremiumLabelForSubscription(subscription)} (Subscription){' '} + {subscription.endsAt && Canceled} + + } + tooltipContent={ + + {subscription.endsAt ? ( + Ends at {getLocalDateAndTime(subscription.endsAt)} + ) : ( + Renews at {getLocalDateAndTime(subscription.renewsAt)} + )} + + } + /> + {!subscription.endsAt && ( + + { + setShowCancelSubscriptionDialogSubscription(subscription) + }} + /> + + } + tooltipContent={Cancel subscription} + /> + )} +
  • + ))} + {productsToShow?.map(product => (
  • {getProductListEntry(product)}
  • ))}
@@ -83,6 +129,18 @@ function PremiumStatus(props: Props) {

)}
+ { + if (showCancelSubscriptionDialogSubscription) { + props.onSubscriptionCancel(showCancelSubscriptionDialogSubscription) + setShowCancelSubscriptionDialogSubscription(undefined) + } + }} + onHide={() => { + setShowCancelSubscriptionDialogSubscription(undefined) + }} + /> ) } diff --git a/global.d.ts b/global.d.ts index 2a5c296f..8e710c63 100644 --- a/global.d.ts +++ b/global.d.ts @@ -269,6 +269,9 @@ interface API { requestArchivedAuctions(itemTag: string, itemFilter?: ItemFilter): Promise exportArchivedAuctionsData(itemTag: string, itemFilter: ItemFilter, discordWebhookUrl: string, flags: string[]): Promise getLinkvertiseLink(): Promise + getPremiumSubscriptions(): Promise + cancelPremiumSubscription(id: string): Promise + purchasePremiumSubscription(productSlug: string, googleToken: string): Promise } interface CacheUtils { @@ -687,3 +690,12 @@ interface ArchivedAuctionResponse { auctions: ArchivedAuction[] queryStatus: 'Success' | 'NoResults' | 'Pending' | 'Partial' } + +interface PremiumSubscription { + externalId: string + endsAt?: Date + productName: string + paymentAmount: string + renewsAt: Date + createdAt: Date +} diff --git a/utils/Parser/APIResponseParser.tsx b/utils/Parser/APIResponseParser.tsx index b443426b..e2940f1d 100644 --- a/utils/Parser/APIResponseParser.tsx +++ b/utils/Parser/APIResponseParser.tsx @@ -712,3 +712,14 @@ export function parseArchivedAuctions(archivedAuctionsResponse: any): ArchivedAu }) } } + +export function parsePremiumSubscription(subscription: any): PremiumSubscription { + return { + externalId: subscription.externalId, + endsAt: subscription.endsAt ? parseDate(subscription.endsAt) : undefined, + productName: subscription.productName, + paymentAmount: subscription.paymentAmount, + renewsAt: parseDate(subscription.renewsAt), + createdAt: parseDate(subscription.createdAt) + } +} diff --git a/utils/PremiumTypeUtils.tsx b/utils/PremiumTypeUtils.tsx index b42b38fc..79118926 100644 --- a/utils/PremiumTypeUtils.tsx +++ b/utils/PremiumTypeUtils.tsx @@ -76,6 +76,19 @@ export function getPremiumType(product: PremiumProduct) { return [...PREMIUM_TYPES].sort((a, b) => b.productId.localeCompare(a.productId)).find(type => product.productSlug.startsWith(type.productId)) } +export function getPremiumLabelForSubscription(subscription: PremiumSubscription) { + if (!subscription.productName) { + return 'Unknown' + } + if (subscription.productName.includes('prem_plus')) { + return 'Premium+' + } + if (subscription.productName.includes('prem')) { + return 'Premium' + } + return subscription.productName +} + export function hasHighEnoughPremium(products: PremiumProduct[], minPremiumType: PREMIUM_RANK) { let hasHighEnoughPremium = false products.forEach(product => {