Files
anonymous_github/public/partials/profile.htm
T
Thomas DurieuxandClaude Opus 5.5 d25e5336ce feat: let authors pay once per paper instead of subscribing
Most authors use the service for one paper a few times a year, so a
subscription does not fit. Add cheap one-time options (Paper pack, Extra
repository, Live updates, Extended expiration, Never expire), cap free
dated expiration at 5 months, keep cheaper subscriptions for heavy users,
and make every new conference a paid review package in three sizes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 16:04:31 +00:00

184 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<div class="container paper-page user-settings">
<header class="settings-title"><h1>Settings</h1><div class="settings-header-actions"><span>@{{ user?.username }}</span></div></header>
<nav class="settings-nav" aria-label="Settings">
<button type="button" v-for="tab in [{id:'defaults',label:'Anonymization defaults'},{id:'billing',label:'Hosting & billing'},{id:'account',label:'Account'}]" :aria-current="settingsTab === tab.id ? 'page' : null" @click="settingsTab = tab.id">{{ tab.label }}</button>
</nav>
<div v-show="settingsTab === 'defaults'">
<p class="settings-scope">For new anonymizations only. Existing repositories keep their settings.</p>
<form class="form needs-validation" name="defaultsForm" novalidate v-form="viewState" @submit.prevent="submitForm($event, () => { saveDefault($event) })">
<section class="settings-row"><div><label class="settings-label" for="terms">Terms to redact</label><p class="settings-help">One term per line.</p></div><div><textarea class="form-control" id="terms" name="terms" :rows="Math.min(14, Math.max(3, String(terms || &quot;&quot;).split(&quot;\n&quot;).length + 1))" placeholder="Jane Smith
MIT CSAIL
jane.smith@example.org" v-field="{ state: viewState, set: value =&gt; { terms = value }, value: terms, form: &quot;defaultsForm&quot;, options: { debounce: 250 } }"></textarea>
<details class="settings-syntax"><summary>Regex &amp; replacement syntax</summary><p>Matches become <code>{{ site_options?.ANONYMIZATION_MASK || 'XXX' }}-[N]</code>. Use <code>term=&gt;replacement</code> for a custom replacement. Regex is supported.</p></details>
<div class="invalid-feedback" v-show="defaultsForm?.terms?.errors?.format">Terms are in an invalid format.</div>
</div></section>
<section class="settings-row"><h2>Reviewer display</h2><div><div class="settings-checks"><label class="settings-check"><input class="form-check-input" type="checkbox" id="link" name="link" v-field="{ state: viewState, set: value =&gt; { options.link = value }, value: options?.link, form: &quot;defaultsForm&quot;, options: {} }">Keep links</label><label class="settings-check"><input class="form-check-input" type="checkbox" id="notebook" name="notebook" v-field="{ state: viewState, set: value =&gt; { options.notebook = value }, value: options?.notebook, form: &quot;defaultsForm&quot;, options: {} }">Render notebooks</label><label class="settings-check"><input class="form-check-input" type="checkbox" id="image" name="image" v-field="{ state: viewState, set: value =&gt; { options.image = value }, value: options?.image, form: &quot;defaultsForm&quot;, options: {} }">Show images</label><label class="settings-check"><input class="form-check-input" type="checkbox" id="pdf" name="pdf" v-field="{ state: viewState, set: value =&gt; { options.pdf = value }, value: options?.pdf, form: &quot;defaultsForm&quot;, options: {} }">Show PDFs</label><label class="settings-check"><input class="form-check-input" type="checkbox" id="loc" name="loc" v-field="{ state: viewState, set: value =&gt; { options.loc = value }, value: options?.loc, form: &quot;defaultsForm&quot;, options: {} }">Show line counts</label><label class="settings-check"><input class="form-check-input" type="checkbox" id="page" name="page" v-field="{ state: viewState, set: value =&gt; { options.page = value }, value: options?.page, form: &quot;defaultsForm&quot;, options: {} }">GitHub Pages</label></div><p class="settings-help">Images and PDFs are not anonymized.</p><p class="settings-help">GitHub Pages must use the anonymized branch.</p></div></section>
<section class="settings-row"><h2>Source updates</h2><div><label class="settings-check"><input class="form-check-input" type="checkbox" id="update" name="update" v-field="{ state: viewState, set: value =&gt; { options.update = value }, value: options?.update, form: &quot;defaultsForm&quot;, options: {} }">Auto-update from GitHub</label><p class="settings-help">Latest commit, at most once an hour.</p></div></section>
<section class="settings-row"><label class="settings-label" for="expiration">On expiration</label><div><select class="form-control" id="expiration" name="expiration" v-field="{ state: viewState, set: value =&gt; { options.expirationMode = value }, value: options?.expirationMode, form: &quot;defaultsForm&quot;, options: {} }">
<option value="redirect">Redirect visitors to the GitHub repository</option>
<option value="remove" selected>Remove the anonymized repository</option>
</select><p class="settings-help">{{ options.expirationMode === 'redirect' ? 'Redirecting reveals the original repository. Choose the date per anonymization.' : 'Set the expiration date for each repository.' }}</p></div></section>
<footer class="settings-save">
<div class="settings-save-status draft-status" role="status"><span v-if="draftRestored">Your unsaved draft was restored.</span><span v-else-if="unsavedChanges">Unsaved changes{{ draftSaved ? ' · Draft saved in this tab' : '' }}</span><span v-else>{{ message ? 'Defaults saved' : 'All changes saved' }}</span></div>
<button type="button" class="draft-discard" v-if="draftRestored || unsavedChanges" @click="discardDraft()">Discard changes</button>
<button id="save" type="submit" class="btn btn-ink" :disabled="saving">{{ saving ? 'Saving…' : 'Save defaults' }}</button>
<div class="settings-save-error" role="alert" v-if="error">{{ error }}</div>
</footer>
</form>
</div>
<section id="settings-billing" v-show="settingsTab === 'billing'" :aria-busy="billingLoading || billingBusy">
<p class="settings-scope">Your plan and usage. Reviewer access is always free.</p>
<p v-if="billingLoading" role="status">Loading billing…</p>
<div v-if="billingError" class="settings-notice" role="alert">{{ billingError }} <button type="button" class="btn btn-outline-ink" @click="loadBilling()" :disabled="billingBusy">Try again</button></div>
<p v-if="billingNotice" class="settings-notice" role="status">{{ billingNotice }}</p>
<div v-if="billingPending" class="settings-notice" role="status"><strong>Confirming your payment</strong><p>Your allowance will update after confirmation. Please do not pay again.</p><button type="button" class="btn btn-outline-ink" @click="refreshBilling()" :disabled="billingBusy">Check payment status</button></div>
<section class="billing-summary">
<div class="billing-summary-head">
<div>
<p class="billing-eyebrow">Current plan</p>
<h2 class="billing-plan-name">{{ profileAuthorGrant() ? profileGrantName(profileAuthorGrant()) : (billing?.enabled ? 'Free' : 'Account allowance') }}</h2>
<p class="billing-plan-meta" v-if="profileAuthorGrant()">{{ profileAuthorGrant().cancelAtPeriodEnd ? 'Renewal cancelled · access until ' : 'Renews ' }}{{ billingDate(profileAuthorGrant().until) }}<template v-if="profileAuthorPurchase()?.amount != null"> · {{ billingPrice(profileAuthorPurchase()) }}</template></p>
<p class="billing-plan-meta" v-else-if="billing?.allowance">{{ billing.allowance.repositories }} repositories · {{ billingBytes(billing.allowance.storage) }} · links up to {{ billing.freeExpirationMonths || 5 }} months</p>
</div>
<div class="billing-summary-actions" v-if="profileAuthorGrant() &amp;&amp; billing?.portal">
<button type="button" class="btn btn-outline-ink" @click="billingPortal()" :disabled="billingBusy">{{ profileAuthorGrant().cancelAtPeriodEnd ? 'Resume renewal' : 'Change or cancel plan' }}</button>
</div>
</div>
<div id="settings-quota"><quota-usage :quota="quota" :fmt="fmt" v-if="quota"></quota-usage><p v-else role="status">Loading usage…</p></div>
<p class="settings-notice" v-if="quota?.repository &amp;&amp; !quota.repository.unlimited &amp;&amp; quota.repository.used >= quota.repository.total">Your free repositories are used up. Add the next one for {{ profileSlotPlan() ? billingPrice(profileSlotPlan()) : 'a one-time payment' }} when you create it, or <a href="/dashboard">remove work you no longer need</a>.</p>
<p class="settings-notice" v-if="profileAuthorGrant()?.status === 'past_due'">A subscription payment is overdue. Update your payment details in Manage billing and invoices.</p>
<p class="settings-notice" v-if="profilePastSubscription()">Your previous subscription has ended. Never-expiring repositories stay online for a 90-day grace period. <a href="/dashboard">Review expiration dates</a>.</p>
<p class="billing-footnote">Coauthored, paid and conference repositories aren't counted here.</p>
</section>
<section class="billing-section" v-if="billing?.enabled &amp;&amp; !profileAuthorGrant() &amp;&amp; (profilePackPlan() || profileAuthorPlans().length)" aria-labelledby="settings-plan-comparison">
<h2 class="billing-section-title" id="settings-plan-comparison" tabindex="-1">Get more</h2>
<div class="billing-offers" v-if="!(showRepositoryPass &amp;&amp; profileAddon() &amp;&amp; !billingPending)">
<article class="billing-offer is-recommended" v-if="profilePackPlan()">
<span class="billing-offer-pill">Recommended</span>
<div class="billing-offer-head"><p class="billing-eyebrow">For one paper</p></div>
<h3>Paper pack</h3>
<p class="billing-offer-price"><strong>{{ billingAmount(profilePackPlan()) }}</strong> once</p>
<ul class="billing-offer-list"><li>One more repository, outside your free ones</li><li>Its own {{ billingBytes(profileSlotPlan()?.storage || profilePassPlan()?.storage) }} of storage</li><li>Automatic updates for 180 days</li><li>Online for up to a year</li><li>No subscription</li></ul>
<button type="button" class="btn btn-ink" @click="openProfileAddon('repo_pack')" :disabled="billingPending || billing.checkoutEnabled === false">Buy a Paper pack</button>
</article>
<article class="billing-offer" v-if="profileAuthorPlans().length">
<div class="billing-offer-head"><p class="billing-eyebrow">For many papers</p><div class="billing-toggle" role="radiogroup" aria-label="Billing frequency"><button type="button" role="radio" v-for="plan in profileAuthorPlansByValue()" :key="plan.id" :aria-checked="profileSelectedPlan()?.id === plan.id ? 'true' : 'false'" :class="{ 'is-on': profileSelectedPlan()?.id === plan.id }" @click="profilePlan = plan.id">{{ plan.interval === 'year' ? 'Yearly' : 'Monthly' }}<span v-if="plan.interval === 'year' &amp;&amp; profileAnnualSavings()" class="billing-toggle-save">−{{ profileAnnualSavings() }}%</span></button></div></div>
<h3>Author subscription</h3>
<p class="billing-offer-price"><strong>{{ billingAmount(profileSelectedPlan()) }}</strong> per {{ profileSelectedPlan()?.interval === 'year' ? 'year' : 'month' }}</p>
<ul class="billing-offer-list"><li>{{ profileSelectedPlan()?.repositories }} repositories, {{ billingBytes(profileSelectedPlan()?.storage) }}</li><li>Automatic updates</li><li>Links that never expire</li><li>Coauthors included</li><li>Cancel renewal anytime</li></ul>
<button type="button" class="btn btn-outline-ink" :disabled="billingBusy || billing.checkoutEnabled === false" @click="checkout(profileSelectedPlan().id)">{{ billingBusy ? 'Please wait…' : 'Subscribe' }}</button>
</article>
</div>
<div class="billing-addons" v-if="profileSingleAddons().length &amp;&amp; !(showRepositoryPass &amp;&amp; profileAddon() &amp;&amp; !billingPending)">
<div class="billing-addons-head"><h3 class="billing-subtitle">Or pay for one thing</h3><span>One-time</span></div>
<ul class="billing-addon-rows"><li v-for="plan in profileSingleAddons()" :key="plan.id"><div><strong>{{ plan.name }}</strong><span>{{ profileAddonBlurb(plan.id) }}</span></div><span class="billing-addon-price">{{ billingAmount(plan) }}</span><button type="button" class="settings-text-button" @click="openProfileAddon(plan.id)" :disabled="billingPending || billing.checkoutEnabled === false">Buy</button></li></ul>
</div>
<div class="billing-buy" v-if="showRepositoryPass &amp;&amp; profileAddon() &amp;&amp; !billingPending" id="settings-addons-title" tabindex="-1">
<div class="billing-buy-info">
<button type="button" class="btn btn-outline-ink billing-back" @click="showRepositoryPass = false">← All options</button>
<h3>{{ profileAddon().name }}</h3>
<p class="billing-offer-price"><strong>{{ billingAmount(profileAddon()) }}</strong> once</p>
<ul class="billing-offer-list"><li v-for="item in profileAddonFeatures(profileAddon().id)" :key="item">{{ item }}</li></ul>
</div>
<div class="billing-buy-form">
<template v-if="profileRepositories().length">
<label class="paper-field-label" for="billing-repository">Which repository?</label>
<select id="billing-repository" class="form-control" v-model="billingRepository"><option value="" disabled>Choose a repository</option><option v-for="repo in profileRepositories()" :key="repo.repoId" :value="repo.repoId">{{ repo.repoId }} · {{ profileRepositoryExpiry(repo) }}</option></select>
<button type="button" class="btn btn-ink" :disabled="billingBusy || !billingRepository || billing.checkoutEnabled === false" @click="checkout(profileAddon().id, billingRepository)">{{ billingBusy ? 'Please wait…' : 'Continue to checkout' }}</button>
<p class="billing-footnote">One-time payment. Tax shown at checkout.</p>
</template>
<p class="billing-buy-desc" v-else>You have no repositories yet. <a href="/anonymize">Create one</a> first.</p>
<p class="billing-footnote" v-if="['repo_slot', 'repo_pack'].includes(profileAddon().id)">Adding a new repository? <a href="/anonymize">Start creating it</a>; the form offers this once your free repositories are in use.</p>
</div>
</div>
<details class="billing-compare"><summary>Compare all features</summary><div class="billing-compare-card">
<table class="settings-comparison"><caption class="sr-only">Free, Paper pack and Author hosting</caption><thead><tr><th scope="col"><span class="sr-only">Feature</span></th><th scope="col">Free<small>€0</small></th><th scope="col" class="is-recommended" v-if="profilePackPlan()">Paper pack<small>{{ billingPrice(profilePackPlan()) }}</small></th><th scope="col" v-if="profileAuthorPlans().length">Author<small>{{ billingPrice(profileSelectedPlan()) }}</small></th></tr></thead><tbody>
<tr><th scope="row">Repositories</th><td>{{ billing.allowance?.repositories }}</td><td class="is-recommended" v-if="profilePackPlan()">+1</td><td v-if="profileAuthorPlans().length">{{ profileSelectedPlan()?.repositories }}</td></tr>
<tr><th scope="row">Source storage</th><td>{{ billingBytes(billing.allowance?.storage) }} total</td><td class="is-recommended" v-if="profilePackPlan()">{{ billingBytes(profileSlotPlan()?.storage || profilePassPlan()?.storage) }} for it</td><td v-if="profileAuthorPlans().length">{{ billingBytes(profileSelectedPlan()?.storage) }} total</td></tr>
<tr><th scope="row">Latest expiration</th><td>{{ billing.freeExpirationMonths || 5 }} months ahead</td><td class="is-recommended" v-if="profilePackPlan()">1 year after purchase</td><td v-if="profileAuthorPlans().length">Never</td></tr>
<tr><th scope="row">Automatic updates</th><td>No</td><td class="is-recommended" v-if="profilePackPlan()">180 days</td><td v-if="profileAuthorPlans().length">Yes</td></tr>
<tr><th scope="row">Renews</th><td>No</td><td class="is-recommended" v-if="profilePackPlan()">No</td><td v-if="profileAuthorPlans().length">Until cancelled</td></tr>
<tr><th scope="row">Reviewer access</th><td>Free</td><td class="is-recommended" v-if="profilePackPlan()">Free</td><td v-if="profileAuthorPlans().length">Free</td></tr>
</tbody></table></div>
</details>
<p class="billing-footnote" v-if="billing.checkoutEnabled === false">New purchases are not available right now. Existing billing can still be managed below.</p>
</section>
<section class="billing-section" v-if="billing?.enabled">
<div class="billing-section-head"><h2 class="billing-section-title">Purchases</h2><button type="button" class="settings-text-button" v-if="!billingPending" @click="refreshBilling()" :disabled="billingBusy">Refresh payment status</button></div>
<p class="billing-empty" v-if="!(billing.purchases || []).length">No purchases yet.</p>
<div class="billing-history-head" v-if="(billing.purchases || []).length" aria-hidden="true"><span>Purchase</span><span>Period</span><span>Amount</span><span>Status</span></div>
<ul class="billing-history" v-if="(billing.purchases || []).length">
<li class="billing-history-row" v-for="purchase in profilePurchases(true).concat(profilePurchases(false))" :key="purchase.id" :class="{ 'is-ended': !['active', 'overdue'].includes(purchase.status) }">
<div class="billing-history-main">
<strong>{{ profileGrantName(purchase) }}<template v-if="purchase.repoId"> · <a :href="'/anonymize/' + encodeURIComponent(purchase.repoId)">{{ purchase.repoId }}</a></template><template v-else-if="purchase.conference"> · <a :href="'/conference/' + encodeURIComponent(purchase.conference)">{{ purchase.conference }}</a></template></strong>
<span>{{ [purchase.purchasedAt ? 'Bought ' + billingDate(purchase.purchasedAt) : '', purchase.parts.length ? profilePackSummary(purchase) : ['refunded', 'disputed'].includes(purchase.status) ? 'Payment ' + purchase.status + ', so what it paid for has ended' : purchase.recurring ? 'Account subscription' : ''].filter(Boolean).join(' · ') }}</span>
</div>
<div class="billing-history-term">{{ profilePurchaseTerm(purchase) }}</div>
<div class="billing-history-paid">{{ purchase.amount != null ? billingPrice(purchase) : '' }}</div>
<div class="billing-history-status"><span class="settings-status" :class="'is-' + purchase.status">{{ profilePurchaseStatus(purchase) }}</span></div>
</li>
</ul>
<div class="billing-manage">
<div><strong>Invoices and payment details</strong><p v-if="billing.portal">Download receipts, update your card or change renewal in Stripe.</p><p v-else>Invoices appear here after your first purchase.</p>
<p>Need a refund? <a href="mailto:tdurieux@anonymous.4open.science">Email the maintainer</a> with the payment date, or read the <a href="/faq#faq-plans">refund policy</a>.</p></div>
<button type="button" class="btn btn-outline-ink" v-if="billing.portal" @click="billingPortal()" :disabled="billingBusy">Manage billing and invoices</button>
</div>
</section>
<p class="billing-conference">Organizing a conference? <template v-if="profileConferencePlan()">Review packages start at {{ billingAmount(profileConferencePlan()) }} once for up to {{ profileConferencePlan().repositories }} submissions, and authors pay nothing.</template><template v-else>One payment hosts every submission, and authors pay nothing.</template> <a href="/conference/new">Create a conference</a></p>
</section>
<section id="settings-account" class="settings-account-panel" v-show="settingsTab === 'account'">
<p class="settings-scope">Your identity, repository access, and account.</p>
<div class="paper-account-card">
<img width="40" height="40" class="rounded-circle" alt="" v-if="user?.photo" :src="safeUrl((user?.photo))">
<div class="paper-account-identity">
<div class="paper-account-name">@{{ user?.username }}</div>
<div class="paper-account-meta">Signed in with GitHub. Anonymous GitHub only reads your repositories.</div>
</div>
<a class="btn btn-outline-ink" href="/api/user/logout" target="_self">Sign out</a>
</div>
<form class="paper-account-email" @submit.prevent="saveAccountEmail()">
<label class="paper-field-label" for="account-email">Email address</label>
<p id="account-email-help" class="paper-section-copy">Used for account alerts. Your address is never shown on anonymous pages.</p>
<input id="account-email" class="form-control" type="email" autocomplete="email" maxlength="254" required v-model="accountEmail" aria-describedby="account-email-help" :disabled="emailLoading || emailSaving || !emailCSRFReady" @input="emailSaved = false" />
<p role="status" v-if="emailLoading">Loading your email address…</p>
<p class="paper-inline-alert paper-inline-alert-error" role="alert" v-if="emailError">{{ emailError }}</p>
<button class="btn btn-outline-ink mt-2" type="button" v-if="!emailCSRFReady &amp;&amp; !emailLoading" @click="loadAccountEmail()">Try again</button>
<button class="btn btn-ink mt-2" type="submit" :disabled="emailLoading || emailSaving || !emailCSRFReady">{{ emailSaving ? 'Saving…' : 'Save email address' }}</button>
<p role="status" v-if="emailSaved">Email address saved.</p>
</form>
<a class="paper-settings-connection-link" href="/connections">
<i class="fab fa-github" aria-hidden="true"></i>
<span>GitHub connections<small>Manage repository access and legacy OAuth.</small></span>
<span aria-hidden="true">&rarr;</span>
</a>
<div class="paper-danger-zone">
<div class="paper-danger-head">Delete account</div>
<p class="paper-danger-copy">
Removes all your anonymized repositories, gists, and pull requests,
revokes the application's access to your GitHub account, and erases
your personal data (username, email, access tokens). This cannot be undone.
</p>
<div class="paper-inline-alert paper-inline-alert-error" role="alert" v-if="deleteError"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-info"></use></svg> <span v-text="deleteError"></span></div>
<button type="button" class="btn btn-outline-danger" @click="confirmDeleteAccount = true" :disabled="deletingAccount">
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-trash"></use></svg>{{ deletingAccount ? 'Deleting…' : 'Delete my account' }}
</button>
<div class="settings-delete-confirm" v-if="confirmDeleteAccount">
<label for="delete-username">Type {{ user?.username }} to confirm</label>
<input id="delete-username" class="form-control" v-model="deleteUsername" autocomplete="off">
<div class="settings-actions"><button type="button" class="btn btn-outline-danger" @click="deleteAccount()" :disabled="deletingAccount || deleteUsername !== user?.username">{{ deletingAccount ? 'Deleting…' : 'Delete permanently' }}</button><button type="button" class="btn btn-outline-ink" @click="confirmDeleteAccount = false; deleteUsername = ''" :disabled="deletingAccount">Cancel</button></div>
</div>
</div>
</section>
</div>