diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/components/options/CategorySkipOptionsComponent.tsx | 40 | ||||
-rw-r--r-- | src/components/options/ToggleOptionComponent.tsx | 7 | ||||
-rw-r--r-- | src/config.ts | 18 | ||||
-rw-r--r-- | src/content.ts | 2 | ||||
-rw-r--r-- | src/popup.ts | 9 | ||||
-rw-r--r-- | src/svg-icons/lock_svg.tsx | 22 | ||||
-rw-r--r-- | src/upsell.ts | 71 | ||||
-rw-r--r-- | src/utils/licenseKey.ts | 65 |
8 files changed, 223 insertions, 11 deletions
diff --git a/src/components/options/CategorySkipOptionsComponent.tsx b/src/components/options/CategorySkipOptionsComponent.tsx index b298347e..338435a8 100644 --- a/src/components/options/CategorySkipOptionsComponent.tsx +++ b/src/components/options/CategorySkipOptionsComponent.tsx @@ -6,6 +6,8 @@ import { Category, CategorySkipOption } from "../../types"; import { getCategorySuffix } from "../../utils/categoryUtils"; import ToggleOptionComponent, { ToggleOptionProps } from "./ToggleOptionComponent"; +import { fetchingChaptersAllowed } from "../../utils/licenseKey"; +import LockSvg from "../../svg-icons/lock_svg"; export interface CategorySkipOptionsProps { category: Category; @@ -16,6 +18,7 @@ export interface CategorySkipOptionsProps { export interface CategorySkipOptionsState { color: string; previewColor: string; + hideChapter: boolean; } class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsProps, CategorySkipOptionsState> { @@ -28,7 +31,14 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr this.state = { color: props.defaultColor || Config.config.barTypes[this.props.category]?.color, previewColor: props.defaultPreviewColor || Config.config.barTypes["preview-" + this.props.category]?.color, - } + hideChapter: true + }; + + fetchingChaptersAllowed().then((allowed) => { + this.setState({ + hideChapter: !allowed + }); + }) } render(): React.ReactElement { @@ -52,12 +62,25 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr } } + let extraClasses = ""; + const disabled = this.props.category === "chapter" && this.state.hideChapter; + if (disabled) { + extraClasses += " disabled"; + + if (!Config.config.showUpsells) { + return <></>; + } + } + return ( <> <tr id={this.props.category + "OptionsRow"} - className="categoryTableElement"> + className={`categoryTableElement${extraClasses}`} > <td id={this.props.category + "OptionName"} className="categoryTableLabel"> + {disabled && + <LockSvg className="upsellButton" onClick={() => chrome.tabs.create({url: chrome.runtime.getURL('upsell/index.html')})}/> + } {chrome.i18n.getMessage("category_" + this.props.category)} </td> @@ -66,6 +89,7 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr <select className="optionsSelector" defaultValue={defaultOption} + disabled={disabled} onChange={this.skipOptionSelected.bind(this)}> {this.getCategorySkipOptions()} </select> @@ -77,6 +101,7 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr <input className="categoryColorTextBox option-text-box" type="color" + disabled={disabled} onChange={(event) => this.setColorState(event, false)} value={this.state.color} /> </td> @@ -96,7 +121,7 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr </tr> <tr id={this.props.category + "DescriptionRow"} - className="small-description categoryTableDescription"> + className={`small-description categoryTableDescription${extraClasses}`}> <td colSpan={2}> {chrome.i18n.getMessage("category_" + this.props.category + "_description")} @@ -107,7 +132,7 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr </td> </tr> - {this.getExtraOptionComponents(this.props.category)} + {this.getExtraOptionComponents(this.props.category, extraClasses, disabled)} </> ); @@ -191,15 +216,16 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr }, 50); } - getExtraOptionComponents(category: string): JSX.Element[] { + getExtraOptionComponents(category: string, extraClasses: string, disabled: boolean): JSX.Element[] { const result = []; for (const option of this.getExtraOptions(category)) { result.push( - <tr key={option.configKey}> + <tr key={option.configKey} className={extraClasses}> <td id={`${category}_${option.configKey}`} className="categoryExtraOptions"> <ToggleOptionComponent configKey={option.configKey} - label={option.label} + label={option.label} + disabled={disabled} /> </td> </tr> diff --git a/src/components/options/ToggleOptionComponent.tsx b/src/components/options/ToggleOptionComponent.tsx index b67024f4..c6ea698d 100644 --- a/src/components/options/ToggleOptionComponent.tsx +++ b/src/components/options/ToggleOptionComponent.tsx @@ -5,6 +5,7 @@ import Config from "../../config"; export interface ToggleOptionProps { configKey: string; label: string; + disabled?: boolean; } export interface ToggleOptionState { @@ -27,7 +28,11 @@ class ToggleOptionComponent extends React.Component<ToggleOptionProps, ToggleOpt <div> <div className="switch-container"> <label className="switch"> - <input id={this.props.configKey} type="checkbox" checked={this.state.enabled} onChange={(e) => this.clicked(e)}/> + <input id={this.props.configKey} + type="checkbox" + checked={this.state.enabled} + disabled={this.props.disabled} + onChange={(e) => this.clicked(e)}/> <span className="slider round"></span> </label> <label className="switch-label" htmlFor={this.props.configKey}> diff --git a/src/config.ts b/src/config.ts index 60286f40..2fc48814 100644 --- a/src/config.ts +++ b/src/config.ts @@ -50,6 +50,7 @@ interface SBConfig { allowExpirements: boolean, showDonationLink: boolean, showPopupDonationCount: number, + showUpsells: boolean, donateClicked: number, autoHideInfoButton: boolean, autoSkipOnMusicVideos: boolean, @@ -81,6 +82,13 @@ interface SBConfig { // What categories should be skipped categorySelections: CategorySelection[], + payments: { + licenseKey: string, + lastCheck: number, + freeAccess: boolean, + chaptersAllowed: boolean + } + // Preview bar barTypes: { "preview-chooseACategory": PreviewBarOption, @@ -140,7 +148,7 @@ const Config: SBObject = { permissions: {}, unsubmittedSegments: {}, defaultCategory: "chooseACategory" as Category, - renderSegmentsAsChapters: true, + renderSegmentsAsChapters: false, whitelistedChannels: [], forceChannelCheck: false, minutesSaved: 0, @@ -176,6 +184,7 @@ const Config: SBObject = { allowExpirements: true, showDonationLink: true, showPopupDonationCount: 0, + showUpsells: true, donateClicked: 0, autoHideInfoButton: true, autoSkipOnMusicVideos: false, @@ -211,6 +220,13 @@ const Config: SBObject = { option: CategorySkipOption.ShowOverlay }], + payments: { + licenseKey: null, + lastCheck: 0, + freeAccess: false, + chaptersAllowed: false + }, + colorPalette: { red: "#780303", white: "#ffffff", diff --git a/src/content.ts b/src/content.ts index f1ce8c36..a1f22fbc 100644 --- a/src/content.ts +++ b/src/content.ts @@ -1026,7 +1026,7 @@ function importExistingChapters(wait: boolean) { existingChaptersImported = true; updatePreviewBar(); } - }); + }).catch(() => {}); // eslint-disable-line @typescript-eslint/no-empty-function } } diff --git a/src/popup.ts b/src/popup.ts index 9d60b058..60c875a1 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -255,7 +255,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> { PageElements.showNoticeAgain.style.display = "unset"; } - utils.sendRequestToServer("GET", "/api/userInfo?value=userName&value=viewCount&value=minutesSaved&value=vip&value=permissions&userID=" + utils.sendRequestToServer("GET", "/api/userInfo?value=userName&value=viewCount&value=minutesSaved&value=vip&value=permissions&value=freeChaptersAccess&userID=" + Config.config.userID, (res) => { if (res.status === 200) { const userInfo = JSON.parse(res.responseText); @@ -286,6 +286,13 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> { Config.config.isVip = userInfo.vip; Config.config.permissions = userInfo.permissions; + + if (userInfo.freeChaptersAccess) { + Config.config.payments.chaptersAllowed = userInfo.freeChaptersAccess; + Config.config.payments.freeAccess = userInfo.freeChaptersAccess; + Config.config.payments.lastCheck = Date.now(); + Config.forceSyncUpdate("payments"); + } } }); diff --git a/src/svg-icons/lock_svg.tsx b/src/svg-icons/lock_svg.tsx new file mode 100644 index 00000000..ef077541 --- /dev/null +++ b/src/svg-icons/lock_svg.tsx @@ -0,0 +1,22 @@ +import * as React from "react"; + +const lockSvg = ({ + fill = "#fcba03", + className = "", + width = "20", + height = "20", + onClick +}): JSX.Element => ( + <svg + xmlns="http://www.w3.org/2000/svg" + height={width} + width={height} + className={className} + fill={fill} + onClick={onClick} > + <path + d="M5.5 18q-.625 0-1.062-.438Q4 17.125 4 16.5v-8q0-.625.438-1.062Q4.875 7 5.5 7H6V5q0-1.667 1.167-2.833Q8.333 1 10 1q1.667 0 2.833 1.167Q14 3.333 14 5v2h.5q.625 0 1.062.438Q16 7.875 16 8.5v8q0 .625-.438 1.062Q15.125 18 14.5 18Zm4.5-4q.625 0 1.062-.438.438-.437.438-1.062t-.438-1.062Q10.625 11 10 11t-1.062.438Q8.5 11.875 8.5 12.5t.438 1.062Q9.375 14 10 14ZM7.5 7h5V5q0-1.042-.729-1.771Q11.042 2.5 10 2.5q-1.042 0-1.771.729Q7.5 3.958 7.5 5Z"/> + </svg> +); + +export default lockSvg; diff --git a/src/upsell.ts b/src/upsell.ts new file mode 100644 index 00000000..faa94a1d --- /dev/null +++ b/src/upsell.ts @@ -0,0 +1,71 @@ +import Config from "./config"; +import { checkLicenseKey } from "./utils/licenseKey"; +import { localizeHtmlPage } from "./utils/pageUtils"; + +import * as countries from "../public/res/countries.json"; + +// This is needed, if Config is not imported before Utils, things break. +// Probably due to cyclic dependencies +Config.config; + +window.addEventListener('DOMContentLoaded', init); + +async function init() { + localizeHtmlPage(); + + const cantAfford = document.getElementById("cantAfford"); + const cantAffordTexts = chrome.i18n.getMessage("cantAfford").split(/{|}/); + cantAfford.appendChild(document.createTextNode(cantAffordTexts[0])); + const discountButton = document.createElement("span"); + discountButton.id = "discountButton"; + discountButton.innerText = cantAffordTexts[1]; + cantAfford.appendChild(discountButton); + cantAfford.appendChild(document.createTextNode(cantAffordTexts[2])); + + const redeemButton = document.getElementById("redeemButton") as HTMLInputElement; + redeemButton.addEventListener("click", async () => { + const licenseKey = redeemButton.value; + + if (await checkLicenseKey(licenseKey)) { + Config.config.payments.licenseKey = licenseKey; + Config.forceSyncUpdate("payments"); + + alert(chrome.i18n.getMessage("redeemSuccess")); + } else { + alert(chrome.i18n.getMessage("redeemFailed")); + } + }); + + discountButton.addEventListener("click", async () => { + const subsidizedSection = document.getElementById("subsidizedPrice"); + subsidizedSection.classList.remove("hidden"); + + const oldSelector = document.getElementById("countrySelector"); + if (oldSelector) oldSelector.remove(); + const countrySelector = document.createElement("select"); + countrySelector.id = "countrySelector"; + countrySelector.className = "optionsSelector"; + const defaultOption = document.createElement("option"); + defaultOption.innerText = chrome.i18n.getMessage("chooseACountry"); + countrySelector.appendChild(defaultOption); + + for (const country of Object.keys(countries)) { + const option = document.createElement("option"); + option.value = country; + option.innerText = country; + countrySelector.appendChild(option); + } + + countrySelector.addEventListener("change", () => { + if (countries[countrySelector.value]?.allowed) { + document.getElementById("subsidizedLink").classList.remove("hidden"); + document.getElementById("noSubsidizedLink").classList.add("hidden"); + } else { + document.getElementById("subsidizedLink").classList.add("hidden"); + document.getElementById("noSubsidizedLink").classList.remove("hidden"); + } + }); + + subsidizedSection.appendChild(countrySelector); + }); +}
\ No newline at end of file diff --git a/src/utils/licenseKey.ts b/src/utils/licenseKey.ts new file mode 100644 index 00000000..53c54c76 --- /dev/null +++ b/src/utils/licenseKey.ts @@ -0,0 +1,65 @@ +import Config from "../config"; +import Utils from "../utils"; +import * as CompileConfig from "../../config.json"; + +const utils = new Utils(); + +export async function checkLicenseKey(licenseKey: string): Promise<boolean> { + const result = await utils.asyncRequestToServer("GET", "/api/verifyToken", { + licenseKey + }); + + try { + if (result.ok && JSON.parse(result.responseText).allowed) { + Config.config.payments.chaptersAllowed = true; + Config.config.payments.lastCheck = Date.now(); + Config.forceSyncUpdate("payments"); + + return true; + } + } catch (e) { } //eslint-disable-line no-empty + + return false +} + +export async function fetchingChaptersAllowed(): Promise<boolean> { + if (Config.config.payments.freeAccess || CompileConfig["freeChapterAccess"]) { + return true; + } + + //more than 14 days + if (Config.config.payments.licenseKey && Date.now() - Config.config.payments.lastCheck > 14 * 24 * 60 * 60 * 1000) { + const licensePromise = checkLicenseKey(Config.config.payments.licenseKey); + + if (!Config.config.payments.chaptersAllowed) { + return licensePromise; + } + } + + if (Config.config.payments.chaptersAllowed) return true; + + if (Config.config.payments.lastCheck === 0) { + // Check for free access if no license key, and it is the first time + const result = await utils.asyncRequestToServer("GET", "/api/userInfo", { + value: "freeChaptersAccess", + userID: Config.config.userID + }); + + try { + if (result.ok) { + const userInfo = JSON.parse(result.responseText); + + Config.config.payments.lastCheck = Date.now(); + if (userInfo.freeChaptersAccess) { + Config.config.payments.freeAccess = true; + Config.config.payments.chaptersAllowed = true; + Config.forceSyncUpdate("payments"); + + return true; + } + } + } catch (e) { } //eslint-disable-line no-empty + } + + return false; +}
\ No newline at end of file |