Add affiliate system

This commit is contained in:
2025-03-21 01:11:03 +01:00
parent f8317a9f32
commit d894246a38
9 changed files with 254 additions and 13 deletions

View File

@@ -185,6 +185,38 @@ be public. For that reason I disabled search indexing on all pixeldrain files.
This protects the privacy of pixeldrain users and helps with preventing
information leaks.
## How does the affiliate program work?
Pixeldrain's affiliate program is a way to earn pixeldrain credit by driving
traffic to pixeldrain. The way it works is that you send people your affiliate
link, if someone accepts to be your affiliate then their active subscription
will earn you pixeldrain credit. The affiliate program is opt-in and fully
transparent. Users will always be notified when their affiliate account is
updated. You can update who you are sponsoring by editing the affiliate name on
your [user settings page](/user/settings).
Here is a summary of all the rules and limitations:
* Each paying customer using your affiliate code will earn you €0.50 in prepaid
credit every month. The credit is added to your account on a daily basis, as
you can see on the [transactions page](/user/prepaid/transactions).
* Sponsoring someone with an affiliate code does not cost you any extra money.
The resulting fee comes out of pixeldrain's pockets.
* You can only earn pixeldrain credit with the affiliate program. There is no
cash out feature.
* When someone who is using your affiliate code cancels their plan, you will
also stop receiving rewards.
* You don't need an active subscription to gain credit through the affiliate
program. You need a positive balance of at least €1 to activate the prepaid
plan.
Some fun facts:
* You only need two affiliates to offset pixeldrain's base subscription fee.
* Each sponsoring user is effectively equal to 125 GB of storage space or 500 GB
of bandwidth usage per month.
* You cannot sponsor yourself.
## Is there a clean pixeldrain logo I can use?
Yes, here's a high resolution pixeldrain logo with text. The font is called

View File

