From 4daf8e48af35f842c9a0ca5c1df70116bebc4cb5 Mon Sep 17 00:00:00 2001 From: Matthias Luger Date: Tue, 15 Oct 2024 22:31:16 +0200 Subject: [PATCH 1/8] add premium subscriptions endpoint --- api/ApiHelper.tsx | 62 +++++++++++++++++++++++++++++++++++++++++++++- api/ApiTypes.d.tsx | 4 ++- global.d.ts | 2 ++ 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/api/ApiHelper.tsx b/api/ApiHelper.tsx index dc03c9ac..edbc1aff 100644 --- a/api/ApiHelper.tsx +++ b/api/ApiHelper.tsx @@ -2568,6 +2568,64 @@ export function initAPI(returnSSRResponse: boolean = false): API { }) } + let createPremiumSubscription = (): 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: () => { + resolve() + }, + reject: (error: any) => { + apiErrorHandler(RequestType.CREATE_PREMIUM_SUBSCRIPTION, error) + reject(error) + } + }) + }) + } + + let deletePremiumSubscription = (id: string): Promise => { + return new Promise((resolve, reject) => { + let googleId = sessionStorage.getItem('googleId') + if (!googleId) { + toast.error('You need to be logged in to delete 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 +2718,9 @@ export function initAPI(returnSSRResponse: boolean = false): API { updateConfig, requestArchivedAuctions, exportArchivedAuctionsData, - getLinkvertiseLink + getLinkvertiseLink, + createPremiumSubscription, + deletePremiumSubscription } } diff --git a/api/ApiTypes.d.tsx b/api/ApiTypes.d.tsx index 5f673fbd..2a03ca47 100644 --- a/api/ApiTypes.d.tsx +++ b/api/ApiTypes.d.tsx @@ -93,7 +93,9 @@ 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' } export enum SubscriptionType { diff --git a/global.d.ts b/global.d.ts index 8ab4bfef..0beba65c 100644 --- a/global.d.ts +++ b/global.d.ts @@ -269,6 +269,8 @@ interface API { requestArchivedAuctions(itemTag: string, itemFilter?: ItemFilter): Promise exportArchivedAuctionsData(itemTag: string, itemFilter: ItemFilter, discordWebhookUrl: string, flags: string[]): Promise getLinkvertiseLink(): Promise + createPremiumSubscription(): Promise + deletePremiumSubscription(id: string): Promise } interface CacheUtils { From 2b21ab1a30dc5fc4bea2ea8a27219699af0d59f5 Mon Sep 17 00:00:00 2001 From: Matthias Luger Date: Wed, 16 Oct 2024 23:57:50 +0200 Subject: [PATCH 2/8] add premium subscription tooltip --- api/ApiHelper.tsx | 4 +-- components/Premium/BuyPremium/BuyPremium.tsx | 11 +++++++ .../BuyPremiumSubscription.tsx | 29 +++++++++++++++++++ global.d.ts | 2 +- 4 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 components/Premium/BuyPremiumSubscription/BuyPremiumSubscription.tsx diff --git a/api/ApiHelper.tsx b/api/ApiHelper.tsx index edbc1aff..7cb59409 100644 --- a/api/ApiHelper.tsx +++ b/api/ApiHelper.tsx @@ -2568,7 +2568,7 @@ export function initAPI(returnSSRResponse: boolean = false): API { }) } - let createPremiumSubscription = (): Promise => { + let getPremiumSubscription = (): Promise => { return new Promise((resolve, reject) => { let googleId = sessionStorage.getItem('googleId') if (!googleId) { @@ -2719,7 +2719,7 @@ export function initAPI(returnSSRResponse: boolean = false): API { requestArchivedAuctions, exportArchivedAuctionsData, getLinkvertiseLink, - createPremiumSubscription, + getPremiumSubscription, deletePremiumSubscription } } diff --git a/components/Premium/BuyPremium/BuyPremium.tsx b/components/Premium/BuyPremium/BuyPremium.tsx index b33b34c4..296afba1 100644 --- a/components/Premium/BuyPremium/BuyPremium.tsx +++ b/components/Premium/BuyPremium/BuyPremium.tsx @@ -10,6 +10,8 @@ import { CoflCoinsDisplay } from '../../CoflCoins/CoflCoinsDisplay' import Number from '../../Number/Number' import styles from './BuyPremium.module.css' import BuyPremiumConfirmationDialog from './BuyPremiumConfirmationDialog' +import Tooltip from '../../Tooltip/Tooltip' +import BuyPremiumSubscription from '../BuyPremiumSubscription/BuyPremiumSubscription' interface Props { activePremiumProduct: PremiumProduct @@ -151,6 +153,15 @@ function BuyPremium(props: Props) { ) : null}

This is a prepaid service. We won't automatically charge you after your premium time runs out!

+

+ If you want to it to automatically renew itself,{' '} + click here} + tooltipContent={} + tooltipTitle={Purchase Subscription} + /> +


+ + ))} + {productsToShow?.map(product => (
  • {getProductListEntry(product)}
  • ))} @@ -70,7 +93,7 @@ function PremiumStatus(props: Props) { Premium Status: - {highestPriorityProduct ? getProductListEntry(highestPriorityProduct) : 'No Premium'} + {highestPriorityProduct ? getProductListEntry(highestPriorityProduct!) : 'No Premium'}

    )} diff --git a/global.d.ts b/global.d.ts index f666930f..41c49525 100644 --- a/global.d.ts +++ b/global.d.ts @@ -269,8 +269,8 @@ interface API { requestArchivedAuctions(itemTag: string, itemFilter?: ItemFilter): Promise exportArchivedAuctionsData(itemTag: string, itemFilter: ItemFilter, discordWebhookUrl: string, flags: string[]): Promise getLinkvertiseLink(): Promise - getPremiumSubscription(): Promise - deletePremiumSubscription(id: string): Promise + getPremiumSubscriptions(): Promise + cancelPremiumSubscription(id: string): Promise } interface CacheUtils { @@ -685,3 +685,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..d3ec59c1 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: parseDate(subscription.endsAt), + 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..6bbaf865 100644 --- a/utils/PremiumTypeUtils.tsx +++ b/utils/PremiumTypeUtils.tsx @@ -76,6 +76,16 @@ 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.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 => { From 2b5d165b42a14fa32adaf9e3f6ecd86099b24aea Mon Sep 17 00:00:00 2001 From: Matthias Luger Date: Sat, 19 Oct 2024 21:55:23 +0200 Subject: [PATCH 5/8] add purchase premium subscription endpoint --- api/ApiHelper.tsx | 45 ++++++++++++++++--- api/ApiTypes.d.tsx | 3 +- components/AccountDetails/AccountDetails.tsx | 27 +++++++++-- .../BuyPremiumSubscription.tsx | 2 +- components/Premium/Premium.tsx | 23 ++++++++-- .../Premium/PremiumStatus/PremiumStatus.tsx | 31 +++++++++++-- global.d.ts | 14 +++++- utils/Parser/APIResponseParser.tsx | 11 +++++ utils/PremiumTypeUtils.tsx | 10 +++++ 9 files changed, 145 insertions(+), 21 deletions(-) diff --git a/api/ApiHelper.tsx b/api/ApiHelper.tsx index 7cb59409..ee42e44c 100644 --- a/api/ApiHelper.tsx +++ b/api/ApiHelper.tsx @@ -30,6 +30,7 @@ import { parsePlayer, parsePopularSearch, parsePremiumProducts, + parsePremiumSubscription, parsePrivacySettings, parseProfitableCrafts, parseRecentAuction, @@ -2568,7 +2569,36 @@ export function initAPI(returnSSRResponse: boolean = false): API { }) } - let getPremiumSubscription = (): Promise => { + let purchasePremiumSubscription = (productSlug: string): Promise => { + return new Promise((resolve, reject) => { + let googleId = sessionStorage.getItem('googleId') + if (!googleId) { + toast.error('You need to be logged in to do linkvertise tasks.') + reject() + return + } + + httpApi.sendApiRequest({ + type: RequestType.PURCHASE_PREMIUM_SUBSCRIPTION, + customRequestURL: `${getApiEndpoint()}/premium/subscription/${productSlug}`, + requestMethod: 'POST', + data: '', + requestHeader: { + GoogleToken: googleId, + 'Content-Type': 'application/json' + }, + resolve: data => { + resolve(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) { @@ -2586,8 +2616,8 @@ export function initAPI(returnSSRResponse: boolean = false): API { GoogleToken: googleId, 'Content-Type': 'application/json' }, - resolve: () => { - resolve() + resolve: (subscriptions: any = []) => { + resolve(subscriptions.map(parsePremiumSubscription)) }, reject: (error: any) => { apiErrorHandler(RequestType.CREATE_PREMIUM_SUBSCRIPTION, error) @@ -2597,11 +2627,11 @@ export function initAPI(returnSSRResponse: boolean = false): API { }) } - let deletePremiumSubscription = (id: string): Promise => { + 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 delete a premium subscription.') + toast.error('You need to be logged in to cancel a premium subscription.') reject() return } @@ -2719,8 +2749,9 @@ export function initAPI(returnSSRResponse: boolean = false): API { requestArchivedAuctions, exportArchivedAuctionsData, getLinkvertiseLink, - getPremiumSubscription, - deletePremiumSubscription + getPremiumSubscriptions, + cancelPremiumSubscription, + purchasePremiumSubscription } } diff --git a/api/ApiTypes.d.tsx b/api/ApiTypes.d.tsx index 2a03ca47..38b4f926 100644 --- a/api/ApiTypes.d.tsx +++ b/api/ApiTypes.d.tsx @@ -95,7 +95,8 @@ export enum RequestType { EXPORT_ARCHIVED_AUCTIONS = 'exportArchivedAuctions', GET_LINKVERTISE_LINK = 'getLinkvertiseLink', CREATE_PREMIUM_SUBSCRIPTION = 'createPremiumSubscription', - DELETE_PREMIUM_SUBSCRIPTION = 'deletePremiumSubscription' + 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..a5b974a2 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.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: + + ))} + {productsToShow?.map(product => (

  • {getProductListEntry(product)}
  • ))} @@ -70,7 +93,7 @@ function PremiumStatus(props: Props) { Premium Status: - {highestPriorityProduct ? getProductListEntry(highestPriorityProduct) : 'No Premium'} + {highestPriorityProduct ? getProductListEntry(highestPriorityProduct!) : 'No Premium'}

    )} diff --git a/global.d.ts b/global.d.ts index f666930f..c6d28c51 100644 --- a/global.d.ts +++ b/global.d.ts @@ -269,8 +269,9 @@ interface API { requestArchivedAuctions(itemTag: string, itemFilter?: ItemFilter): Promise exportArchivedAuctionsData(itemTag: string, itemFilter: ItemFilter, discordWebhookUrl: string, flags: string[]): Promise getLinkvertiseLink(): Promise - getPremiumSubscription(): Promise - deletePremiumSubscription(id: string): Promise + getPremiumSubscriptions(): Promise + cancelPremiumSubscription(id: string): Promise + purchasePremiumSubscription(productSlug: string): Promise } interface CacheUtils { @@ -685,3 +686,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..d3ec59c1 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: parseDate(subscription.endsAt), + 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..6bbaf865 100644 --- a/utils/PremiumTypeUtils.tsx +++ b/utils/PremiumTypeUtils.tsx @@ -76,6 +76,16 @@ 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.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 => { From 5a846259cbabd4802416ea22d68cd0b9d96e5163 Mon Sep 17 00:00:00 2001 From: Matthias Luger Date: Tue, 5 Nov 2024 23:17:13 +0100 Subject: [PATCH 6/8] fixed typo issue added confirmation dialog --- api/ApiHelper.tsx | 13 +-- components/Premium/BuyPremium/BuyPremium.tsx | 91 +++++++++++++------ .../BuyPremiumConfirmationDialog.module.css | 5 + .../BuyPremiumConfirmationDialog.tsx | 28 ++++-- .../BuyPremiumSubscription.tsx | 32 ------- .../Premium/PremiumStatus/PremiumStatus.tsx | 10 +- global.d.ts | 2 +- 7 files changed, 97 insertions(+), 84 deletions(-) create mode 100644 components/Premium/BuyPremiumConfirmationDialog/BuyPremiumConfirmationDialog.module.css rename components/Premium/{BuyPremium => BuyPremiumConfirmationDialog}/BuyPremiumConfirmationDialog.tsx (75%) delete mode 100644 components/Premium/BuyPremiumSubscription/BuyPremiumSubscription.tsx diff --git a/api/ApiHelper.tsx b/api/ApiHelper.tsx index b03bf4de..f923e3ff 100644 --- a/api/ApiHelper.tsx +++ b/api/ApiHelper.tsx @@ -2569,26 +2569,19 @@ export function initAPI(returnSSRResponse: boolean = false): API { }) } - let purchasePremiumSubscription = (productSlug: string): Promise => { + let purchasePremiumSubscription = (productSlug: string, googleToken: string): Promise => { return new Promise((resolve, reject) => { - let googleId = sessionStorage.getItem('googleId') - if (!googleId) { - toast.error('You need to be logged in to do linkvertise tasks.') - reject() - return - } - httpApi.sendApiRequest({ type: RequestType.PURCHASE_PREMIUM_SUBSCRIPTION, customRequestURL: `${getApiEndpoint()}/premium/subscription/${productSlug}`, requestMethod: 'POST', data: '', requestHeader: { - GoogleToken: googleId, + GoogleToken: googleToken, 'Content-Type': 'application/json' }, resolve: data => { - resolve(data) + resolve(parsePaymentResponse(data)) }, reject: (error: any) => { apiErrorHandler(RequestType.PURCHASE_PREMIUM_SUBSCRIPTION, error) diff --git a/components/Premium/BuyPremium/BuyPremium.tsx b/components/Premium/BuyPremium/BuyPremium.tsx index 21cf0fb9..aa21df71 100644 --- a/components/Premium/BuyPremium/BuyPremium.tsx +++ b/components/Premium/BuyPremium/BuyPremium.tsx @@ -1,17 +1,15 @@ 'use client' import { ChangeEvent, useState } from 'react' -import { Button, Card, Form, Modal, ToggleButton, ToggleButtonGroup } from 'react-bootstrap' +import { Button, Card, Form, ToggleButton, ToggleButtonGroup } from 'react-bootstrap' import { toast } from 'react-toastify' import api from '../../../api/ApiHelper' import { CUSTOM_EVENTS } from '../../../api/ApiTypes.d' import { useCoflCoins } from '../../../utils/Hooks' -import { getPremiumType, PREMIUM_TYPES } from '../../../utils/PremiumTypeUtils' +import { PREMIUM_TYPES } from '../../../utils/PremiumTypeUtils' import { CoflCoinsDisplay } from '../../CoflCoins/CoflCoinsDisplay' import Number from '../../Number/Number' import styles from './BuyPremium.module.css' -import BuyPremiumConfirmationDialog from './BuyPremiumConfirmationDialog' -import Tooltip from '../../Tooltip/Tooltip' -import BuyPremiumSubscription from '../BuyPremiumSubscription/BuyPremiumSubscription' +import BuyPremiumConfirmationDialog from '../BuyPremiumConfirmationDialog/BuyPremiumConfirmationDialog' interface Props { activePremiumProduct: PremiumProduct @@ -23,7 +21,8 @@ function BuyPremium(props: Props) { let [purchaseSuccessfulOption, setPurchaseSuccessfulDuration] = useState() let [isPurchasing, setIsPurchasing] = useState(false) let [purchasePremiumOption, setPurchasePremiumOption] = useState(PREMIUM_TYPES[0].options[0]) - let [showConfirmationDialog, setShowConfirmationDialog] = useState(false) + let [showPrepaidConfirmationDialog, setShowPrepaidConfirmationDialog] = useState(false) + let [showSubscriptionConfirmationDialog, setShowSubscriptionConfirmationDialog] = useState(false) let coflCoins = useCoflCoins() function onDurationChange(event: ChangeEvent) { @@ -40,7 +39,7 @@ function BuyPremium(props: Props) { } function onPremiumBuy(googleToken: string) { - setShowConfirmationDialog(false) + setShowPrepaidConfirmationDialog(false) setIsPurchasing(true) api.purchaseWithCoflcoins(purchasePremiumOption.productId, googleToken, purchasePremiumOption.value).then(() => { @@ -56,14 +55,42 @@ function BuyPremium(props: Props) { }) } + function onSubscriptionBuy(googleToken: string) { + let productId = '' + if (purchasePremiumType.productId === 'premium') { + productId = 'l_premium' + } + if (purchasePremiumType.productId === 'premium_plus') { + productId = 'l_prem_plus' + } + + api.purchasePremiumSubscription(productId, googleToken).then(data => { + window.open(data.directLink, '_self') + }) + } + function onPremiumBuyCancel() { - setShowConfirmationDialog(false) + setShowPrepaidConfirmationDialog(false) + } + + function onSubscriptionBuyCancel() { + setShowSubscriptionConfirmationDialog(false) } function getPurchasePrice() { return purchasePremiumOption.value * purchasePremiumOption.price } + function getSubscriptionPrice() { + if (purchasePremiumType.productId === 'premium') { + return 8.69 + } + if (purchasePremiumType.productId === 'premium_plus') { + return 29.69 + } + return -1 + } + function getDurationString(): string { let durationString = purchasePremiumType.durationString let duration = +purchasePremiumOption.value @@ -153,26 +180,17 @@ function BuyPremium(props: Props) { ) : null}

    This is a prepaid service. We won't automatically charge you after your premium time runs out!

    - {purchasePremiumType.productId === 'premium' ? ( -

    - If you want to it to automatically renew itself,{' '} - click here} - tooltipContent={} - tooltipTitle={Purchase Premium Subscription} - /> -

    - ) : null} - {purchasePremiumType.productId === 'premium_plus' ? ( + {purchasePremiumType.productId === 'premium' || purchasePremiumType.productId === 'premium_plus' ? (

    If you want to it to automatically renew itself,{' '} - click here} - tooltipContent={} - tooltipTitle={Purchase Prmeium+ Subscription} - /> + { + setShowSubscriptionConfirmationDialog(true) + }} + style={{ color: '#007bff', cursor: 'pointer' }} + > + click here +

    ) : null}
    @@ -180,7 +198,7 @@ function BuyPremium(props: Props) { style={{ marginTop: '10px' }} variant="success" onClick={() => { - setShowConfirmationDialog(true) + setShowPrepaidConfirmationDialog(true) }} disabled={getPurchasePrice() > coflCoins || isPurchasing} > @@ -205,15 +223,30 @@ function BuyPremium(props: Props) { + CoflCoins + + } purchasePremiumType={purchasePremiumType} onHide={onPremiumBuyCancel} onConfirm={onPremiumBuy} activePremiumProduct={props.activePremiumProduct} /> + {getSubscriptionPrice()} per month} + purchasePremiumType={purchasePremiumType} + onHide={onSubscriptionBuyCancel} + onConfirm={onSubscriptionBuy} + activePremiumProduct={props.activePremiumProduct} + /> ) } diff --git a/components/Premium/BuyPremiumConfirmationDialog/BuyPremiumConfirmationDialog.module.css b/components/Premium/BuyPremiumConfirmationDialog/BuyPremiumConfirmationDialog.module.css new file mode 100644 index 00000000..ac0d3a08 --- /dev/null +++ b/components/Premium/BuyPremiumConfirmationDialog/BuyPremiumConfirmationDialog.module.css @@ -0,0 +1,5 @@ +.label { + width: 200px; + float: left; + margin: 0; +} \ No newline at end of file diff --git a/components/Premium/BuyPremium/BuyPremiumConfirmationDialog.tsx b/components/Premium/BuyPremiumConfirmationDialog/BuyPremiumConfirmationDialog.tsx similarity index 75% rename from components/Premium/BuyPremium/BuyPremiumConfirmationDialog.tsx rename to components/Premium/BuyPremiumConfirmationDialog/BuyPremiumConfirmationDialog.tsx index ff9845bb..bb440a00 100644 --- a/components/Premium/BuyPremium/BuyPremiumConfirmationDialog.tsx +++ b/components/Premium/BuyPremiumConfirmationDialog/BuyPremiumConfirmationDialog.tsx @@ -1,18 +1,19 @@ 'use client' import { Button, Modal } from 'react-bootstrap' -import styles from './BuyPremium.module.css' -import Number from '../../Number/Number' +import styles from './BuyPremiumConfirmationDialog.module.css' import { getPremiumType } from '../../../utils/PremiumTypeUtils' import { useState } from 'react' import { GoogleLogin } from '@react-oauth/google' import { toast } from 'react-toastify' +import { duration } from 'moment' interface Props { + type: 'prepaid' | 'subscription' show: boolean purchasePremiumType: PremiumType purchasePremiumOption: PremiumTypeOption - durationString: string - purchasePrice: number + durationString?: JSX.Element | string + purchasePrice: JSX.Element | string activePremiumProduct: PremiumProduct onHide() onConfirm(googleToken: string) @@ -38,16 +39,23 @@ export default function BuyPremiumConfirmationDialog(props: Props) { Type: {props.purchasePremiumType.label} -
  • - Duration: - {props.purchasePremiumOption.label} {props.durationString} -
  • + {props.durationString && ( +
  • + Duration: + {props.purchasePremiumOption.label} {props.durationString} +
  • + )}
  • Price: - CoflCoins + {props.purchasePrice}
  • -

    The time will be added to account. After you confirmed the purchase, it can't be canceled/moved to another account

    + {props.type === 'prepaid' && ( +

    The time will be added to account. After you confirmed the purchase, it can't be canceled/moved to another account

    + )} + {props.type === 'subscription' && ( +

    This subscription will be automatically renewed every month. It can be canceled at any time and will then run out.

    + )} {props.activePremiumProduct && getPremiumType(props.activePremiumProduct)?.productId !== props.purchasePremiumType.productId ? (

    diff --git a/components/Premium/BuyPremiumSubscription/BuyPremiumSubscription.tsx b/components/Premium/BuyPremiumSubscription/BuyPremiumSubscription.tsx deleted file mode 100644 index 451e9215..00000000 --- a/components/Premium/BuyPremiumSubscription/BuyPremiumSubscription.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react' -import GenericProviderPurchaseCard from '../../CoflCoins/GenericProviderPurchaseCard' -import api from '../../../api/ApiHelper' - -interface Props { - productId: string - price: number -} - -const BuyPremiumSubscription = (props: Props) => { - function onPayLemonSqueezy() { - api.purchasePremiumSubscription(props.productId).then(data => { - window.open(data.directLink, '_self') - }) - } - - return ( - <> -

    Click the button below to purchase a premium subscription. This subscription will be automatically renewed every month.

    - - - ) -} - -export default BuyPremiumSubscription diff --git a/components/Premium/PremiumStatus/PremiumStatus.tsx b/components/Premium/PremiumStatus/PremiumStatus.tsx index 8a3635cc..f6707f41 100644 --- a/components/Premium/PremiumStatus/PremiumStatus.tsx +++ b/components/Premium/PremiumStatus/PremiumStatus.tsx @@ -6,7 +6,6 @@ import { getHighestPriorityPremiumProduct, getPremiumLabelForSubscription, getPr import Tooltip from '../../Tooltip/Tooltip' import styles from './PremiumStatus.module.css' import { Button } from 'react-bootstrap' -import api from '../../../api/ApiHelper' interface Props { products: PremiumProduct[] @@ -77,7 +76,14 @@ function PremiumStatus(props: Props) { ) } /> - diff --git a/global.d.ts b/global.d.ts index c6d28c51..bc53fd07 100644 --- a/global.d.ts +++ b/global.d.ts @@ -271,7 +271,7 @@ interface API { getLinkvertiseLink(): Promise getPremiumSubscriptions(): Promise cancelPremiumSubscription(id: string): Promise - purchasePremiumSubscription(productSlug: string): Promise + purchasePremiumSubscription(productSlug: string, googleToken: string): Promise } interface CacheUtils { From 6202f613097d60b2143e1ec116b35a3545cda3ac Mon Sep 17 00:00:00 2001 From: Matthias Luger Date: Wed, 6 Nov 2024 22:19:33 +0100 Subject: [PATCH 7/8] add cancel dialog and fix premium status display --- components/AccountDetails/AccountDetails.tsx | 4 +- .../TransactionHistory/TransactionHistory.tsx | 6 ++ .../CancelSubscriptionConfirmDialog.tsx | 33 ++++++++++ components/Premium/Premium.tsx | 2 +- .../Premium/PremiumStatus/PremiumStatus.tsx | 65 ++++++++++++++----- global.d.ts | 2 +- utils/Parser/APIResponseParser.tsx | 2 +- utils/PremiumTypeUtils.tsx | 3 + 8 files changed, 94 insertions(+), 23 deletions(-) create mode 100644 components/Premium/CancelSubscriptionConfirmDialog/CancelSubscriptionConfirmDialog.tsx diff --git a/components/AccountDetails/AccountDetails.tsx b/components/AccountDetails/AccountDetails.tsx index a5b974a2..3ee142a9 100644 --- a/components/AccountDetails/AccountDetails.tsx +++ b/components/AccountDetails/AccountDetails.tsx @@ -60,8 +60,8 @@ function AccountDetails() { function loadPremiumSubscriptions(): Promise { return api.getPremiumSubscriptions().then(subscriptions => { - subscriptions = subscriptions.filter(subscription => subscription.endsAt.getTime() > new Date().getTime()) - setPremiumSubscriptions(subscriptions) + subscriptions = subscriptions.filter(subscription => !subscription.endsAt || subscription.endsAt.getTime() > new Date().getTime()) + setPremiumSubscriptions([...subscriptions]) }) } diff --git a/components/AccountDetails/TransactionHistory/TransactionHistory.tsx b/components/AccountDetails/TransactionHistory/TransactionHistory.tsx index 5473b38c..216b6b8d 100644 --- a/components/AccountDetails/TransactionHistory/TransactionHistory.tsx +++ b/components/AccountDetails/TransactionHistory/TransactionHistory.tsx @@ -54,6 +54,12 @@ function TransactionHistory() { if (transaction.productId.startsWith('pre_api')) { return 'Bought Pre-API' } + if (transaction.productId.startsWith('l_premium')) { + return 'Bought Premium (Subscription)' + } + if (transaction.productId.startsWith('l_prem_plus')) { + return 'Bought Premium+ (Subscription)' + } const parts = transaction.reference.split('.') let suffix = '' diff --git a/components/Premium/CancelSubscriptionConfirmDialog/CancelSubscriptionConfirmDialog.tsx b/components/Premium/CancelSubscriptionConfirmDialog/CancelSubscriptionConfirmDialog.tsx new file mode 100644 index 00000000..01b88cc9 --- /dev/null +++ b/components/Premium/CancelSubscriptionConfirmDialog/CancelSubscriptionConfirmDialog.tsx @@ -0,0 +1,33 @@ +import React from 'react' +import { Modal, Button } from 'react-bootstrap' + +interface CancelSubscriptionConfirmDialogProps { + show: boolean + onHide: () => void + onConfirm: () => void +} + +const CancelSubscriptionConfirmDialog = ({ show, onHide, onConfirm }: CancelSubscriptionConfirmDialogProps) => { + return ( + + + Confirmation + + +
    +

    Are you sure you want to cancel your subscription?

    +
    + + +
    +
    +
    +
    + ) +} + +export default CancelSubscriptionConfirmDialog diff --git a/components/Premium/Premium.tsx b/components/Premium/Premium.tsx index 7097fb8a..beec91f6 100644 --- a/components/Premium/Premium.tsx +++ b/components/Premium/Premium.tsx @@ -49,7 +49,7 @@ function Premium() { function loadPremiumSubscriptions(): Promise { return api.getPremiumSubscriptions().then(subscriptions => { - subscriptions = subscriptions.filter(subscription => subscription.endsAt.getTime() > new Date().getTime()) + subscriptions = subscriptions.filter(subscription => !subscription.endsAt || subscription.endsAt.getTime() > new Date().getTime()) setPremiumSubscriptions(subscriptions) }) } diff --git a/components/Premium/PremiumStatus/PremiumStatus.tsx b/components/Premium/PremiumStatus/PremiumStatus.tsx index f6707f41..a137ec6e 100644 --- a/components/Premium/PremiumStatus/PremiumStatus.tsx +++ b/components/Premium/PremiumStatus/PremiumStatus.tsx @@ -5,7 +5,8 @@ import { getLocalDateAndTime } from '../../../utils/Formatter' import { getHighestPriorityPremiumProduct, getPremiumLabelForSubscription, getPremiumType } from '../../../utils/PremiumTypeUtils' import Tooltip from '../../Tooltip/Tooltip' import styles from './PremiumStatus.module.css' -import { Button } from 'react-bootstrap' +import { CancelOutlined } from '@mui/icons-material' +import CancelSubscriptionConfirmDialog from '../CancelSubscriptionConfirmDialog/CancelSubscriptionConfirmDialog' interface Props { products: PremiumProduct[] @@ -17,6 +18,7 @@ interface Props { function PremiumStatus(props: Props) { let [highestPriorityProduct, setHighestPriorityProduct] = useState() let [productsToShow, setProductsToShow] = useState() + let [showCancelSubscriptionDialogSubscription, setShowCancelSubscriptionDialogSubscription] = useState() useEffect(() => { let products = props.products @@ -53,10 +55,12 @@ function PremiumStatus(props: Props) { ) } + let numberOfEntriesToShow = (productsToShow?.length || 0) + (props.subscriptions?.length || 0) + return ( <>
    - {(productsToShow && productsToShow.length > 1) || (props.subscriptions && props.subscriptions.length > 1) ? ( + {numberOfEntriesToShow > 1 ? (
    Premium Status: @@ -67,25 +71,38 @@ function PremiumStatus(props: Props) { {' '} {getPremiumLabelForSubscription(subscription)} (Subscription)} + content={ + + {getPremiumLabelForSubscription(subscription)} (Subscription){' '} + {subscription.endsAt && Canceled} + + } tooltipContent={ - subscription.endsAt ? ( - ends at {getLocalDateAndTime(subscription.endsAt)} - ) : ( - renews at {getLocalDateAndTime(subscription.renewsAt)} - ) + + {subscription.endsAt ? ( + Ends at {getLocalDateAndTime(subscription.endsAt)} + ) : ( + Renews at {getLocalDateAndTime(subscription.renewsAt)} + )} + } /> - + {!subscription.endsAt && ( + + { + setShowCancelSubscriptionDialogSubscription(subscription) + }} + /> + + } + tooltipContent={Cancel subscription} + /> + )} ))} {productsToShow?.map(product => ( @@ -103,6 +120,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 bc53fd07..1808dda1 100644 --- a/global.d.ts +++ b/global.d.ts @@ -689,7 +689,7 @@ interface ArchivedAuctionResponse { interface PremiumSubscription { externalId: string - endsAt: Date + endsAt?: Date productName: string paymentAmount: string renewsAt: Date diff --git a/utils/Parser/APIResponseParser.tsx b/utils/Parser/APIResponseParser.tsx index d3ec59c1..e2940f1d 100644 --- a/utils/Parser/APIResponseParser.tsx +++ b/utils/Parser/APIResponseParser.tsx @@ -716,7 +716,7 @@ export function parseArchivedAuctions(archivedAuctionsResponse: any): ArchivedAu export function parsePremiumSubscription(subscription: any): PremiumSubscription { return { externalId: subscription.externalId, - endsAt: parseDate(subscription.endsAt), + endsAt: subscription.endsAt ? parseDate(subscription.endsAt) : undefined, productName: subscription.productName, paymentAmount: subscription.paymentAmount, renewsAt: parseDate(subscription.renewsAt), diff --git a/utils/PremiumTypeUtils.tsx b/utils/PremiumTypeUtils.tsx index 6bbaf865..79118926 100644 --- a/utils/PremiumTypeUtils.tsx +++ b/utils/PremiumTypeUtils.tsx @@ -77,6 +77,9 @@ export function getPremiumType(product: PremiumProduct) { } export function getPremiumLabelForSubscription(subscription: PremiumSubscription) { + if (!subscription.productName) { + return 'Unknown' + } if (subscription.productName.includes('prem_plus')) { return 'Premium+' } From dc7314f3e84ec482caf28b59b8984fd25126a1ef Mon Sep 17 00:00:00 2001 From: Matthias Luger Date: Wed, 6 Nov 2024 23:35:42 +0100 Subject: [PATCH 8/8] don't show premium subscription purchase, if you already have a subscription --- components/Premium/BuyPremium/BuyPremium.tsx | 4 +++- components/Premium/Premium.tsx | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/components/Premium/BuyPremium/BuyPremium.tsx b/components/Premium/BuyPremium/BuyPremium.tsx index aa21df71..1155f37a 100644 --- a/components/Premium/BuyPremium/BuyPremium.tsx +++ b/components/Premium/BuyPremium/BuyPremium.tsx @@ -13,6 +13,7 @@ import BuyPremiumConfirmationDialog from '../BuyPremiumConfirmationDialog/BuyPre interface Props { activePremiumProduct: PremiumProduct + premiumSubscriptions: PremiumSubscription[] onNewActivePremiumProduct() } @@ -180,7 +181,8 @@ function BuyPremium(props: Props) {
    ) : null}

    This is a prepaid service. We won't automatically charge you after your premium time runs out!

    - {purchasePremiumType.productId === 'premium' || purchasePremiumType.productId === 'premium_plus' ? ( + {(purchasePremiumType.productId === 'premium' || purchasePremiumType.productId === 'premium_plus') && + (!props.premiumSubscriptions || props.premiumSubscriptions.length === 0) ? (

    If you want to it to automatically renew itself,{' '} {isLoggedIn ? (

    - +
    ) : null} {isLoggedIn ? (