aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAjay <[email protected]>2022-09-01 15:15:30 -0400
committerAjay <[email protected]>2022-09-01 15:15:30 -0400
commitc6e30236e973e7764582bf7a0b8f0f10bdc73fbb (patch)
tree52bded8a60ca682bfa81ce77bcd1486e8b911ee9
parent34c4ecf9409eeb13b731c59e62e3d62b13e5b5c1 (diff)
downloadSponsorBlock-c6e30236e973e7764582bf7a0b8f0f10bdc73fbb.tar.gz
SponsorBlock-c6e30236e973e7764582bf7a0b8f0f10bdc73fbb.zip
Add license requirement
-rw-r--r--public/_locales/en/messages.json47
-rw-r--r--public/options/options.css9
-rw-r--r--public/options/options.html12
-rw-r--r--public/res/countries.json1
-rw-r--r--public/upsell/index.html94
-rw-r--r--public/upsell/styles.css387
-rw-r--r--src/components/options/CategorySkipOptionsComponent.tsx40
-rw-r--r--src/components/options/ToggleOptionComponent.tsx7
-rw-r--r--src/config.ts18
-rw-r--r--src/content.ts2
-rw-r--r--src/popup.ts9
-rw-r--r--src/svg-icons/lock_svg.tsx22
-rw-r--r--src/upsell.ts71
-rw-r--r--src/utils/licenseKey.ts65
-rw-r--r--webpack/webpack.common.js3
15 files changed, 775 insertions, 12 deletions
diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json
index ab6c4e4c..ae14284a 100644
--- a/public/_locales/en/messages.json
+++ b/public/_locales/en/messages.json
@@ -1103,5 +1103,52 @@
"Import": {
"message": "Import",
"description": "Button to initiate importing segments. Appears under the textbox where they paste in the data"
+ },
+ "redeemSuccess": {
+ "message": "Reedem Successful!"
+ },
+ "redeemFailed": {
+ "message": "License key is invalid"
+ },
+ "hideUpsells": {
+ "message": "Hide options not available without extra payment"
+ },
+ "chooseACountry": {
+ "message": "Choose a country"
+ },
+ "noDiscount": {
+ "message": "You do not qualify for a discount"
+ },
+ "discountLink": {
+ "message": "Discount Link (See the pink price)"
+ },
+ "selectYourCountry": {
+ "message": "Select your country"
+ },
+ "alreadyDonated": {
+ "message": "If you've donated any amount before now, you may redeem free access by emailing:",
+ "description": "After the colon is an email address"
+ },
+ "cantAfford": {
+ "message": "If you can't afford to purchase a license, click {here} to see if you are eligible for a discount",
+ "description": "Keep the curly braces"
+ },
+ "patreonSignIn": {
+ "message": "Sign in with Patreon"
+ },
+ "redeem": {
+ "message": "Redeem"
+ },
+ "joinOnPatreon": {
+ "message": "Subscribe on Patreon"
+ },
+ "oneTimePurchase": {
+ "message": "One Time Purchase"
+ },
+ "enterLicenseKey": {
+ "message": "Enter License Key"
+ },
+ "chaptersPage1": {
+ "message": "SponsorBlock crowd-sourced chapters feature is only available to people who purchase a license, or for people who are granted access for free due their past contributions"
}
}
diff --git a/public/options/options.css b/public/options/options.css
index 36f4d5d7..2bd27161 100644
--- a/public/options/options.css
+++ b/public/options/options.css
@@ -317,6 +317,10 @@ input[type='number'] {
color: grey;
}
+tr.disabled {
+ opacity: 0.3;
+}
+
#options {
height: 100vh;
flex-basis: 80%;
@@ -678,4 +682,9 @@ svg {
#options > div {
max-width: 100%;
}
+}
+
+.upsellButton {
+ cursor: pointer;
+ vertical-align: middle;
} \ No newline at end of file
diff --git a/public/options/options.html b/public/options/options.html
index 150212ce..765134ec 100644
--- a/public/options/options.html
+++ b/public/options/options.html
@@ -302,6 +302,18 @@
</div>
</div>
+ <div data-type="toggle" data-toggle-type="reverse" data-sync="showUpsells" data-no-safari="true">
+ <div class="switch-container">
+ <label class="switch">
+ <input id="showUpsell" type="checkbox" checked>
+ <span class="slider round"></span>
+ </label>
+ <label class="switch-label" for="showUpsells">
+ __MSG_hideUpsells__
+ </label>
+ </div>
+ </div>
+
</div>
<div id="keybinds" class="option-group hidden">
diff --git a/public/res/countries.json b/public/res/countries.json
new file mode 100644
index 00000000..33839f29
--- /dev/null
+++ b/public/res/countries.json
@@ -0,0 +1 @@
+{"Albania":{"allowed":true},"Algeria":{"allowed":true},"Angola":{"allowed":true},"Argentina":{"allowed":true},"Armenia":{"allowed":true},"Australia":{"allowed":false},"Austria":{"allowed":false},"Azerbaijan":{"allowed":true},"Bangladesh":{"allowed":true},"Belarus":{"allowed":true},"Belgium":{"allowed":false},"Belize":{"allowed":true},"Benin":{"allowed":true},"Bhutan":{"allowed":true},"Bolivia":{"allowed":true},"Bosnia and Herzegovina":{"allowed":true},"Botswana":{"allowed":true},"Brazil":{"allowed":true},"Bulgaria":{"allowed":true},"Burkina Faso":{"allowed":true},"Burundi":{"allowed":true},"Cameroon":{"allowed":true},"Canada":{"allowed":false},"Central African Republic":{"allowed":true},"Chad":{"allowed":true},"Chile":{"allowed":true},"China":{"allowed":true},"Colombia":{"allowed":true},"Comoros":{"allowed":true},"Costa Rica":{"allowed":true},"Croatia":{"allowed":true},"Cyprus":{"allowed":false},"Czech Republic":{"allowed":false},"Denmark":{"allowed":false},"Djibouti":{"allowed":true},"Dominican Republic":{"allowed":true},"DR Congo":{"allowed":true},"Ecuador":{"allowed":true},"Egypt":{"allowed":true},"El Salvador":{"allowed":true},"Estonia":{"allowed":false},"Eswatini":{"allowed":true},"Ethiopia":{"allowed":true},"Fiji":{"allowed":true},"Finland":{"allowed":false},"France":{"allowed":false},"Gabon":{"allowed":true},"Gambia":{"allowed":true},"Georgia":{"allowed":true},"Germany":{"allowed":false},"Ghana":{"allowed":true},"Greece":{"allowed":true},"Guatemala":{"allowed":true},"Guinea":{"allowed":true},"Guinea-Bissau":{"allowed":true},"Guyana":{"allowed":true},"Haiti":{"allowed":true},"Honduras":{"allowed":true},"Hungary":{"allowed":true},"Iceland":{"allowed":false},"India":{"allowed":true},"Iran":{"allowed":true},"Iraq":{"allowed":true},"Ireland":{"allowed":false},"Israel":{"allowed":false},"Italy":{"allowed":false},"Ivory Coast":{"allowed":true},"Jamaica":{"allowed":true},"Japan":{"allowed":false},"Jordan":{"allowed":true},"Kazakhstan":{"allowed":true},"Kenya":{"allowed":true},"Kiribati":{"allowed":true},"Kyrgyzstan":{"allowed":true},"Laos":{"allowed":true},"Latvia":{"allowed":true},"Lebanon":{"allowed":true},"Lesotho":{"allowed":true},"Liberia":{"allowed":true},"Lithuania":{"allowed":true},"Luxembourg":{"allowed":false},"Madagascar":{"allowed":true},"Malawi":{"allowed":true},"Malaysia":{"allowed":true},"Maldives":{"allowed":true},"Mali":{"allowed":true},"Malta":{"allowed":false},"Mauritania":{"allowed":true},"Mauritius":{"allowed":true},"Mexico":{"allowed":true},"Micronesia":{"allowed":true},"Moldova":{"allowed":true},"Mongolia":{"allowed":true},"Montenegro":{"allowed":true},"Morocco":{"allowed":true},"Mozambique":{"allowed":true},"Myanmar":{"allowed":true},"Namibia":{"allowed":true},"Nepal":{"allowed":true},"Netherlands":{"allowed":false},"Nicaragua":{"allowed":true},"Niger":{"allowed":true},"Nigeria":{"allowed":true},"North Macedonia":{"allowed":true},"Norway":{"allowed":false},"Pakistan":{"allowed":true},"Panama":{"allowed":true},"Papua New Guinea":{"allowed":true},"Paraguay":{"allowed":true},"Peru":{"allowed":true},"Philippines":{"allowed":true},"Poland":{"allowed":true},"Portugal":{"allowed":true},"Republic of the Congo":{"allowed":true},"Romania":{"allowed":true},"Russia":{"allowed":true},"Rwanda":{"allowed":true},"Saint Lucia":{"allowed":true},"Samoa":{"allowed":true},"Sao Tome and Principe":{"allowed":true},"Senegal":{"allowed":true},"Serbia":{"allowed":true},"Seychelles":{"allowed":true},"Sierra Leone":{"allowed":true},"Slovakia":{"allowed":true},"Slovenia":{"allowed":false},"Solomon Islands":{"allowed":true},"South Africa":{"allowed":true},"South Korea":{"allowed":false},"South Sudan":{"allowed":true},"Spain":{"allowed":false},"Sri Lanka":{"allowed":true},"Sudan":{"allowed":true},"Suriname":{"allowed":true},"Sweden":{"allowed":false},"Switzerland":{"allowed":false},"Syria":{"allowed":true},"Taiwan":{"allowed":false},"Tajikistan":{"allowed":true},"Tanzania":{"allowed":true},"Thailand":{"allowed":true},"Timor-Leste":{"allowed":true},"Togo":{"allowed":true},"Tonga":{"allowed":true},"Trinidad and Tobago":{"allowed":true},"Tunisia":{"allowed":true},"Turkey":{"allowed":true},"Turkmenistan":{"allowed":true},"Tuvalu":{"allowed":true},"Uganda":{"allowed":true},"Ukraine":{"allowed":true},"United Arab Emirates":{"allowed":false},"United Kingdom":{"allowed":false},"United States":{"allowed":false},"Uruguay":{"allowed":true},"Uzbekistan":{"allowed":true},"Vanuatu":{"allowed":true},"Venezuela":{"allowed":true},"Vietnam":{"allowed":true},"Yemen":{"allowed":true},"Zambia":{"allowed":true},"Zimbabwe":{"allowed":true}} \ No newline at end of file
diff --git a/public/upsell/index.html b/public/upsell/index.html
new file mode 100644
index 00000000..1dbc21af
--- /dev/null
+++ b/public/upsell/index.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+
+<head>
+ <title>Upsell - SponsorBlock</title>
+ <meta charset="utf-8">
+
+ <link href="styles.css" rel="stylesheet" />
+
+ <script src="../js/vendor.js"></script>
+ <script src="../js/upsell.js"></script>
+</head>
+
+<body class="sponsorBlockPageBody">
+
+ <div id="title" class="titleBar">
+ <img src="../icons/LogoSponsorBlocker256px.png" height="80" class="profilepic" />
+ SponsorBlock
+ </div>
+
+ <br />
+
+ <div class="center">
+ <p>
+ __MSG_chaptersPage1__
+ </p>
+ </div>
+
+ <div class="center">
+ <iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/H_mP7bpbA_c?modestbranding=1&rel=0" title="Demo Video"
+ frameborder="0" allow="autoplay; clipboard-write; encrypted-media; picture-in-picture"
+ allowfullscreen>
+ </iframe>
+ </div>
+
+
+
+ <br />
+
+ <div class="center row-item">
+ <a href="https://buy.ajay.app/l/sponsorblock" class="option-link side-by-side" target="_blank" rel="noreferrer">
+ <div id="oneTimePurchase" class="option-button inline">
+ __MSG_oneTimePurchase__
+ </div>
+ </a>
+
+ <a href="https://www.patreon.com/ajayyy" class="option-link side-by-side" target="_blank" rel="noreferrer">
+ <div class="option-button side-by-side inline">
+ __MSG_joinOnPatreon__
+ </div>
+ </a>
+ </div>
+
+ <div class="center row-item">
+ <input id="redeemCodeInput" class="option-text-box" type="text" placeholder="__MSG_enterLicenseKey__">
+ <div id="redeemButton" class="option-button inline">
+ __MSG_redeem__
+ </div>
+ </div>
+
+ <div class="center row-item">
+ <a href="https://www.patreon.com/oauth2/authorize?response_type=code&client_id=-W7ib8J-LB3jowb1fqE07A7RDUovy45_pOoWcjby6yr5upo6At8Jlg2BPhWDXO2k&redirect_uri=https%3A%2F%2Fsponsor.ajay.app%3A3000%2Fapi%2FgenerateToken%2Fpatreon"
+ class="option-link" target="_blank" rel="noreferrer">
+ <div class="option-button inline">
+ __MSG_patreonSignIn__
+ </div>
+ </a>
+ </div>
+
+ <div id="cantAfford" class="center">
+
+ </div>
+
+ <div class="center">
+ __MSG_alreadyDonated__ [email protected]
+ </div>
+
+ <div id="subsidizedPrice" class="center hidden">
+ __MSG_selectYourCountry__
+ </div>
+
+ <div id="subsidizedLink" class="center hidden">
+ <a href="https://buy.ajay.app/l/sponsorblock/purchasing-power" class="option-link" target="_blank"
+ rel="noreferrer">
+ <div class="option-button inline">
+ __MSG_discountLink__
+ </div>
+ </a>
+ </div>
+
+ <div id="noSubsidizedLink" class="center hidden">
+ __MSG_noDiscount__
+ </div>
+
+</body> \ No newline at end of file
diff --git a/public/upsell/styles.css b/public/upsell/styles.css
new file mode 100644
index 00000000..0c0d6b6b
--- /dev/null
+++ b/public/upsell/styles.css
@@ -0,0 +1,387 @@
+/* Based on options page CSS */
+html {
+ color-scheme: dark;
+}
+
+body {
+ font-family: sans-serif;
+}
+
+.center {
+ text-align: center;
+}
+
+.center p {
+ margin: auto;
+}
+
+.inline {
+ display: inline-block;
+}
+
+.bold {
+ font-weight: bold;
+}
+
+.hidden {
+ display: none !important;
+}
+
+.row-item {
+ margin-top: 10px;
+ margin-bottom: 10px;
+}
+
+.keybind-status {
+ display: inline;
+}
+
+.small-description {
+ color: white;
+ font-size: 13px;
+}
+
+.medium-description {
+ color: white;
+ font-size: 15px;
+}
+
+.option-text-box {
+ width: 300px;
+}
+
+.option-button {
+ cursor: pointer;
+
+ background-color: #c00000;
+ padding: 10px;
+ color: white;
+ border-radius: 5px;
+ font-size: 14px;
+
+ width: max-content;
+}
+
+.option-link {
+ text-decoration: none;
+}
+
+.option-link.side-by-side {
+ padding: 50px;
+}
+
+.option-button:hover {
+ background-color: #fc0303;
+}
+
+.option-button.disabled {
+ cursor: default;
+
+ background-color: #520000;
+ color: grey;
+}
+
+#options {
+ max-width: 60%;
+ text-align: left;
+ display: inline-block;
+}
+
+.switch-container:after {
+ content: attr(label-name);
+ position: absolute;
+ padding: 4px;
+ width: max-content;
+
+ font-size: 14px;
+ color: white;
+}
+
+.text-label-container {
+ font-size: 14px;
+ color: white;
+}
+
+.switch {
+ position: relative;
+ display: inline-block;
+ width: 40px;
+ height: 24px;
+}
+
+.switch input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+}
+
+.slider {
+ position: absolute;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: #707070;
+}
+
+.animated * {
+ -webkit-transition: .4s;
+ transition: .4s;
+}
+
+.slider:before {
+ position: absolute;
+ content: "";
+ height: 16px;
+ width: 16px;
+ left: 4px;
+ bottom: 4px;
+ background-color: white;
+}
+
+.animated .slider:before {
+ -webkit-transition: .4s;
+ transition: .4s;
+}
+
+input:checked + .slider {
+ background-color: #fc0303;
+}
+
+input:checked + .slider:before {
+ -webkit-transform: translateX(16px);
+ -ms-transform: translateX(16px);
+ transform: translateX(16px);
+}
+
+/* Rounded sliders */
+.slider.round {
+ border-radius: 34px;
+}
+
+.slider.round:before {
+ border-radius: 50%;
+}
+
+
+/* Boilerplate CSS from https://ajay.app */
+
+body {
+ background-color: #333333;
+}
+
+.projectPreview {
+ position: relative;
+}
+
+.projectPreviewImage {
+ position: absolute;
+ left: -90px;
+ width: 80px;
+ top: 50%;
+ transform: translateY(-50%);
+}
+
+.projectPreviewImageLarge {
+ position: absolute;
+ left: -210px;
+ width: 200px;
+ top: 50%;
+ transform: translateY(-20%);
+}
+
+.projectPreviewImageLargeRight {
+ position: absolute;
+ right: -210px;
+ width: 200px;
+ top: 50%;
+ transform: translateY(-50%);
+}
+
+.createdBy {
+ font-size: 14px;
+ text-align: center;
+ padding-top: 0px;
+ padding-bottom: 0px;
+
+ display: inline-block;
+}
+
+#title {
+ background-color: #636363;
+
+ text-align: center;
+ vertical-align: middle;
+
+ font-size: 50px;
+ color: #212121;
+
+ padding: 20px;
+
+ text-decoration: none;
+
+ transition: font-size 1s;
+}
+
+.subtitle {
+ font-size: 40px;
+ color: #dad8d8;
+
+ padding-top: 10px;
+
+ transition: font-size 0.4s;
+}
+
+.subtitle:hover {
+ font-size: 45px;
+
+ transition: font-size 0.4s;
+}
+
+.profilepic {
+ background-color: #636363 !important;
+ vertical-align: middle;
+}
+
+.profilepiccircle {
+ vertical-align: middle;
+ overflow: hidden;
+ border-radius: 50%;
+}
+
+a {
+ text-decoration: underline;
+ color: inherit;
+}
+
+.link {
+ padding: 20px;
+
+ height: 80px;
+
+ transition: height 0.2s;
+}
+
+.link:hover {
+ height: 95px;
+
+ transition: height 0.2s;
+}
+
+#contact,.smalllink {
+ font-size: 25px;
+ color: #e8e8e8;
+
+ text-align: center;
+
+ padding: 10px;
+}
+
+#contact {
+ text-decoration: none;
+}
+
+p,li {
+ font-size: 20px;
+ color: #c4c4c4;
+
+ padding: 10px;
+}
+
+p,li,code,a {
+ max-width: 60%;
+ text-align: left;
+ overflow-wrap: break-word;
+}
+
+@media screen and (orientation:portrait) {
+ p,li,code,a {
+ max-width: 100%;
+ }
+
+ .projectPreviewImage {
+ position: unset;
+ width: 130px;
+ display: block;
+ margin: auto;
+ transform: none;
+ }
+}
+
+.previewImage {
+ max-height: 200px;
+}
+
+img {
+ max-width: 100%;
+
+ text-align: center;
+}
+
+#recentPostTitle {
+ font-size: 30px;
+ color: #dad8d8;
+}
+
+#recentPostDate {
+ font-size: 15px;
+ color: #dad8d8;
+}
+
+h1,h2,h3,h4,h5,h6 {
+ color: #dad8d8;
+}
+
+svg {
+ text-decoration: none;
+}
+
+.number-container:before {
+ content: attr(label-name);
+ padding-right: 4px;
+ width: max-content;
+
+ font-size: 14px;
+ color: white;
+}
+
+/* React styles */
+
+.categoryTableElement {
+ font-size: 16px;
+
+ color: white;
+}
+
+.categoryTableElement > * {
+ padding-right: 15px;
+ padding-bottom: 15px;
+}
+
+.optionsSelector {
+ background-color: #c00000;
+ color: white;
+
+ border: none;
+ font-size: 14px;
+ padding: 5px;
+ border-radius: 5px;
+}
+
+.categoryColorTextBox {
+ width: 60px;
+
+ background: none;
+ border: none;
+}
+
+#subsidizedPrice {
+ margin-top: 5px;
+ margin-bottom: 5px;
+}
+
+#discountButton {
+ text-decoration: underline;
+ cursor: pointer;
+} \ No newline at end of file
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
diff --git a/webpack/webpack.common.js b/webpack/webpack.common.js
index 78015cbf..73c17807 100644
--- a/webpack/webpack.common.js
+++ b/webpack/webpack.common.js
@@ -31,7 +31,8 @@ module.exports = env => ({
content: path.join(__dirname, srcDir + 'content.ts'),
options: path.join(__dirname, srcDir + 'options.ts'),
help: path.join(__dirname, srcDir + 'help.ts'),
- permissions: path.join(__dirname, srcDir + 'permissions.ts')
+ permissions: path.join(__dirname, srcDir + 'permissions.ts'),
+ upsell: path.join(__dirname, srcDir + 'upsell.ts')
},
output: {
path: path.join(__dirname, '../dist/js'),