Add affiliate prompt branding option

This commit is contained in:
2025-03-21 12:57:53 +01:00
parent d894246a38
commit 7aff2a2ead
7 changed files with 312 additions and 150 deletions

View File

@@ -70,6 +70,7 @@ let toolbar_toggle = () => {
let downloader
let list_updater
let details_window
let affiliate_prompt
let details_visible = false
let qr_window
let qr_visible = false
@@ -256,6 +257,9 @@ const apply_customizations = file => {
if (file.branding.footer_link) {
custom_footer_link = file.branding.footer_link
}
if (file.branding.affiliate_prompt) {
affiliate_prompt.prompt(file.branding.affiliate_prompt)
}
if (file.branding.disable_download_button && !file.can_edit) {
disable_download_button = true
}
@@ -619,7 +623,7 @@ const keyboard_event = evt => {
<!-- At the bottom so it renders over everything else -->
<LoadingIndicator loading={loading}/>
<AffiliatePrompt/>
<AffiliatePrompt bind:this={affiliate_prompt}/>
</div>
<style>

View File

@@ -3,10 +3,18 @@ export let on = false
export let group_first = false
export let group_middle = false
export let group_last = false
export let action = (e: MouseEvent) => {}
const click = (e: MouseEvent) => {
on = !on
if (typeof action === "function") {
action(e)
}
}
</script>
<button
on:click={() => on = !on}
on:click={click}
type="button"
class="button"
class:button_highlight={on}

View File

@@ -0,0 +1,107 @@
// Response types
// ==============
export type GenericResponse = {
value: string,
message: string,
}
export type User = {
username: string,
email: string,
subscription: Subscription,
storage_space_used: number,
filesystem_storage_used: number,
is_admin: boolean,
balance_micro_eur: number,
hotlinking_enabled: boolean,
monthly_transfer_cap: number,
monthly_transfer_used: number,
file_viewer_branding: Map<string, string>,
file_embed_domains: string,
skip_file_viewer: boolean,
affiliate_user_name: string,
}
export type Subscription = {
id: string,
name: string,
type: string,
file_size_limit: number,
file_expiry_days: number,
storage_space: number,
price_per_tb_storage: number,
price_per_tb_bandwidth: number,
monthly_transfer_cap: number,
file_viewer_branding: boolean,
filesystem_access: boolean,
filesystem_storage_limit: number,
}
// Utility funcs
// =============
export const get_endpoint = () => {
if ((window as any).api_endpoint !== undefined) {
return (window as any).api_endpoint as string
}
console.warn("api_endpoint property is not defined on window")
return "/api"
}
export const check_response = async (resp: Response) => {
let text = await resp.text()
if (resp.status >= 400) {
let error: any
try {
error = JSON.parse(text) as GenericResponse
} catch (err) {
error = text
}
throw error
}
return JSON.parse(text)
}
export const dict_to_form = (dict: Object) => {
let form = new FormData()
for (let key of Object.keys(dict)) {
if (dict[key] === undefined) {
continue
} else if (dict[key] instanceof Date) {
form.append(key, new Date(dict[key]).toISOString())
} else if (typeof dict[key] === "object") {
form.append(key, JSON.stringify(dict[key]))
} else {
form.append(key, dict[key])
}
}
return form
}
// API methods
// ===========
export const get_user = async () => {
if ((window as any).user !== undefined) {
return (window as any).user as User
}
console.warn("user property is not defined on window")
return await check_response(await fetch(get_endpoint() + "/user")) as User
}
export const put_user = async (data: Object) => {
check_response(await fetch(
get_endpoint() + "/user",
{ method: "PUT", body: dict_to_form(data) },
))
// Update the window.user variable
for (let key of Object.keys(data)) {
((window as any).user as User)[key] = data[key]
}
}

View File

@@ -1,8 +1,18 @@
<script>
import { onMount } from "svelte";
import CopyButton from "../layout/CopyButton.svelte";
import Form from "./../util/Form.svelte";
import Button from "../layout/Button.svelte";
let affiliate_link = window.location.protocol+"//"+window.location.host + "?ref=" + window.user.username
let affiliate_link = window.location.protocol+"//"+window.location.host + "?ref=" + encodeURIComponent(window.user.username)
let affiliate_deny = false
onMount(() => {
affiliate_deny = localStorage.getItem("affiliate_deny") === "1"
})
const enable_affiliate_prompt = () => {
affiliate_deny = false
localStorage.removeItem("affiliate_deny")
}
let account_settings = {
name: "account_settings",
@@ -144,15 +154,27 @@ let delete_account = {
Your own affiliate link is
<a href="{affiliate_link}">{affiliate_link}</a>
<CopyButton small_icon text={affiliate_link}/>. Share this link
with premium pixeldrain users to gain commissions. For a
detailed description of the affiliate program please check out
the <a href="/about#toc_12">Q&A page</a>.
with premium pixeldrain users to gain commissions. You can use
the "?ref={encodeURIComponent(window.user.username)}" referral
code on download pages too. For a detailed description of the
affiliate program please check out the <a
href="/about#toc_12">Q&A page</a>.
</p>
<p>
Note that the link includes the name of your pixeldrain
account. If you change your account name the link will stop
working and you might stop receiving commissions.
</p>
{#if affiliate_deny}
<div class="highlight_blue">
You currently have affiliate prompts disabled. You will not
see affiliate requests from other users. If you wish to
enable it again, click here:
<br/>
<Button click={enable_affiliate_prompt} label="Enable affiliate prompts"/>
</div>
{/if}
</div>
</fieldset>

View File

@@ -1,22 +1,35 @@
<script>
<script lang="ts">
import { onMount } from "svelte";
import Modal from "../util/Modal.svelte";
import LoadingIndicator from "../util/LoadingIndicator.svelte";
import { get_user, put_user } from "../lib/PixeldrainAPI.mjs";
// When the always flag is set then the pop-up will also show if the user
// already has an affiliate ID set
export let always = false
let modal
let loading
let ref
let modal: Modal
let loading: boolean
let referral: string
let shown = false
export const prompt = async (ref: string) => {
referral = ref
const user = await get_user()
if (referral === null) {
return
} else if (referral === user.affiliate_user_name) {
return // User is already supporting this affiliate ID
} else if (referral === user.username) {
return // This is your own referral link
}
onMount(() => {
if (!always) {
if (window.user.subscription.id === "") {
if (user.subscription.id === "") {
// User does not have an active subscription, setting referral will
// not have effect
return
} else if (window.user.affiliate_user_name !== "") {
} else if (user.affiliate_user_name !== "") {
// User is already sponsoring someone
return
} else if (localStorage.getItem("affiliate_deny") === "1") {
@@ -25,28 +38,21 @@ onMount(() => {
}
}
ref = new URLSearchParams(document.location.search).get("ref")
if (ref === null) {
// The prompt can only be shown once per page. This should prevent it from
// showing up every time someone loads a new file.
if (shown === true) {
return
} else if (ref === window.user.affiliate_user_name) {
return // User is already supporting this affiliate ID
}
shown = true
modal.show()
})
}
onMount(() => prompt(new URLSearchParams(document.location.search).get("ref")))
const allow = async () => {
loading = true
try {
const form = new FormData()
form.append("affiliate_user_name", ref)
const resp = await fetch(window.api_endpoint+"/user", { method: "PUT", body: form });
if(resp.status >= 400) {
throw (await resp.json()).message
}
// Update the window.user variable
window.user.affiliate_user_name = ref
await put_user({affiliate_user_name: referral})
// Close the popup
modal.hide()
@@ -67,10 +73,10 @@ const deny = () => {
<LoadingIndicator bind:loading={loading} />
<section>
<p>
Hi! {ref} wants you to sponsor their pixeldrain account. This will
give them €0.50 every month in pixeldrain prepaid credit. They can
use this credit to get a discount on their file sharing and storage
efforts. Here is a short summary of what this entails:
Hi! {referral} wants you to sponsor their pixeldrain account. This
will give them €0.50 every month in pixeldrain prepaid credit. They
can use this credit to get a discount on their file storage and
sharing costs. Here is a short summary of what this entails:
</p>
<ul>
<li>

View File

@@ -6,6 +6,7 @@ import SuccessMessage from "../util/SuccessMessage.svelte";
import ThemePicker from "../util/ThemePicker.svelte";
import { onMount } from "svelte";
import Persistence from "../icons/Persistence.svelte";
import ToggleButton from "../layout/ToggleButton.svelte";
let loading = false
let success_message
@@ -19,6 +20,7 @@ let header_link = ""
let background_image = ""
let footer_image = ""
let footer_link = ""
let affiliate_prompt = false
let disable_download_button = false
let disable_share_button = false
@@ -59,6 +61,9 @@ let save = async () => {
form.append("footer_link", footer_link)
form.append("disable_download_button", disable_download_button)
form.append("disable_share_button", disable_share_button)
if (affiliate_prompt) {
form.append("affiliate_prompt", window.user.username)
}
try {
const resp = await fetch(
@@ -90,6 +95,7 @@ onMount(() => {
background_image = b.background_image ? b.background_image : ""
footer_image = b.footer_image ? b.footer_image : ""
footer_link = b.footer_link ? b.footer_link : ""
affiliate_prompt = b.affiliate_prompt === window.user.username ? true : false
disable_download_button = b.disable_download_button ? b.disable_download_button : false
disable_share_button = b.disable_share_button ? b.disable_share_button : false
}
@@ -105,12 +111,6 @@ onMount(() => {
Sharing settings are not available for your account. Subscribe to
the Persistence plan or higher to enable these features.
</div>
{:else if !window.user.hotlinking_enabled}
<div class="highlight_yellow">
To use custom file viewer branding hotlinking needs to be
enabled. Enable hotlinking on the
<a href="/user/sharing/bandwidth">sharing settings page</a>.
</div>
{/if}
<SuccessMessage bind:this={success_message}></SuccessMessage>
@@ -125,7 +125,8 @@ onMount(() => {
should use APNG or WebP. Avoid using animated GIFs as they are very slow
to load.
</p>
<h3>Theme</h3>
<fieldset>
<legend>Theme</legend>
<p>
Choose a theme for your download pages. This theme will override the
theme preference of the person viewing the file. Set to 'None' to let
@@ -135,8 +136,10 @@ onMount(() => {
theme={theme}
on:theme_change={e => {theme = e.detail; save()}}>
</ThemePicker>
</fieldset>
<h3>Header image</h3>
<fieldset>
<legend>Header image</legend>
<p>
Will be shown above the file. Maximum height is 90px. Will be shrunk if
larger. You can also add a link to open when the visitor clicks the
@@ -162,8 +165,10 @@ onMount(() => {
<CustomBanner src={"/api/file/"+header_image} link={header_link}></CustomBanner>
</div>
{/if}
</fieldset>
<h3>Background image</h3>
<fieldset>
<legend>Background image</legend>
<p>
This image will be shown behind the file which is being viewed. I
recommend choosing something dark and not too distracting. Try to keep
@@ -183,8 +188,10 @@ onMount(() => {
<img class="background_preview" src="/api/file/{background_image}" alt="Custom file viewer background"/>
</div>
{/if}
</fieldset>
<h3>Footer image</h3>
<fieldset>
<legend>Footer image</legend>
<p>
Will be shown below the file. Maximum height is 90px. Will be shrunk if
larger.
@@ -208,8 +215,22 @@ onMount(() => {
<CustomBanner src={"/api/file/"+footer_image} link={footer_link}></CustomBanner>
</div>
{/if}
</fieldset>
<h3>Toolbar buttons</h3>
<fieldset>
<legend>Affiliate prompt</legend>
<p>
When this is enabled premium users on your download pages will be
asked to support you through pixeldrain's <a
href="/about#toc_12">affiliate program</a>.
</p>
<ToggleButton bind:on={affiliate_prompt} action={save}>
Enable affiliate prompt
</ToggleButton>
</fieldset>
<fieldset>
<legend>Toolbar buttons</legend>
<p>
If you don't want to make it obvious that your files can be downloaded
or shared while still allowing people to view them through the site you
@@ -224,23 +245,15 @@ onMount(() => {
files. The buttons are still available to you. If you want to see the
effects you can open your file in an incognito window.
</p>
Disable download button:
<button on:click={() => {disable_download_button = !disable_download_button; save()}}>
{#if disable_download_button}
<i class="icon">check</i> ON (click to turn off)
{:else}
<i class="icon">close</i> OFF (click to turn on)
{/if}
</button>
<ToggleButton bind:on={disable_download_button} action={save}>
Disable download button
</ToggleButton>
<br/>
Disable share button:
<button on:click={() => {disable_share_button = !disable_share_button; save()}}>
{#if disable_share_button}
<i class="icon">check</i> ON (click to turn off)
{:else}
<i class="icon">close</i> OFF (click to turn on)
{/if}
</button>
<ToggleButton bind:on={disable_share_button} action={save}>
Disable share button
</ToggleButton>
</fieldset>
</section>
<FilePicker

View File

@@ -141,6 +141,7 @@ onMount(() => {
</ul>
</div>
</div>
<div>
<div class="feat_label" class:feat_highlight={subscription === "prepaid"}>
Prepaid<br/>
@@ -188,6 +189,7 @@ onMount(() => {
</ul>
</div>
</div>
<div>
<div class="feat_label" class:feat_highlight={subscription === ""}>
Free<br/>