Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions tools/kofi-worker/email.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,13 @@ export function codeLines(code) {
return lines.map((line, index) => (index < lines.length - 1 ? `${line}-` : line))
}

/** Folio's own link for a code (RedeemActivity, since 0.6.5): on an Android phone with Folio it adds the code in one tap. */
/**
* The link in the email: a page on the site that shows the code and, on Android, opens Folio's own redeem link
* (folio://redeem, handled by RedeemActivity since 0.6.5). The email cannot link to folio:// itself, because Gmail does
* nothing with a custom scheme. The code sits after the #, so a browser never sends it to a server.
*/
export function redeemLink(code) {
return `folio://redeem?c=${encodeURIComponent(String(code))}`
return `https://foliolauncher.com/redeem/#${encodeURIComponent(String(code))}`
}

const SUBJECT = 'Your Folio supporter code'
Expand All @@ -44,7 +48,7 @@ export function supporterEmail({ name = '', code, pool = '' }) {
'',
` ${code}`,
'',
'On your Android phone, this link adds it in one tap:',
'On your Android phone, this link opens a page that adds it to Folio:',
` ${redeemLink(code)}`,
'',
'In Folio: Settings › Supporter › Redeem a Code, and paste it in. It is checked on your phone, so it works',
Expand Down Expand Up @@ -109,10 +113,10 @@ export function supporterEmail({ name = '', code, pool = '' }) {
<tr><td class="pad" style="padding:20px 32px 0">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%"><tr>
<td class="btn" align="center" bgcolor="${TEAL}" style="background:${TEAL};border-radius:12px">
<a href="${link}" class="btntext" style="display:block;padding:15px 22px;font:600 16px/1.2 ${font};color:#ffffff;text-decoration:none">Open in Folio</a>
<a href="${link}" class="btntext" style="display:block;padding:15px 22px;font:600 16px/1.2 ${font};color:#ffffff;text-decoration:none">Add to Folio</a>
</td>
</tr></table>
<p class="dim" style="margin:10px 0 0;font:400 13px/1.5 ${font};color:#6c6c70;text-align:center">On your Android phone, this adds the code in one tap.</p>
<p class="dim" style="margin:10px 0 0;font:400 13px/1.5 ${font};color:#6c6c70;text-align:center">Opens a page that adds the code in Folio on your Android phone.</p>
</td></tr>
<tr><td class="pad" style="padding:22px 32px 4px">
<p class="ink" style="margin:0 0 14px;font:400 16px/1.55 ${font};color:#1c1c1e">In Folio: <span class="path" style="background:#f2f2f7;color:#1c1c1e;border-radius:6px;padding:2px 7px;font-weight:600;white-space:nowrap">Settings › Supporter › Redeem a Code</span>, and paste it in. It is checked on your phone, so it works offline and tells nobody that you supported.</p>
Expand Down
21 changes: 11 additions & 10 deletions tools/kofi-worker/email.test.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* The supporter email: McCal's words kept as written, the HTML safe to build from a name anyone can type on Ko-fi, and
* the "Open in Folio" link in the exact shape RedeemActivity (app/.../RedeemActivity.kt, since 0.6.5) accepts.
* the button's link to the redeem page on foliolauncher.com, which hands the code to Folio's RedeemActivity.
* Run with `node --test email.test.mjs`; no network.
*/
import assert from 'node:assert/strict'
Expand Down Expand Up @@ -53,14 +53,15 @@ test('the HTML has no script, no javascript: link, and reaches out to nowhere bu
for (const line of codeLines(CODE)) assert.ok(html.includes(line))
})

test('the Open in Folio link is the one RedeemActivity reads', () => {
test('the button opens the redeem page on the site, with the code after the # so it never reaches a server', () => {
const link = redeemLink(CODE)
assert.equal(link, `folio://redeem?c=${CODE}`)
assert.ok(supporterEmail({ code: CODE, pool: 'm1' }).html.includes(`href="${link}"`))
assert.ok(supporterEmail({ code: CODE, pool: 'm1' }).text.includes(link))
// The app's redeemCode(): the prefix, a c= parameter, and 20 to 200 letters, digits and hyphens.
const query = link.slice('folio://redeem'.length).split('?')[1]
const code = decodeURIComponent(query.split('&').find((p) => p.startsWith('c=')).slice(2))
assert.ok(code.length >= 20 && code.length <= 200 && /^[A-Za-z0-9-]+$/.test(code))
assert.equal(code, CODE)
assert.equal(link, `https://foliolauncher.com/redeem/#${CODE}`)
const { html, text } = supporterEmail({ code: CODE, pool: 'm1' })
assert.ok(html.includes(`href="${link}"`))
assert.ok(text.includes(link))
assert.ok(!html.includes('folio://') && !text.includes('folio://'), 'Gmail does nothing with a custom scheme')
// The page (folio-site public/redeem.js) takes letters, digits and hyphens, 20 to 200 of them, from the fragment.
const fragment = decodeURIComponent(link.split('#')[1])
assert.ok(/^[A-Za-z0-9-]{20,200}$/.test(fragment))
assert.equal(fragment, CODE)
})
2 changes: 1 addition & 1 deletion tools/kofi-worker/worker.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,6 @@ test('the email goes to Resend as both HTML and plain text, with the link and th
assert.equal(body.reply_to, 'me@example.com')
assert.equal(body.subject, 'Your Folio supporter code')
assert.ok(body.text.includes('CODE-1') && body.html.includes('CODE-1'))
assert.ok(body.html.includes('href="folio://redeem?c=CODE-1"'))
assert.ok(body.html.includes('href="https://foliolauncher.com/redeem/#CODE-1"'))
assert.equal(DB.handled.get('m1').emailed, 1)
})
Loading