diff --git a/dripoffers.js b/dripoffers.js index d2443c2..e09029b 100644 --- a/dripoffers.js +++ b/dripoffers.js @@ -108,6 +108,23 @@ function countriesFor(geo, tiers) { return [...new Set(out.map(c => String(c).toUpperCase()))].join(','); } +// What a click-earner sees in the offerwall list. A campaign's `name` is the member's own +// internal label and is often too short to mean anything to a stranger ("MG"), so the ad's +// headline comes first, then the label, and a very short label is qualified with the +// destination host rather than dropped or dressed up in copy we invented for them. The +// platform's own floor is five characters. +function titleFor(campaign, url) { + const clean = v => String(v || '').replace(/\s+/g, ' ').trim(); + const head = clean(campaign.title), label = clean(campaign.name); + let host = ''; + try { host = new URL(url).hostname.replace(/^www\./, ''); } catch (e) {} + const pick = head.length >= 5 ? head + : label.length >= 5 ? label + : label && host ? label + ' (' + host + ')' + : host; + return pick.length >= 5 ? pick.slice(0, 250) : null; +} + // Push a campaign. Returns null when it is not worth syndicating (or cannot be targeted // faithfully) rather than throwing, so the caller treats "too small", "wrong format" and // "disabled" the same quiet way. @@ -125,7 +142,8 @@ async function push(campaign, opts) { const countries = countriesFor(campaign.geo, (opts && opts.tiers) || null); if (countries === null) return null; - const title = String(campaign.name || campaign.title || 'InstantAdPay').slice(0, 250); + const title = titleFor(campaign, url); + if (!title) return null; // The offerwall shows this line under the title, so it has to read like an offer rather // than like an internal campaign record. const description = String(campaign.title || campaign.body || campaign.name || title).slice(0, 250); @@ -156,6 +174,6 @@ async function remove(campaignId) { } async function packs() { return enabled() ? call('packs', {}) : null; } -module.exports = { enabled, push, readServed, pause, resume, remove, packs, +module.exports = { enabled, push, readServed, pause, resume, remove, packs, titleFor, clicksFor, countriesFor, kindOk, refFor, MIN_CREDITS, MAX_CLICKS, MIN_CLICKS, PACK_ID, KINDS }; diff --git a/qa/dripoffers-bridge.mjs b/qa/dripoffers-bridge.mjs index ea5bbad..6e722cc 100644 --- a/qa/dripoffers-bridge.mjs +++ b/qa/dripoffers-bridge.mjs @@ -24,6 +24,14 @@ t('tier 3 alone is skipped, not silently widened', drip.countriesFor('3', tiers) t('credits convert to clicks one for one', drip.clicksFor(4000) === 4000, String(drip.clicksFor(4000))); t('one campaign cannot swallow the platform', drip.clicksFor(500000) === drip.MAX_CLICKS, String(drip.clicksFor(500000))); +// the offerwall title a stranger actually reads (the platform's floor is 5 characters) +const T = (c, u) => drip.titleFor(c, u || 'https://www.mailer.gold/x'); +t('the ad headline wins over the internal label', T({ title: 'Get paid instantly', name: 'MG' }) === 'Get paid instantly', String(T({ title: 'Get paid instantly', name: 'MG' }))); +t('a usable label is used as-is', T({ name: 'Branded Voice' }) === 'Branded Voice', String(T({ name: 'Branded Voice' }))); +t('a two-letter label is qualified, not dropped', T({ name: 'MG' }) === 'MG (mailer.gold)', String(T({ name: 'MG' }))); +t('no label at all falls back to the destination', T({}) === 'mailer.gold', String(T({}))); +t('nothing usable returns null rather than a made-up title', T({}, 'not a url') === null, String(T({}, 'not a url'))); + t('a click format is accepted', drip.kindOk('banner') && drip.kindOk('visits') && drip.kindOk('text')); t('video and solo are not click formats', !drip.kindOk('video') && !drip.kindOk('solo') && !drip.kindOk('featured'));