@@ -20,6 +20,7 @@ import ListStats from "./ListStats.svelte";
import ListUpdater from "./ListUpdater.svelte";
import CopyButton from "../layout/CopyButton.svelte";
import Menu from "../filesystem/Menu.svelte"
import AffiliatePrompt from "../user_home/AffiliatePrompt.svelte";
let loading = true
let embedded = false
@@ -617,6 +618,8 @@ const keyboard_event = evt => {
<!-- At the bottom so it renders over everything else -->
<LoadingIndicator loading={loading}/>
<AffiliatePrompt/>
</div>
<style>

View File

@@ -14,6 +14,7 @@ import { writable } from 'svelte/store';
import TransferLimit from '../file_viewer/TransferLimit.svelte';
import { stats } from "../lib/StatsSocket.js"
import { css_from_path } from './edit_window/Branding';
import AffiliatePrompt from '../user_home/AffiliatePrompt.svelte';
let file_viewer
let file_preview
@@ -198,6 +199,8 @@ const download = () => {
even when the user navigates to a different directory -->
<FSUploadWidget nav={nav} bind:this={upload_widget} />
<AffiliatePrompt/>
<LoadingIndicator loading={$loading}/>
</div>

View File

@@ -4,6 +4,7 @@ import { copy_text } from "../util/Util.svelte";
export let text = ""
export let style = ""
export let large_icon = false
export let small_icon = false
let failed = false
let success = false
@@ -31,6 +32,7 @@ export const copy = () => {
class:button_highlight={success}
class:button_red={failed}
class:large_icon
class:small_icon
title="Copy text to clipboard"
disabled={text === ""}
>
@@ -55,4 +57,8 @@ export const copy = () => {
font-size: 40px;
display: block;
}
.small_icon>.icon {
font-size: 1.2em;
display: block;
}
</style>

View File

@@ -1,6 +1,9 @@
<script>
import CopyButton from "../layout/CopyButton.svelte";
import Form from "./../util/Form.svelte";
let affiliate_link = window.location.protocol+"//"+window.location.host + "?ref=" + window.user.username
let account_settings = {
name: "account_settings",
fields: [
@@ -40,6 +43,7 @@ let account_settings = {
description: `Changing your username also changes the name used to
log in. If you forget your username you can still log in using
your e-mail address if you have one configured`,
separator: true,
},
],
submit_label: `<i class="icon">save</i> Save`,
@@ -57,10 +61,35 @@ let account_settings = {
form.append("password_new", fields.password_new1)
form.append("username", fields.username)
const resp = await fetch(
window.api_endpoint+"/user",
{ method: "PUT", body: form }
);
const resp = await fetch(window.api_endpoint+"/user", { method: "PUT", body: form });
if(resp.status >= 400) {
return {error_json: await resp.json()}
}
return {success: true, message: "Success! Your changes have been saved"}
},
}
const affiliate_settings = {
name: "affiliate_settings",
fields: [
{
name: "affiliate_user_name",
label: "Affiliate user name",
type: "text",
default_value: window.user.affiliate_user_name,
description: `The affiliate user name can be the name of a
pixeldrain account you wish to support with your subscription.
The account will receive a fee of €0.50 for every month that
your premium plan is active. This does not cost you anything
extra.`,
},
],
submit_label: `<i class="icon">save</i> Save`,
on_submit: async fields => {
const form = new FormData()
form.append("affiliate_user_name", fields.affiliate_user_name)
const resp = await fetch(window.api_endpoint+"/user", { method: "PUT", body: form });
if(resp.status >= 400) {
return {error_json: await resp.json()}
}
@@ -102,15 +131,33 @@ let delete_account = {
</script>
<section>
<br/>
<div class="highlight_border">
<h3>Account settings</h3>
<fieldset>
<legend>Account settings</legend>
<Form config={account_settings}></Form>
</div>
<br/>
<div class="highlight_border">
<h3>Delete account</h3>
</fieldset>
<fieldset>
<legend>Affiliate settings</legend>
<Form config={affiliate_settings}></Form>
<div class="form">
<p>
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>.
</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>
</div>
</fieldset>
<fieldset>
<legend>Delete account</legend>
<Form config={delete_account}></Form>
</div>
<br/>
</fieldset>
</section>

View File

@@ -0,0 +1,127 @@
<script>
import { onMount } from "svelte";
import Modal from "../util/Modal.svelte";
import LoadingIndicator from "../util/LoadingIndicator.svelte";
// 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
onMount(() => {
if (!always) {
if (window.user.subscription.id === "") {
// User does not have an active subscription, setting referral will
// not have effect
return
} else if (window.user.affiliate_user_name !== "") {
// User is already sponsoring someone
return
} else if (localStorage.getItem("affiliate_deny") === "1") {
// User has dismissed the pop-up in the past
return
}
}
ref = new URLSearchParams(document.location.search).get("ref")
if (ref === null) {
return
} else if (ref === window.user.affiliate_user_name) {
return // User is already supporting this affiliate ID
}
modal.show()
})
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
// Close the popup
modal.hide()
} catch (err) {
alert(err)
} finally {
loading = false
}
}
const deny = () => {
localStorage.setItem("affiliate_deny", "1")
modal.hide()
}
</script>
<Modal bind:this={modal} title="Affiliate sponsoring request" width="700px">
<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:
</p>
<ul>
<li>
Sponsoring only works while you have an active subscription
plan. When your subscription deactivates the creator will no
longer receive commissions.
</li>
<li>
Pixeldrain credit cannot be cashed out. So they are not earning
real money with this.
</li>
<li>
This does not cost you any extra money. The commissions paid out
to the creator are paid for by pixeldrain itself.
</li>
<li>
You can change who you are sponsoring at any time on your <a
href="/user/settings">account settings page</a>.
</li>
<li>
If you want to know more about the affiliate program check out
the <a href="/about#toc_12">Q&A page</a>.
</li>
</ul>
<p>
If you click 'Accept' then the requested affiliate code will be
added to your account and the creator will start earning. If you
choose 'Deny' then we will never show this pop-up again.
</p>
<div class="buttons">
<button class="button button_red" on:click={e => deny()}>
Deny
</button>
<button class="button button_highlight" on:click={e => allow()}>
Accept
</button>
</div>
</section>
</Modal>
<style>
.buttons {
display: flex;
flex-direction: row;
justify-content: space-around;
gap: 10px;
}
.button {
flex: 1 1 auto;
margin: 0;
font-size: 1.2em;
justify-content: center;
}
</style>

View File

@@ -11,6 +11,7 @@ import BandwidthSharing from "./BandwidthSharing.svelte";
import EmbeddingControls from "./EmbeddingControls.svelte";
import PageBranding from "./PageBranding.svelte";
import Dashboard from "./dashboard/Dashboard.svelte";
import AffiliatePrompt from "./AffiliatePrompt.svelte";
let pages = [
{
@@ -88,3 +89,5 @@ let pages = [
</script>
<TabMenu pages={pages} title="Welcome, {window.user.username}!"/>
<AffiliatePrompt always/>

View File

@@ -16,6 +16,7 @@ let transactions = {
total_storage_charge: 0,
total_bandwidth_used: 0,
total_bandwidth_charge: 0,
total_affiliate_amount: 0,
total_deposited: 0,
total_deducted: 0,
}
@@ -42,6 +43,7 @@ const load_transactions = async () => {
total_storage_charge: 0,
total_bandwidth_used: 0,
total_bandwidth_charge: 0,
total_affiliate_amount: 0,
total_deposited: 0,
total_deducted: 0,
}
@@ -54,6 +56,7 @@ const load_transactions = async () => {
month.total_storage_charge += row.storage_charge
month.total_bandwidth_used += row.bandwidth_used
month.total_bandwidth_charge += row.bandwidth_charge
month.total_affiliate_amount += row.affiliate_amount
month.total_deducted += row.subscription_charge + row.storage_charge + row.bandwidth_charge
})
@@ -139,6 +142,9 @@ onMount(() => {
<li>
Total charge: <Euro amount={transactions.total_deducted} precision="4"/>
</li>
<li>
Affiliate rewards: <Euro amount={transactions.total_affiliate_amount} precision="4"/>
</li>
<li>
Deposited: <Euro amount={transactions.total_deposited} precision="4"/>
</li>
@@ -153,6 +159,7 @@ onMount(() => {
<td>Subscription</td>
<td>Storage</td>
<td>Bandwidth</td>
<td>Affiliate</td>
<td>Deposit</td>
</tr>
</thead>
@@ -176,6 +183,10 @@ onMount(() => {
<Euro amount={row.bandwidth_charge} precision="6"/>
({formatDataVolume(row.bandwidth_used, 3)})
</td>
<td>
<Euro amount={row.affiliate_amount} precision="6"/>
({row.affiliate_count})
</td>
<td>
<Euro amount={row.deposit_amount}/>
</td>

View File

@@ -17,6 +17,7 @@ let transactions = {
total_storage_charge: 0,
total_bandwidth_used: 0,
total_bandwidth_charge: 0,
total_affiliate_amount: 0,
total_deposited: 0,
total_deducted: 0,
balance_start: 0,
@@ -45,6 +46,7 @@ const load_transactions = async () => {
total_storage_charge: 0,
total_bandwidth_used: 0,
total_bandwidth_charge: 0,
total_affiliate_amount: 0,
total_deposited: 0,
total_deducted: 0,
balance_start: 0,
@@ -66,6 +68,7 @@ const load_transactions = async () => {
month.total_storage_charge += row.storage_charge
month.total_bandwidth_used += row.bandwidth_used
month.total_bandwidth_charge += row.bandwidth_charge
month.total_affiliate_amount += row.affiliate_amount
month.total_deducted += row.subscription_charge + row.storage_charge + row.bandwidth_charge
})
@@ -148,6 +151,12 @@ onMount(() => {
(used {formatDataVolume(transactions.total_bandwidth_used, 3)})
</td>
</tr>
<tr>
<td>Affiliate rewards</td>
<td>
<Euro amount={transactions.total_affiliate_amount} precision="4"/>
</td>
</tr>
<tr>
<td>Deposited</td>
<td><Euro amount={transactions.total_deposited} precision="4"/></td>