insert-affiliate-js-sdk 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +28 -0
- package/CHANGELOG.md +19 -0
- package/README.md +175 -0
- package/dist/index.d.ts +262 -1
- package/dist/index.js +993 -0
- package/package.json +7 -3
- package/src/index.ts +16 -1
- package/src/referrals/referAFriendModal.ts +534 -0
- package/src/referrals/referralApi.ts +385 -0
- package/src/referrals/referralStrings.ts +85 -0
- package/src/referrals/referralTypes.ts +211 -0
- package/src/referrals/referrerTokenStore.ts +42 -0
- package/src/sdk/InsertAffiliate.ts +263 -0
package/dist/index.js
CHANGED
|
@@ -73,6 +73,786 @@ if (!String.prototype.hasOwnProperty("hashCode")) {
|
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// src/referrals/referrerTokenStore.ts
|
|
77
|
+
var KEY_PREFIX = "insertAffiliateReferrerToken_";
|
|
78
|
+
var memoryTokens = {};
|
|
79
|
+
var keyFor = (companyId) => `${KEY_PREFIX}${companyId}`;
|
|
80
|
+
var readReferrerToken = (companyId) => {
|
|
81
|
+
let stored = null;
|
|
82
|
+
try {
|
|
83
|
+
stored = localStorage.getItem(keyFor(companyId));
|
|
84
|
+
} catch (e) {
|
|
85
|
+
}
|
|
86
|
+
return stored || memoryTokens[companyId] || null;
|
|
87
|
+
};
|
|
88
|
+
var saveReferrerToken = (companyId, token) => {
|
|
89
|
+
try {
|
|
90
|
+
localStorage.setItem(keyFor(companyId), token);
|
|
91
|
+
delete memoryTokens[companyId];
|
|
92
|
+
return true;
|
|
93
|
+
} catch (e) {
|
|
94
|
+
memoryTokens[companyId] = token;
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
var clearReferrerToken = (companyId) => {
|
|
99
|
+
delete memoryTokens[companyId];
|
|
100
|
+
try {
|
|
101
|
+
localStorage.removeItem(keyFor(companyId));
|
|
102
|
+
} catch (e) {
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// src/referrals/referralApi.ts
|
|
107
|
+
var BASE_URL = "https://api.insertaffiliate.com/V1/sdk/affiliate";
|
|
108
|
+
var TOKEN_HEADER = "X-Insert-Affiliate-Token";
|
|
109
|
+
var PLATFORM = "web";
|
|
110
|
+
var TRIGGERS = ["install", "event", "purchase"];
|
|
111
|
+
var str = (value) => typeof value === "string" ? value : "";
|
|
112
|
+
var num = (value) => {
|
|
113
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
114
|
+
return Number.isFinite(n) ? n : 0;
|
|
115
|
+
};
|
|
116
|
+
var trigger = (value) => TRIGGERS.includes(value) ? value : "purchase";
|
|
117
|
+
var isObject = (value) => typeof value === "object" && value !== null;
|
|
118
|
+
var parseReferrerAffiliate = (raw) => {
|
|
119
|
+
const data = isObject(raw) ? raw : {};
|
|
120
|
+
return {
|
|
121
|
+
affiliateName: str(data.affiliateName),
|
|
122
|
+
affiliateShortCode: str(data.affiliateShortCode),
|
|
123
|
+
deeplinkurl: str(data.deeplinkurl)
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
var parseRewardCodes = (value) => (Array.isArray(value) ? value : []).filter(isObject).map((item) => ({
|
|
127
|
+
code: str(item.code),
|
|
128
|
+
redeemUrl: str(item.redeemUrl),
|
|
129
|
+
store: str(item.store) || "app_store",
|
|
130
|
+
grantedAt: str(item.grantedAt)
|
|
131
|
+
})).filter((item) => item.code);
|
|
132
|
+
var parseMyAffiliateDetails = (raw) => {
|
|
133
|
+
const data = isObject(raw) ? raw : {};
|
|
134
|
+
return __spreadProps(__spreadValues({}, parseReferrerAffiliate(data)), {
|
|
135
|
+
referralTrigger: trigger(data.referralTrigger),
|
|
136
|
+
referralCount: num(data.referralCount),
|
|
137
|
+
installCount: num(data.installCount),
|
|
138
|
+
eventCount: num(data.eventCount),
|
|
139
|
+
purchaseCount: num(data.purchaseCount),
|
|
140
|
+
totalEarned: num(data.totalEarned),
|
|
141
|
+
totalPaid: num(data.totalPaid),
|
|
142
|
+
totalUnpaid: num(data.totalUnpaid),
|
|
143
|
+
currency: str(data.currency) || "USD",
|
|
144
|
+
dashboardUrl: str(data.dashboardUrl),
|
|
145
|
+
rewardsGranted: num(data.rewardsGranted),
|
|
146
|
+
premiumUntil: str(data.premiumUntil) || null,
|
|
147
|
+
rewardCodes: parseRewardCodes(data.rewardCodes)
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
var parseReferralProgramConfig = (raw) => {
|
|
151
|
+
const data = isObject(raw) ? raw : {};
|
|
152
|
+
const color = str(data.primaryColor);
|
|
153
|
+
return {
|
|
154
|
+
enabled: data.enabled === true,
|
|
155
|
+
companyName: str(data.companyName),
|
|
156
|
+
referralTrigger: trigger(data.referralTrigger),
|
|
157
|
+
headline: str(data.headline),
|
|
158
|
+
rewardText: str(data.rewardText),
|
|
159
|
+
primaryColor: /^#[0-9a-fA-F]{6}$/.test(color) ? color : ""
|
|
160
|
+
};
|
|
161
|
+
};
|
|
162
|
+
var errorResult = (code, message) => ({
|
|
163
|
+
status: "error",
|
|
164
|
+
errorCode: code,
|
|
165
|
+
errorMessage: message
|
|
166
|
+
});
|
|
167
|
+
var parseEnrolmentResponse = (httpStatus, body) => {
|
|
168
|
+
const data = isObject(body) ? body : {};
|
|
169
|
+
if (httpStatus < 200 || httpStatus >= 300) {
|
|
170
|
+
return {
|
|
171
|
+
result: errorResult(
|
|
172
|
+
str(data.code) || `HTTP_${httpStatus}`,
|
|
173
|
+
str(data.error) || `Request failed with status ${httpStatus}.`
|
|
174
|
+
),
|
|
175
|
+
token: null
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if (data.status === "verificationRequired") {
|
|
179
|
+
return { result: { status: "verificationRequired" }, token: null };
|
|
180
|
+
}
|
|
181
|
+
const token = str(data.token);
|
|
182
|
+
if ((data.status === "created" || data.status === "connected") && token) {
|
|
183
|
+
return {
|
|
184
|
+
result: { status: data.status, affiliate: parseReferrerAffiliate(data.affiliate) },
|
|
185
|
+
token
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return { result: errorResult("NETWORK_ERROR", "Unexpected response from the server."), token: null };
|
|
189
|
+
};
|
|
190
|
+
var DECIMAL_DIGIT = new RegExp("\\p{Nd}", "u");
|
|
191
|
+
var normalizeVerificationCode = (value) => {
|
|
192
|
+
let out = "";
|
|
193
|
+
Array.from(String(value == null ? "" : value)).forEach((char) => {
|
|
194
|
+
if (char >= "0" && char <= "9") {
|
|
195
|
+
out += char;
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (!DECIMAL_DIGIT.test(char)) return;
|
|
199
|
+
const point = char.codePointAt(0);
|
|
200
|
+
let start = point;
|
|
201
|
+
while (start > 0 && DECIMAL_DIGIT.test(String.fromCodePoint(start - 1))) start -= 1;
|
|
202
|
+
out += String((point - start) % 10);
|
|
203
|
+
});
|
|
204
|
+
return out;
|
|
205
|
+
};
|
|
206
|
+
var buildReferralShareText = (affiliate, companyName, message) => {
|
|
207
|
+
const code = affiliate.affiliateShortCode || "";
|
|
208
|
+
const hasLink = /^http/i.test(affiliate.deeplinkurl || "");
|
|
209
|
+
const link = hasLink ? affiliate.deeplinkurl : code;
|
|
210
|
+
const appName = companyName || "the app";
|
|
211
|
+
const custom = (message || "").trim();
|
|
212
|
+
if (!link) return "";
|
|
213
|
+
if (custom && /\{link\}|\{code\}/.test(custom)) {
|
|
214
|
+
return custom.replace(/\{link\}/g, link).replace(/\{code\}/g, code);
|
|
215
|
+
}
|
|
216
|
+
if (hasLink) {
|
|
217
|
+
return `${custom || `Try ${companyName || "this app"}:`} ${link}`;
|
|
218
|
+
}
|
|
219
|
+
return custom ? `${custom} ${code}` : `Use my code ${code} in ${appName}`;
|
|
220
|
+
};
|
|
221
|
+
var readJson = async (response) => {
|
|
222
|
+
try {
|
|
223
|
+
return await response.json();
|
|
224
|
+
} catch (e) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
var withoutEmpty = (payload) => {
|
|
229
|
+
const out = {};
|
|
230
|
+
Object.keys(payload).forEach((key) => {
|
|
231
|
+
const value = payload[key];
|
|
232
|
+
if (typeof value === "string" && value.trim()) out[key] = value.trim();
|
|
233
|
+
});
|
|
234
|
+
return out;
|
|
235
|
+
};
|
|
236
|
+
var networkError = () => errorResult("NETWORK_ERROR", "Could not reach Insert Affiliate. Check the connection and try again.");
|
|
237
|
+
var fetchReferralProgramConfig = async (companyId, log) => {
|
|
238
|
+
const url = `${BASE_URL}/config/${encodeURIComponent(companyId)}`;
|
|
239
|
+
log(`Making API call to: ${url}`);
|
|
240
|
+
try {
|
|
241
|
+
const response = await fetch(url);
|
|
242
|
+
log(`Referral config response status: ${response.status}`);
|
|
243
|
+
if (!response.ok) return null;
|
|
244
|
+
return parseReferralProgramConfig(await readJson(response));
|
|
245
|
+
} catch (error) {
|
|
246
|
+
log(`Error fetching referral config: ${error}`);
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
var postEnrolment = async (path, companyId, payload, log) => {
|
|
251
|
+
const url = `${BASE_URL}/${path}`;
|
|
252
|
+
log(`Making API call to: ${url}`);
|
|
253
|
+
try {
|
|
254
|
+
const response = await fetch(url, {
|
|
255
|
+
method: "POST",
|
|
256
|
+
headers: { "Content-Type": "application/json" },
|
|
257
|
+
body: JSON.stringify(__spreadValues({ companyId, platform: PLATFORM }, withoutEmpty(payload)))
|
|
258
|
+
});
|
|
259
|
+
log(`Referral ${path} response status: ${response.status}`);
|
|
260
|
+
const { result, token } = parseEnrolmentResponse(response.status, await readJson(response));
|
|
261
|
+
if (token && !saveReferrerToken(companyId, token)) {
|
|
262
|
+
log("localStorage unavailable; referrer token kept for this page load only");
|
|
263
|
+
}
|
|
264
|
+
log(`Referral ${path} result: ${result.status}${result.errorCode ? ` (${result.errorCode})` : ""}`);
|
|
265
|
+
return result;
|
|
266
|
+
} catch (error) {
|
|
267
|
+
log(`Network error during referral ${path}: ${error}`);
|
|
268
|
+
return networkError();
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
var isRevokedTokenResponse = (httpStatus, body) => {
|
|
272
|
+
const code = isObject(body) ? str(body.code) : "";
|
|
273
|
+
return httpStatus === 401 && code === "INVALID_TOKEN" || httpStatus === 404 && code === "AFFILIATE_NOT_FOUND";
|
|
274
|
+
};
|
|
275
|
+
var clearRevokedToken = (companyId, sentToken, log) => {
|
|
276
|
+
if (readReferrerToken(companyId) !== sentToken) {
|
|
277
|
+
log("Referrer token no longer valid; a newer token is stored, keeping it");
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
clearReferrerToken(companyId);
|
|
281
|
+
log("Referrer token no longer valid; cleared");
|
|
282
|
+
};
|
|
283
|
+
var fetchMyAffiliateDetails = async (companyId, token, log) => {
|
|
284
|
+
const url = `${BASE_URL}/me`;
|
|
285
|
+
log(`Making API call to: ${url}`);
|
|
286
|
+
try {
|
|
287
|
+
const response = await fetch(url, { headers: { [TOKEN_HEADER]: token } });
|
|
288
|
+
log(`Referral details response status: ${response.status}`);
|
|
289
|
+
const body = await readJson(response);
|
|
290
|
+
if (isRevokedTokenResponse(response.status, body)) {
|
|
291
|
+
clearRevokedToken(companyId, token, log);
|
|
292
|
+
return { kind: "signedOut" };
|
|
293
|
+
}
|
|
294
|
+
if (!response.ok || !isObject(body)) return { kind: "failed" };
|
|
295
|
+
return { kind: "ok", details: parseMyAffiliateDetails(body) };
|
|
296
|
+
} catch (error) {
|
|
297
|
+
log(`Error fetching referral details: ${error}`);
|
|
298
|
+
return { kind: "failed" };
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
var postReferrerIdentity = async (companyId, token, payload, log) => {
|
|
302
|
+
const url = `${BASE_URL}/me/identity`;
|
|
303
|
+
log(`Making API call to: ${url}`);
|
|
304
|
+
try {
|
|
305
|
+
const response = await fetch(url, {
|
|
306
|
+
method: "POST",
|
|
307
|
+
headers: { "Content-Type": "application/json", [TOKEN_HEADER]: token },
|
|
308
|
+
body: JSON.stringify(withoutEmpty(payload))
|
|
309
|
+
});
|
|
310
|
+
log(`Referrer identity response status: ${response.status}`);
|
|
311
|
+
const body = await readJson(response);
|
|
312
|
+
if (isRevokedTokenResponse(response.status, body)) {
|
|
313
|
+
clearRevokedToken(companyId, token, log);
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
return response.ok && isObject(body) && body.saved === true;
|
|
317
|
+
} catch (error) {
|
|
318
|
+
log(`Error saving referrer identity: ${error}`);
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
var copyText = async (text) => {
|
|
323
|
+
try {
|
|
324
|
+
if (typeof navigator !== "undefined" && navigator.clipboard && navigator.clipboard.writeText) {
|
|
325
|
+
await navigator.clipboard.writeText(text);
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
} catch (e) {
|
|
329
|
+
}
|
|
330
|
+
return false;
|
|
331
|
+
};
|
|
332
|
+
var shareText = async (text) => {
|
|
333
|
+
if (!text) return "failed";
|
|
334
|
+
if (typeof navigator !== "undefined" && typeof navigator.share === "function") {
|
|
335
|
+
try {
|
|
336
|
+
await navigator.share({ text });
|
|
337
|
+
return "shared";
|
|
338
|
+
} catch (error) {
|
|
339
|
+
if (error && error.name === "AbortError") return "cancelled";
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return await copyText(text) ? "copied" : "failed";
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// src/referrals/referralStrings.ts
|
|
346
|
+
var DEFAULT_REFERRAL_STRINGS = {
|
|
347
|
+
// Joining
|
|
348
|
+
emailLabel: "Email",
|
|
349
|
+
nameLabel: "Name",
|
|
350
|
+
joinButton: "Get my link",
|
|
351
|
+
joiningButton: "Please wait...",
|
|
352
|
+
// Email code step
|
|
353
|
+
codeLabel: "Code",
|
|
354
|
+
codeSentNotice: "We sent a 6-digit code to {email}. Enter it below to connect this device.",
|
|
355
|
+
verifyButton: "Verify",
|
|
356
|
+
verifyingButton: "Verifying...",
|
|
357
|
+
resendButton: "Send a new code",
|
|
358
|
+
sendingNotice: "Sending...",
|
|
359
|
+
codeResentNotice: "We sent a new code.",
|
|
360
|
+
differentEmailButton: "Use a different email",
|
|
361
|
+
errorCodeLength: "Enter the 6-digit code from the email.",
|
|
362
|
+
// Joined
|
|
363
|
+
copyCodeButton: "Copy code",
|
|
364
|
+
copyLinkButton: "Copy link",
|
|
365
|
+
copiedNotice: "Copied",
|
|
366
|
+
copyFailedNotice: "Could not copy. Select the text to copy it.",
|
|
367
|
+
shareButton: "Share",
|
|
368
|
+
shareFailedNotice: "Could not share. Copy your code instead.",
|
|
369
|
+
referralsLabel: "Referrals",
|
|
370
|
+
earnedLabel: "Earned",
|
|
371
|
+
premiumUntil: "Free premium until {date}",
|
|
372
|
+
rewardsHeading: "Your rewards",
|
|
373
|
+
redeemButton: "Redeem",
|
|
374
|
+
dashboardLink: "Open my dashboard",
|
|
375
|
+
// Frame and states
|
|
376
|
+
closeButton: "Close",
|
|
377
|
+
loading: "Loading...",
|
|
378
|
+
tryAgainButton: "Try again",
|
|
379
|
+
// Errors, by the server's error code
|
|
380
|
+
errorProgramDisabled: "Referrals are not available in this app right now.",
|
|
381
|
+
errorAffiliateLimitReached: "The referral program is full right now. Please try again later.",
|
|
382
|
+
errorInvalidCode: "That code is wrong or has expired. Check it or send a new code.",
|
|
383
|
+
errorTooManyCodes: "Too many codes have been sent. Please wait a while and try again.",
|
|
384
|
+
errorRateLimited: "Too many attempts. Please try again later.",
|
|
385
|
+
errorInvalidEmail: "Please enter a valid email address.",
|
|
386
|
+
errorNetwork: "Could not connect. Check your connection and try again.",
|
|
387
|
+
errorServer: "Something went wrong. Please try again."
|
|
388
|
+
};
|
|
389
|
+
var ERROR_STRING_KEYS = {
|
|
390
|
+
PROGRAM_DISABLED: "errorProgramDisabled",
|
|
391
|
+
AFFILIATE_LIMIT_REACHED: "errorAffiliateLimitReached",
|
|
392
|
+
INVALID_CODE: "errorInvalidCode",
|
|
393
|
+
TOO_MANY_CODES: "errorTooManyCodes",
|
|
394
|
+
RATE_LIMITED: "errorRateLimited",
|
|
395
|
+
INVALID_EMAIL: "errorInvalidEmail",
|
|
396
|
+
NETWORK_ERROR: "errorNetwork"
|
|
397
|
+
};
|
|
398
|
+
var resolveReferralStrings = (overrides) => {
|
|
399
|
+
const resolved = __spreadValues({}, DEFAULT_REFERRAL_STRINGS);
|
|
400
|
+
if (!overrides) return resolved;
|
|
401
|
+
Object.keys(DEFAULT_REFERRAL_STRINGS).forEach((key) => {
|
|
402
|
+
const value = overrides[key];
|
|
403
|
+
if (typeof value === "string" && value.trim()) resolved[key] = value;
|
|
404
|
+
});
|
|
405
|
+
return resolved;
|
|
406
|
+
};
|
|
407
|
+
var fillPlaceholders = (text, values) => Object.keys(values).reduce(
|
|
408
|
+
(out, name) => out.split(`{${name}}`).join(values[name]),
|
|
409
|
+
text
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
// src/referrals/referAFriendModal.ts
|
|
413
|
+
var DEFAULT_COLOR = "#6A0DAD";
|
|
414
|
+
var DEFAULT_HEADLINE = "Refer a friend";
|
|
415
|
+
var DEFAULT_FONT = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
416
|
+
var STYLE = `
|
|
417
|
+
.ia-raf-overlay{position:fixed;inset:0;z-index:2147483000;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;padding:16px;box-sizing:border-box}
|
|
418
|
+
.ia-raf-dialog{position:relative;width:100%;max-width:420px;max-height:calc(100vh - 32px);overflow:auto;background:#fff;color:#1a1a1a;border-radius:var(--ia-raf-radius);box-shadow:0 12px 40px rgba(0,0,0,.25);padding:24px;box-sizing:border-box;font-family:var(--ia-raf-font);font-size:15px;line-height:1.45;text-align:left}
|
|
419
|
+
.ia-raf-dialog *{box-sizing:border-box}
|
|
420
|
+
.ia-raf-title{margin:0 36px 8px 0;font-size:20px;font-weight:700;line-height:1.3}
|
|
421
|
+
.ia-raf-reward{margin:0 0 16px;color:#444}
|
|
422
|
+
.ia-raf-close{position:absolute;margin:0;top:19px;right:14px;width:36px;height:36px;display:flex;align-items:center;justify-content:center;padding:0 0 2px;border:0;border-radius:50%;background:transparent;color:#555;font-size:26px;line-height:1;cursor:pointer}
|
|
423
|
+
.ia-raf-close:hover{background:#f0f0f3}
|
|
424
|
+
.ia-raf-label{display:block;margin:0 0 4px;font-size:13px;font-weight:600}
|
|
425
|
+
.ia-raf-input{display:block;width:100%;margin:0 0 12px;padding:10px 12px;border:1px solid #c8c8cc;border-radius:calc(var(--ia-raf-radius) * .66);background:#fff;color:#1a1a1a;font:inherit}
|
|
426
|
+
.ia-raf-dialog :focus-visible{outline:2px solid var(--ia-raf-primary);outline-offset:2px}
|
|
427
|
+
.ia-raf-btn{display:block;width:100%;margin:8px 0 0;padding:12px;border:1px solid var(--ia-raf-primary);border-radius:calc(var(--ia-raf-radius) * .66);background:var(--ia-raf-primary);color:#fff;font:inherit;font-weight:600;cursor:pointer}
|
|
428
|
+
.ia-raf-btn[disabled]{opacity:.6;cursor:default}
|
|
429
|
+
.ia-raf-btn-secondary{background:transparent;color:var(--ia-raf-primary)}
|
|
430
|
+
.ia-raf-textbtn{display:inline-block;margin:8px 16px 0 0;padding:4px 0;border:0;background:none;color:var(--ia-raf-primary);font:inherit;font-size:14px;text-decoration:underline;cursor:pointer}
|
|
431
|
+
.ia-raf-error{margin:0 0 12px;padding:8px 12px;border-radius:calc(var(--ia-raf-radius) * .5);background:#fdecee;color:#b00020;font-size:14px}
|
|
432
|
+
.ia-raf-error:empty{display:none}
|
|
433
|
+
.ia-raf-muted{margin:0 0 12px;color:#555;font-size:14px}
|
|
434
|
+
.ia-raf-row{display:flex;align-items:center;gap:8px;margin:0 0 8px;padding:10px 12px;border:1px dashed var(--ia-raf-primary);border-radius:calc(var(--ia-raf-radius) * .66)}
|
|
435
|
+
.ia-raf-code{flex:1;min-width:0;font-size:20px;font-weight:700;letter-spacing:1px;word-break:break-all}
|
|
436
|
+
.ia-raf-url{flex:1;min-width:0;font-size:13px;color:#444;word-break:break-all}
|
|
437
|
+
.ia-raf-copy{flex:none;margin:0;padding:6px 10px;border:1px solid var(--ia-raf-primary);border-radius:calc(var(--ia-raf-radius) * .5);background:transparent;color:var(--ia-raf-primary);font:inherit;font-size:13px;font-weight:600;cursor:pointer}
|
|
438
|
+
.ia-raf-stats{display:flex;gap:12px;margin:16px 0 8px}
|
|
439
|
+
.ia-raf-stat{flex:1;padding:12px;border-radius:calc(var(--ia-raf-radius) * .66);background:#f4f4f7;text-align:center}
|
|
440
|
+
.ia-raf-stat-value{display:block;font-size:20px;font-weight:700}
|
|
441
|
+
.ia-raf-stat-label{display:block;color:#666;font-size:12px}
|
|
442
|
+
.ia-raf-premium{margin:8px 0;padding:10px 12px;border-radius:calc(var(--ia-raf-radius) * .66);background:#eef7ef;color:#1b5e20;font-size:14px;font-weight:600;text-align:center}
|
|
443
|
+
.ia-raf-rewards-title{margin:16px 0 8px;font-size:15px;font-weight:700}
|
|
444
|
+
.ia-raf-rewards{margin:0;padding:0;list-style:none}
|
|
445
|
+
.ia-raf-reward-code{flex:1;min-width:0;font-size:15px;font-weight:600;letter-spacing:.5px;word-break:break-all}
|
|
446
|
+
.ia-raf-status{min-height:20px;margin:8px 0 0;color:#2e7d32;font-size:13px;text-align:center}
|
|
447
|
+
`;
|
|
448
|
+
var instanceCount = 0;
|
|
449
|
+
var openModal = null;
|
|
450
|
+
function el(tag, props = {}, children = []) {
|
|
451
|
+
const node = document.createElement(tag);
|
|
452
|
+
if (props.className) node.className = props.className;
|
|
453
|
+
if (props.text !== void 0) node.textContent = props.text;
|
|
454
|
+
if (props.attrs) {
|
|
455
|
+
Object.keys(props.attrs).forEach((name) => node.setAttribute(name, props.attrs[name]));
|
|
456
|
+
}
|
|
457
|
+
children.forEach((child) => {
|
|
458
|
+
if (child === null || child === void 0 || child === false) return;
|
|
459
|
+
node.appendChild(typeof child === "string" ? document.createTextNode(child) : child);
|
|
460
|
+
});
|
|
461
|
+
return node;
|
|
462
|
+
}
|
|
463
|
+
function button(text, className, onClick) {
|
|
464
|
+
const node = el("button", { className, text, attrs: { type: "button" } });
|
|
465
|
+
node.addEventListener("click", onClick);
|
|
466
|
+
return node;
|
|
467
|
+
}
|
|
468
|
+
function formatMoney(amount, currency) {
|
|
469
|
+
try {
|
|
470
|
+
return new Intl.NumberFormat(void 0, { style: "currency", currency: currency || "USD" }).format(amount);
|
|
471
|
+
} catch (e) {
|
|
472
|
+
return `${amount.toFixed(2)} ${currency}`;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
var isHttpsUrl = (value) => /^https:\/\//i.test(value);
|
|
476
|
+
function futureDateLabel(iso, now = Date.now()) {
|
|
477
|
+
if (!iso) return null;
|
|
478
|
+
const date = new Date(iso);
|
|
479
|
+
if (Number.isNaN(date.getTime()) || date.getTime() <= now) return null;
|
|
480
|
+
try {
|
|
481
|
+
return date.toLocaleDateString(void 0, { year: "numeric", month: "long", day: "numeric" });
|
|
482
|
+
} catch (e) {
|
|
483
|
+
return date.toDateString();
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function presentReferAFriend(options, deps) {
|
|
487
|
+
if (openModal) {
|
|
488
|
+
openModal.focus();
|
|
489
|
+
return openModal.handle;
|
|
490
|
+
}
|
|
491
|
+
instanceCount += 1;
|
|
492
|
+
const titleId = `ia-raf-title-${instanceCount}`;
|
|
493
|
+
let closed = false;
|
|
494
|
+
let email = (options.email || "").trim();
|
|
495
|
+
let name = (options.name || "").trim();
|
|
496
|
+
let companyName = "";
|
|
497
|
+
let busy = false;
|
|
498
|
+
let accountSaved = false;
|
|
499
|
+
const text = resolveReferralStrings(options.strings);
|
|
500
|
+
const messageFor = (code) => text[code && ERROR_STRING_KEYS[code] || "errorServer"];
|
|
501
|
+
const previousFocus = document.activeElement;
|
|
502
|
+
const previousOverflow = document.body.style.overflow;
|
|
503
|
+
const style = el("style", { text: STYLE });
|
|
504
|
+
const title = el("h2", { className: "ia-raf-title", text: options.headline || DEFAULT_HEADLINE, attrs: { id: titleId } });
|
|
505
|
+
const reward = el("p", { className: "ia-raf-reward", text: options.rewardText || "" });
|
|
506
|
+
reward.hidden = !options.rewardText;
|
|
507
|
+
const body = el("div");
|
|
508
|
+
const closeButton = button("\xD7", "ia-raf-close", () => close());
|
|
509
|
+
closeButton.setAttribute("aria-label", text.closeButton);
|
|
510
|
+
const dialog = el("div", {
|
|
511
|
+
className: "ia-raf-dialog",
|
|
512
|
+
attrs: { role: "dialog", "aria-modal": "true", "aria-labelledby": titleId }
|
|
513
|
+
}, [closeButton, title, reward, body]);
|
|
514
|
+
dialog.style.setProperty("--ia-raf-font", options.fontFamily || DEFAULT_FONT);
|
|
515
|
+
dialog.style.setProperty("--ia-raf-radius", `${typeof options.cornerRadius === "number" ? Math.max(0, options.cornerRadius) : 12}px`);
|
|
516
|
+
setColor(options.primaryColor || DEFAULT_COLOR);
|
|
517
|
+
const overlay = el("div", { className: "ia-raf-overlay" }, [dialog]);
|
|
518
|
+
overlay.addEventListener("mousedown", (event) => {
|
|
519
|
+
if (event.target === overlay) close();
|
|
520
|
+
});
|
|
521
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
522
|
+
document.head.appendChild(style);
|
|
523
|
+
document.body.appendChild(overlay);
|
|
524
|
+
document.body.style.overflow = "hidden";
|
|
525
|
+
function setColor(color) {
|
|
526
|
+
dialog.style.setProperty("--ia-raf-primary", color);
|
|
527
|
+
}
|
|
528
|
+
function close() {
|
|
529
|
+
if (closed) return;
|
|
530
|
+
closed = true;
|
|
531
|
+
if (openModal && openModal.handle === handle) openModal = null;
|
|
532
|
+
document.removeEventListener("keydown", onKeyDown, true);
|
|
533
|
+
overlay.remove();
|
|
534
|
+
style.remove();
|
|
535
|
+
document.body.style.overflow = previousOverflow;
|
|
536
|
+
if (previousFocus && typeof previousFocus.focus === "function") {
|
|
537
|
+
try {
|
|
538
|
+
previousFocus.focus();
|
|
539
|
+
} catch (e) {
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (options.onClose) options.onClose();
|
|
543
|
+
}
|
|
544
|
+
function focusables() {
|
|
545
|
+
const selector = 'button:not([disabled]), input:not([disabled]), [href], [tabindex]:not([tabindex="-1"])';
|
|
546
|
+
return Array.prototype.slice.call(dialog.querySelectorAll(selector));
|
|
547
|
+
}
|
|
548
|
+
function onKeyDown(event) {
|
|
549
|
+
if (event.key === "Escape") {
|
|
550
|
+
event.preventDefault();
|
|
551
|
+
event.stopPropagation();
|
|
552
|
+
close();
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
if (event.key !== "Tab") return;
|
|
556
|
+
const items = focusables();
|
|
557
|
+
if (!items.length) return;
|
|
558
|
+
const first = items[0];
|
|
559
|
+
const last = items[items.length - 1];
|
|
560
|
+
if (!dialog.contains(document.activeElement)) {
|
|
561
|
+
event.preventDefault();
|
|
562
|
+
first.focus();
|
|
563
|
+
} else if (event.shiftKey && document.activeElement === first) {
|
|
564
|
+
event.preventDefault();
|
|
565
|
+
last.focus();
|
|
566
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
567
|
+
event.preventDefault();
|
|
568
|
+
first.focus();
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
function render(children, focusTarget) {
|
|
572
|
+
if (closed) return;
|
|
573
|
+
body.textContent = "";
|
|
574
|
+
children.forEach((child) => {
|
|
575
|
+
if (child) body.appendChild(typeof child === "string" ? document.createTextNode(child) : child);
|
|
576
|
+
});
|
|
577
|
+
const target = focusTarget || body.querySelector("input, button") || closeButton;
|
|
578
|
+
target.focus();
|
|
579
|
+
}
|
|
580
|
+
function renderLoading() {
|
|
581
|
+
render([el("p", { className: "ia-raf-muted", text: text.loading, attrs: { role: "status" } })], closeButton);
|
|
582
|
+
}
|
|
583
|
+
function renderMessage(message, retry) {
|
|
584
|
+
render([
|
|
585
|
+
el("p", { className: "ia-raf-error", text: message, attrs: { role: "alert" } }),
|
|
586
|
+
retry ? button(text.tryAgainButton, "ia-raf-btn", retry) : null
|
|
587
|
+
]);
|
|
588
|
+
}
|
|
589
|
+
function errorBox() {
|
|
590
|
+
return el("p", { className: "ia-raf-error", attrs: { role: "alert" } });
|
|
591
|
+
}
|
|
592
|
+
function field(label, input) {
|
|
593
|
+
input.className = "ia-raf-input";
|
|
594
|
+
input.id = `${titleId}-${input.name}`;
|
|
595
|
+
return [el("label", { className: "ia-raf-label", text: label, attrs: { for: input.id } }), input];
|
|
596
|
+
}
|
|
597
|
+
async function runBusy(submit, busyText, task) {
|
|
598
|
+
if (busy) return;
|
|
599
|
+
busy = true;
|
|
600
|
+
const idleText = submit.textContent || "";
|
|
601
|
+
submit.disabled = true;
|
|
602
|
+
submit.textContent = busyText;
|
|
603
|
+
try {
|
|
604
|
+
await task();
|
|
605
|
+
} finally {
|
|
606
|
+
busy = false;
|
|
607
|
+
if (!closed && submit.isConnected) {
|
|
608
|
+
submit.disabled = false;
|
|
609
|
+
submit.textContent = idleText;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
function renderEnrolForm() {
|
|
614
|
+
const error = errorBox();
|
|
615
|
+
const emailInput = el("input", {
|
|
616
|
+
attrs: { type: "email", name: "email", autocomplete: "email", required: "true" }
|
|
617
|
+
});
|
|
618
|
+
emailInput.value = email;
|
|
619
|
+
const nameInput = el("input", { attrs: { type: "text", name: "name", autocomplete: "name" } });
|
|
620
|
+
nameInput.value = name;
|
|
621
|
+
const submit = el("button", { className: "ia-raf-btn", text: text.joinButton, attrs: { type: "submit" } });
|
|
622
|
+
const form = el("form", { attrs: { novalidate: "true" } }, [
|
|
623
|
+
error,
|
|
624
|
+
...field(text.emailLabel, emailInput),
|
|
625
|
+
...field(text.nameLabel, nameInput),
|
|
626
|
+
submit
|
|
627
|
+
]);
|
|
628
|
+
form.addEventListener("submit", (event) => {
|
|
629
|
+
event.preventDefault();
|
|
630
|
+
email = emailInput.value.trim();
|
|
631
|
+
name = nameInput.value.trim();
|
|
632
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
633
|
+
error.textContent = text.errorInvalidEmail;
|
|
634
|
+
emailInput.focus();
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
error.textContent = "";
|
|
638
|
+
void runBusy(submit, text.joiningButton, async () => {
|
|
639
|
+
const result = await deps.enrol(email, name);
|
|
640
|
+
if (!closed) await handleEnrolment(result, error);
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
render([form], email ? submit : emailInput);
|
|
644
|
+
}
|
|
645
|
+
function renderCodeStep(notice) {
|
|
646
|
+
const error = errorBox();
|
|
647
|
+
const status = el("p", { className: "ia-raf-status", attrs: { role: "status", "aria-live": "polite" } });
|
|
648
|
+
const codeInput = el("input", {
|
|
649
|
+
attrs: {
|
|
650
|
+
type: "text",
|
|
651
|
+
name: "code",
|
|
652
|
+
inputmode: "numeric",
|
|
653
|
+
autocomplete: "one-time-code"
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
const submit = el("button", { className: "ia-raf-btn", text: text.verifyButton, attrs: { type: "submit" } });
|
|
657
|
+
let verifying = false;
|
|
658
|
+
const syncSubmit = () => {
|
|
659
|
+
if (!verifying) submit.disabled = normalizeVerificationCode(codeInput.value).length !== 6;
|
|
660
|
+
};
|
|
661
|
+
codeInput.addEventListener("input", syncSubmit);
|
|
662
|
+
syncSubmit();
|
|
663
|
+
const resend = button(text.resendButton, "ia-raf-textbtn", () => {
|
|
664
|
+
if (busy) return;
|
|
665
|
+
error.textContent = "";
|
|
666
|
+
status.textContent = text.sendingNotice;
|
|
667
|
+
busy = true;
|
|
668
|
+
void deps.enrol(email, name).then((result) => {
|
|
669
|
+
busy = false;
|
|
670
|
+
if (closed) return;
|
|
671
|
+
status.textContent = "";
|
|
672
|
+
if (result.status === "verificationRequired") {
|
|
673
|
+
status.textContent = text.codeResentNotice;
|
|
674
|
+
} else {
|
|
675
|
+
void handleEnrolment(result, error);
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
const changeEmail = button(text.differentEmailButton, "ia-raf-textbtn", () => {
|
|
680
|
+
if (!busy) renderEnrolForm();
|
|
681
|
+
});
|
|
682
|
+
const form = el("form", { attrs: { novalidate: "true" } }, [
|
|
683
|
+
el("p", { className: "ia-raf-muted", text: fillPlaceholders(text.codeSentNotice, { email }) }),
|
|
684
|
+
error,
|
|
685
|
+
...field(text.codeLabel, codeInput),
|
|
686
|
+
submit,
|
|
687
|
+
el("div", {}, [resend, changeEmail]),
|
|
688
|
+
status
|
|
689
|
+
]);
|
|
690
|
+
form.addEventListener("submit", (event) => {
|
|
691
|
+
event.preventDefault();
|
|
692
|
+
const code = normalizeVerificationCode(codeInput.value);
|
|
693
|
+
if (code.length !== 6) {
|
|
694
|
+
error.textContent = text.errorCodeLength;
|
|
695
|
+
codeInput.focus();
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
error.textContent = "";
|
|
699
|
+
verifying = true;
|
|
700
|
+
void runBusy(submit, text.verifyingButton, async () => {
|
|
701
|
+
const result = await deps.verify(email, code, name);
|
|
702
|
+
if (!closed) await handleEnrolment(result, error);
|
|
703
|
+
}).then(() => {
|
|
704
|
+
verifying = false;
|
|
705
|
+
if (!closed && submit.isConnected) syncSubmit();
|
|
706
|
+
});
|
|
707
|
+
});
|
|
708
|
+
render([form], codeInput);
|
|
709
|
+
if (notice) status.textContent = notice;
|
|
710
|
+
}
|
|
711
|
+
async function handleEnrolment(result, error) {
|
|
712
|
+
if (result.status === "verificationRequired") {
|
|
713
|
+
renderCodeStep();
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
if (result.status === "error") {
|
|
717
|
+
error.textContent = messageFor(result.errorCode);
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
renderLoading();
|
|
721
|
+
const loaded = await deps.loadDetails();
|
|
722
|
+
if (loaded.kind === "ok") {
|
|
723
|
+
renderEnrolled(loaded.details, loaded.details);
|
|
724
|
+
} else if (result.affiliate) {
|
|
725
|
+
renderEnrolled(result.affiliate, null);
|
|
726
|
+
} else {
|
|
727
|
+
renderMessage(messageFor("NETWORK_ERROR"), () => void start());
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
function renderEnrolled(affiliate, stats) {
|
|
731
|
+
const status = el("p", { className: "ia-raf-status", attrs: { role: "status", "aria-live": "polite" } });
|
|
732
|
+
const hasLink = /^http/i.test(affiliate.deeplinkurl);
|
|
733
|
+
const messageToShare = buildReferralShareText(affiliate, companyName, options.shareMessage);
|
|
734
|
+
const copyButton = (label, value) => {
|
|
735
|
+
const node = button(label, "ia-raf-copy", () => {
|
|
736
|
+
void copyText(value).then((ok) => {
|
|
737
|
+
if (closed) return;
|
|
738
|
+
status.textContent = ok ? text.copiedNotice : text.copyFailedNotice;
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
node.setAttribute("aria-label", `${label}: ${value}`);
|
|
742
|
+
return node;
|
|
743
|
+
};
|
|
744
|
+
const children = [];
|
|
745
|
+
if (affiliate.affiliateShortCode) {
|
|
746
|
+
children.push(el("div", { className: "ia-raf-row" }, [
|
|
747
|
+
el("span", { className: "ia-raf-code", text: affiliate.affiliateShortCode }),
|
|
748
|
+
copyButton(text.copyCodeButton, affiliate.affiliateShortCode)
|
|
749
|
+
]));
|
|
750
|
+
}
|
|
751
|
+
if (hasLink) {
|
|
752
|
+
children.push(el("div", { className: "ia-raf-row" }, [
|
|
753
|
+
el("span", { className: "ia-raf-url", text: affiliate.deeplinkurl }),
|
|
754
|
+
copyButton(text.copyLinkButton, affiliate.deeplinkurl)
|
|
755
|
+
]));
|
|
756
|
+
}
|
|
757
|
+
if (messageToShare) {
|
|
758
|
+
children.push(button(text.shareButton, "ia-raf-btn", () => {
|
|
759
|
+
void shareText(messageToShare).then((outcome) => {
|
|
760
|
+
if (closed) return;
|
|
761
|
+
if (outcome === "copied") status.textContent = text.copiedNotice;
|
|
762
|
+
else if (outcome === "failed") status.textContent = text.shareFailedNotice;
|
|
763
|
+
else status.textContent = "";
|
|
764
|
+
});
|
|
765
|
+
}));
|
|
766
|
+
}
|
|
767
|
+
if (stats) {
|
|
768
|
+
children.push(el("div", { className: "ia-raf-stats" }, [
|
|
769
|
+
el("div", { className: "ia-raf-stat" }, [
|
|
770
|
+
el("span", { className: "ia-raf-stat-value", text: String(stats.referralCount) }),
|
|
771
|
+
el("span", { className: "ia-raf-stat-label", text: text.referralsLabel })
|
|
772
|
+
]),
|
|
773
|
+
el("div", { className: "ia-raf-stat" }, [
|
|
774
|
+
el("span", { className: "ia-raf-stat-value", text: formatMoney(stats.totalEarned, stats.currency) }),
|
|
775
|
+
el("span", { className: "ia-raf-stat-label", text: text.earnedLabel })
|
|
776
|
+
])
|
|
777
|
+
]));
|
|
778
|
+
const premiumUntil = futureDateLabel(stats.premiumUntil);
|
|
779
|
+
if (premiumUntil) {
|
|
780
|
+
children.push(el("p", {
|
|
781
|
+
className: "ia-raf-premium",
|
|
782
|
+
text: fillPlaceholders(text.premiumUntil, { date: premiumUntil })
|
|
783
|
+
}));
|
|
784
|
+
}
|
|
785
|
+
if (stats.rewardCodes.length) {
|
|
786
|
+
const rewardsTitleId = `${titleId}-rewards`;
|
|
787
|
+
children.push(
|
|
788
|
+
el("h3", { className: "ia-raf-rewards-title", text: text.rewardsHeading, attrs: { id: rewardsTitleId } }),
|
|
789
|
+
el("ul", { className: "ia-raf-rewards", attrs: { "aria-labelledby": rewardsTitleId } }, stats.rewardCodes.map((reward2) => {
|
|
790
|
+
const redeemUrl = reward2.redeemUrl;
|
|
791
|
+
let redeem = null;
|
|
792
|
+
if (isHttpsUrl(redeemUrl)) {
|
|
793
|
+
redeem = button(text.redeemButton, "ia-raf-copy", () => {
|
|
794
|
+
window.open(redeemUrl, "_blank", "noopener,noreferrer");
|
|
795
|
+
});
|
|
796
|
+
redeem.setAttribute("aria-label", `${text.redeemButton} ${reward2.code}`);
|
|
797
|
+
}
|
|
798
|
+
return el("li", { className: "ia-raf-row" }, [
|
|
799
|
+
el("span", { className: "ia-raf-reward-code", text: reward2.code }),
|
|
800
|
+
redeem
|
|
801
|
+
]);
|
|
802
|
+
}))
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
if (isHttpsUrl(stats.dashboardUrl)) {
|
|
806
|
+
const dashboardUrl = stats.dashboardUrl;
|
|
807
|
+
children.push(button(text.dashboardLink, "ia-raf-btn ia-raf-btn-secondary", () => {
|
|
808
|
+
window.open(dashboardUrl, "_blank", "noopener,noreferrer");
|
|
809
|
+
}));
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
children.push(status);
|
|
813
|
+
render(children);
|
|
814
|
+
}
|
|
815
|
+
async function start() {
|
|
816
|
+
renderLoading();
|
|
817
|
+
const [config, loaded] = await Promise.all([
|
|
818
|
+
deps.loadConfig(),
|
|
819
|
+
deps.hasToken().then((has) => has ? deps.loadDetails() : { kind: "signedOut" })
|
|
820
|
+
]);
|
|
821
|
+
if (closed) return;
|
|
822
|
+
if (config) {
|
|
823
|
+
companyName = config.companyName;
|
|
824
|
+
if (!options.primaryColor && config.primaryColor) setColor(config.primaryColor);
|
|
825
|
+
if (!options.headline && config.headline) title.textContent = config.headline;
|
|
826
|
+
if (!options.rewardText && config.rewardText) {
|
|
827
|
+
reward.textContent = config.rewardText;
|
|
828
|
+
reward.hidden = false;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
if (loaded.kind === "ok") {
|
|
832
|
+
if (!accountSaved && (options.appUserId || options.playPurchaseToken)) {
|
|
833
|
+
accountSaved = true;
|
|
834
|
+
void deps.saveAccount();
|
|
835
|
+
}
|
|
836
|
+
renderEnrolled(loaded.details, loaded.details);
|
|
837
|
+
} else if (loaded.kind === "failed") {
|
|
838
|
+
renderMessage(messageFor("NETWORK_ERROR"), () => void start());
|
|
839
|
+
} else if (config && !config.enabled) {
|
|
840
|
+
renderMessage(messageFor("PROGRAM_DISABLED"));
|
|
841
|
+
} else {
|
|
842
|
+
renderEnrolForm();
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
const handle = { close };
|
|
846
|
+
openModal = {
|
|
847
|
+
handle,
|
|
848
|
+
focus: () => {
|
|
849
|
+
if (!dialog.contains(document.activeElement)) (focusables()[0] || closeButton).focus();
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
void start();
|
|
853
|
+
return handle;
|
|
854
|
+
}
|
|
855
|
+
|
|
76
856
|
// src/sdk/InsertAffiliate.ts
|
|
77
857
|
var InsertAffiliate = class {
|
|
78
858
|
static verboseLog(message) {
|
|
@@ -641,6 +1421,214 @@ var InsertAffiliate = class {
|
|
|
641
1421
|
this.verboseLog(`Error fetching/opening offer code: ${err}`);
|
|
642
1422
|
}
|
|
643
1423
|
}
|
|
1424
|
+
// ---------------------------------------------------------------------------
|
|
1425
|
+
// In-app referrals: make the app's own user an affiliate and read their stats
|
|
1426
|
+
// ---------------------------------------------------------------------------
|
|
1427
|
+
static async referralCompanyId() {
|
|
1428
|
+
try {
|
|
1429
|
+
return this.companyCode || await getValue("companyCode");
|
|
1430
|
+
} catch (e) {
|
|
1431
|
+
return this.companyCode;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* The device id in this browser's "{shortCode}-{deviceId}" identifier, so the
|
|
1436
|
+
* server can tell a referrer apart from the friends they refer.
|
|
1437
|
+
* Null when storage is unavailable.
|
|
1438
|
+
*/
|
|
1439
|
+
static async referralDeviceId() {
|
|
1440
|
+
try {
|
|
1441
|
+
return await this.getOrCreateUserID();
|
|
1442
|
+
} catch (e) {
|
|
1443
|
+
return null;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
static notInitializedResult() {
|
|
1447
|
+
this.verboseLog("Cannot use referrals: no company code available. Call initialize first.");
|
|
1448
|
+
return {
|
|
1449
|
+
status: "error",
|
|
1450
|
+
errorCode: "NOT_INITIALIZED",
|
|
1451
|
+
errorMessage: "The Insert Affiliate SDK is not initialized with a company code."
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* Makes the app's user an affiliate (a referrer) of this company.
|
|
1456
|
+
* A new email is created straight away and this device is connected.
|
|
1457
|
+
* An email that is already an affiliate is sent a 6-digit code instead:
|
|
1458
|
+
* the result is `verificationRequired`; finish with verifyAffiliateCode.
|
|
1459
|
+
* @param email The user's email (usually the app's logged-in user)
|
|
1460
|
+
* @param name The user's display name
|
|
1461
|
+
* @param options The user's own accounts (RevenueCat / Adapty app user id,
|
|
1462
|
+
* Google Play purchase token), used to grant their referral rewards
|
|
1463
|
+
*/
|
|
1464
|
+
static async createAffiliateForUser(email, name, options) {
|
|
1465
|
+
this.verboseLog("Creating affiliate for app user...");
|
|
1466
|
+
const companyId = await this.referralCompanyId();
|
|
1467
|
+
if (!companyId) return this.notInitializedResult();
|
|
1468
|
+
const result = await postEnrolment("enrol", companyId, __spreadValues({
|
|
1469
|
+
email: (email || "").trim(),
|
|
1470
|
+
name: (name || "").trim()
|
|
1471
|
+
}, await this.referrerAccountFields(options)), (message) => this.verboseLog(message));
|
|
1472
|
+
if (result.status !== "error") this.lastReferralDetails = null;
|
|
1473
|
+
return result;
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Finishes connecting this device with the 6-digit code emailed by
|
|
1477
|
+
* createAffiliateForUser. On success the device is connected.
|
|
1478
|
+
* @param email The same email passed to createAffiliateForUser
|
|
1479
|
+
* @param code The 6-digit code from the email
|
|
1480
|
+
* @param name Optional display name, used if the affiliate is created now
|
|
1481
|
+
* @param options The user's own accounts, as for createAffiliateForUser
|
|
1482
|
+
*/
|
|
1483
|
+
static async verifyAffiliateCode(email, code, name, options) {
|
|
1484
|
+
this.verboseLog("Verifying affiliate code...");
|
|
1485
|
+
const companyId = await this.referralCompanyId();
|
|
1486
|
+
if (!companyId) return this.notInitializedResult();
|
|
1487
|
+
const result = await postEnrolment("verify", companyId, __spreadValues({
|
|
1488
|
+
email: (email || "").trim(),
|
|
1489
|
+
code: normalizeVerificationCode(code),
|
|
1490
|
+
name: (name || "").trim()
|
|
1491
|
+
}, await this.referrerAccountFields(options)), (message) => this.verboseLog(message));
|
|
1492
|
+
if (result.status !== "error") this.lastReferralDetails = null;
|
|
1493
|
+
return result;
|
|
1494
|
+
}
|
|
1495
|
+
static async referrerAccountFields(options) {
|
|
1496
|
+
return {
|
|
1497
|
+
deviceId: await this.referralDeviceId() || void 0,
|
|
1498
|
+
appUserId: options && options.appUserId,
|
|
1499
|
+
playPurchaseToken: options && options.playPurchaseToken
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
/**
|
|
1503
|
+
* Saves the connected referrer's own accounts, for apps whose user
|
|
1504
|
+
* subscribes or logs in after joining. The server then grants any
|
|
1505
|
+
* rewards that were waiting for them.
|
|
1506
|
+
* @param options The RevenueCat / Adapty app user id and/or the Google Play purchase token
|
|
1507
|
+
* @returns True when saved; false when no referrer is connected on this device
|
|
1508
|
+
* or the request failed. A revoked connection is cleared.
|
|
1509
|
+
*/
|
|
1510
|
+
static async setReferrerAccount(options) {
|
|
1511
|
+
this.verboseLog("Saving referrer account...");
|
|
1512
|
+
const companyId = await this.referralCompanyId();
|
|
1513
|
+
const token = companyId ? readReferrerToken(companyId) : null;
|
|
1514
|
+
if (!companyId || !token) {
|
|
1515
|
+
this.verboseLog("Cannot save referrer account: no referrer connected on this device");
|
|
1516
|
+
return false;
|
|
1517
|
+
}
|
|
1518
|
+
const saved = await postReferrerIdentity(
|
|
1519
|
+
companyId,
|
|
1520
|
+
token,
|
|
1521
|
+
await this.referrerAccountFields(options),
|
|
1522
|
+
(message) => this.verboseLog(message)
|
|
1523
|
+
);
|
|
1524
|
+
if (!readReferrerToken(companyId)) this.lastReferralDetails = null;
|
|
1525
|
+
this.verboseLog(`Referrer account saved: ${saved}`);
|
|
1526
|
+
return saved;
|
|
1527
|
+
}
|
|
1528
|
+
static async loadMyAffiliateDetails() {
|
|
1529
|
+
const companyId = await this.referralCompanyId();
|
|
1530
|
+
const token = companyId ? readReferrerToken(companyId) : null;
|
|
1531
|
+
if (!companyId || !token) {
|
|
1532
|
+
this.verboseLog("No referrer connected on this device");
|
|
1533
|
+
this.lastReferralDetails = null;
|
|
1534
|
+
return { kind: "signedOut" };
|
|
1535
|
+
}
|
|
1536
|
+
const loaded = await fetchMyAffiliateDetails(companyId, token, (message) => this.verboseLog(message));
|
|
1537
|
+
if (loaded.kind === "ok") this.lastReferralDetails = loaded.details;
|
|
1538
|
+
if (loaded.kind === "signedOut") this.lastReferralDetails = null;
|
|
1539
|
+
return loaded;
|
|
1540
|
+
}
|
|
1541
|
+
/**
|
|
1542
|
+
* The connected user's affiliate details and referral stats.
|
|
1543
|
+
* Values are for display: grant anything valuable from your server.
|
|
1544
|
+
* @returns The details, or null when this device has no referrer connected
|
|
1545
|
+
* (or the request failed). A revoked connection is cleared.
|
|
1546
|
+
*/
|
|
1547
|
+
static async getMyAffiliateDetails() {
|
|
1548
|
+
this.verboseLog("Getting my affiliate details...");
|
|
1549
|
+
const loaded = await this.loadMyAffiliateDetails();
|
|
1550
|
+
return loaded.kind === "ok" ? loaded.details : null;
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* Whether this device has a referrer connected for this company.
|
|
1554
|
+
* Local check only, no network.
|
|
1555
|
+
*/
|
|
1556
|
+
static async isUserAnAffiliate() {
|
|
1557
|
+
const companyId = await this.referralCompanyId();
|
|
1558
|
+
return !!companyId && !!readReferrerToken(companyId);
|
|
1559
|
+
}
|
|
1560
|
+
/** Disconnects the referrer from this device (call on app logout). The affiliate account is kept. */
|
|
1561
|
+
static async signOutAffiliate() {
|
|
1562
|
+
this.verboseLog("Signing out referrer on this device");
|
|
1563
|
+
const companyId = await this.referralCompanyId();
|
|
1564
|
+
if (companyId) clearReferrerToken(companyId);
|
|
1565
|
+
this.lastReferralDetails = null;
|
|
1566
|
+
}
|
|
1567
|
+
/** The company's in-app referral settings (program on/off, copy, colour), or null if unavailable. */
|
|
1568
|
+
static async getReferralProgramConfig() {
|
|
1569
|
+
this.verboseLog("Getting referral program config...");
|
|
1570
|
+
const companyId = await this.referralCompanyId();
|
|
1571
|
+
if (!companyId) {
|
|
1572
|
+
this.verboseLog("Cannot get referral config: no company code available");
|
|
1573
|
+
return null;
|
|
1574
|
+
}
|
|
1575
|
+
const config = await fetchReferralProgramConfig(companyId, (message) => this.verboseLog(message));
|
|
1576
|
+
if (config) this.lastReferralConfig = config;
|
|
1577
|
+
return config;
|
|
1578
|
+
}
|
|
1579
|
+
/**
|
|
1580
|
+
* Shares the connected user's referral link with the system share sheet,
|
|
1581
|
+
* or copies it to the clipboard where sharing is unavailable.
|
|
1582
|
+
* Call from a click handler. Browsers only allow sharing and copying
|
|
1583
|
+
* shortly after the tap, so this never waits on the network: it uses the
|
|
1584
|
+
* details from the last getMyAffiliateDetails call and the app name from
|
|
1585
|
+
* the last getReferralProgramConfig call. Load both before the user taps.
|
|
1586
|
+
* Without loaded details it returns 'failed' and starts loading them, so a
|
|
1587
|
+
* later tap can share.
|
|
1588
|
+
* @param message Optional message. May use {link} and {code} placeholders.
|
|
1589
|
+
*/
|
|
1590
|
+
static async shareReferralLink(message) {
|
|
1591
|
+
this.verboseLog("Sharing referral link...");
|
|
1592
|
+
const details = this.lastReferralDetails;
|
|
1593
|
+
const config = this.lastReferralConfig;
|
|
1594
|
+
if (!config) void this.getReferralProgramConfig();
|
|
1595
|
+
if (!details) {
|
|
1596
|
+
this.verboseLog("Cannot share: referrer details not loaded. Call getMyAffiliateDetails before the tap");
|
|
1597
|
+
void this.getMyAffiliateDetails();
|
|
1598
|
+
return "failed";
|
|
1599
|
+
}
|
|
1600
|
+
const text = buildReferralShareText(details, config ? config.companyName : "", message);
|
|
1601
|
+
const outcome = await shareText(text);
|
|
1602
|
+
this.verboseLog(`Share outcome: ${outcome}`);
|
|
1603
|
+
return outcome;
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Presents the drop-in "Refer a friend" modal. Handles enrolment, the
|
|
1607
|
+
* email code step, sharing and stats. Browser only. Only one modal is
|
|
1608
|
+
* shown at a time: calling this while it is open focuses the open one.
|
|
1609
|
+
* @returns A handle whose close() dismisses the modal
|
|
1610
|
+
*/
|
|
1611
|
+
static showReferAFriend(options = {}) {
|
|
1612
|
+
if (typeof document === "undefined" || !document.body) {
|
|
1613
|
+
console.error("[Insert Affiliate] showReferAFriend needs a browser document.");
|
|
1614
|
+
return { close: () => void 0 };
|
|
1615
|
+
}
|
|
1616
|
+
if (!this.companyCode) {
|
|
1617
|
+
this.verboseLog("showReferAFriend called before initialize; using the stored company code if any");
|
|
1618
|
+
}
|
|
1619
|
+
const account = {
|
|
1620
|
+
appUserId: options.appUserId,
|
|
1621
|
+
playPurchaseToken: options.playPurchaseToken
|
|
1622
|
+
};
|
|
1623
|
+
return presentReferAFriend(options, {
|
|
1624
|
+
hasToken: () => this.isUserAnAffiliate(),
|
|
1625
|
+
loadConfig: () => this.getReferralProgramConfig(),
|
|
1626
|
+
loadDetails: () => this.loadMyAffiliateDetails(),
|
|
1627
|
+
enrol: (email, name) => this.createAffiliateForUser(email, name, account),
|
|
1628
|
+
verify: (email, code, name) => this.verifyAffiliateCode(email, code, name, account),
|
|
1629
|
+
saveAccount: () => this.setReferrerAccount(account)
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
644
1632
|
static async getOrCreateUserID() {
|
|
645
1633
|
this.verboseLog("Getting or creating user ID...");
|
|
646
1634
|
let id = await getValue("userId");
|
|
@@ -685,6 +1673,11 @@ InsertAffiliate.affiliateAttributionActiveTime = null;
|
|
|
685
1673
|
// in milliseconds
|
|
686
1674
|
InsertAffiliate.preventAffiliateTransfer = false;
|
|
687
1675
|
InsertAffiliate.offerCode = null;
|
|
1676
|
+
// Last referral details and config seen. shareReferralLink uses only these,
|
|
1677
|
+
// so the share sheet opens without a network round trip (browsers require
|
|
1678
|
+
// the share to follow the user's tap closely).
|
|
1679
|
+
InsertAffiliate.lastReferralDetails = null;
|
|
1680
|
+
InsertAffiliate.lastReferralConfig = null;
|
|
688
1681
|
// Annotate the CommonJS export names for ESM import in node:
|
|
689
1682
|
0 && (module.exports = {
|
|
690
1683
|
InsertAffiliate
|