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
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// src/referrals/referrerTokenStore.ts
|
|
2
|
+
// The device token that lets this browser read its own referrer's stats.
|
|
3
|
+
// One token per company ID in localStorage. Storage can be unavailable
|
|
4
|
+
// (private mode, blocked site data, a full quota), so every access is
|
|
5
|
+
// guarded; when saving throws, the token lives in memory for this page load
|
|
6
|
+
// only and reads fall back to it. The token is never logged.
|
|
7
|
+
|
|
8
|
+
const KEY_PREFIX = 'insertAffiliateReferrerToken_';
|
|
9
|
+
const memoryTokens: Record<string, string> = {};
|
|
10
|
+
|
|
11
|
+
const keyFor = (companyId: string): string => `${KEY_PREFIX}${companyId}`;
|
|
12
|
+
|
|
13
|
+
export const readReferrerToken = (companyId: string): string | null => {
|
|
14
|
+
let stored: string | null = null;
|
|
15
|
+
try {
|
|
16
|
+
stored = localStorage.getItem(keyFor(companyId));
|
|
17
|
+
} catch {
|
|
18
|
+
// Storage unreadable; use the in-memory copy.
|
|
19
|
+
}
|
|
20
|
+
return stored || memoryTokens[companyId] || null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Returns false when only the in-memory copy could be kept. */
|
|
24
|
+
export const saveReferrerToken = (companyId: string, token: string): boolean => {
|
|
25
|
+
try {
|
|
26
|
+
localStorage.setItem(keyFor(companyId), token);
|
|
27
|
+
delete memoryTokens[companyId];
|
|
28
|
+
return true;
|
|
29
|
+
} catch {
|
|
30
|
+
memoryTokens[companyId] = token;
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const clearReferrerToken = (companyId: string): void => {
|
|
36
|
+
delete memoryTokens[companyId];
|
|
37
|
+
try {
|
|
38
|
+
localStorage.removeItem(keyFor(companyId));
|
|
39
|
+
} catch {
|
|
40
|
+
// Nothing stored that we can reach.
|
|
41
|
+
}
|
|
42
|
+
};
|
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
// src/sdk/InsertAffiliate.ts
|
|
2
2
|
import { getValue, saveValue } from '../utils/asyncStorage';
|
|
3
3
|
import { generateShortDeviceID, generateUUID } from '../utils/helpers';
|
|
4
|
+
import {
|
|
5
|
+
buildReferralShareText,
|
|
6
|
+
fetchMyAffiliateDetails,
|
|
7
|
+
fetchReferralProgramConfig,
|
|
8
|
+
MyDetailsFetch,
|
|
9
|
+
normalizeVerificationCode,
|
|
10
|
+
postEnrolment,
|
|
11
|
+
postReferrerIdentity,
|
|
12
|
+
shareText,
|
|
13
|
+
} from '../referrals/referralApi';
|
|
14
|
+
import { clearReferrerToken, readReferrerToken } from '../referrals/referrerTokenStore';
|
|
15
|
+
import { presentReferAFriend } from '../referrals/referAFriendModal';
|
|
16
|
+
import type {
|
|
17
|
+
AffiliateEnrolmentResult,
|
|
18
|
+
MyAffiliateDetails,
|
|
19
|
+
ReferAFriendHandle,
|
|
20
|
+
ReferAFriendOptions,
|
|
21
|
+
ReferralProgramConfig,
|
|
22
|
+
ReferralShareOutcome,
|
|
23
|
+
ReferrerAccountOptions,
|
|
24
|
+
} from '../referrals/referralTypes';
|
|
4
25
|
|
|
5
26
|
interface IapticAndroidReceipt {
|
|
6
27
|
orderId: string;
|
|
@@ -44,6 +65,11 @@ export class InsertAffiliate {
|
|
|
44
65
|
private static affiliateAttributionActiveTime: number | null = null; // in milliseconds
|
|
45
66
|
private static preventAffiliateTransfer: boolean = false;
|
|
46
67
|
private static offerCode: string | null = null;
|
|
68
|
+
// Last referral details and config seen. shareReferralLink uses only these,
|
|
69
|
+
// so the share sheet opens without a network round trip (browsers require
|
|
70
|
+
// the share to follow the user's tap closely).
|
|
71
|
+
private static lastReferralDetails: MyAffiliateDetails | null = null;
|
|
72
|
+
private static lastReferralConfig: ReferralProgramConfig | null = null;
|
|
47
73
|
|
|
48
74
|
private static verboseLog(message: string): void {
|
|
49
75
|
if (this.verboseLogging) {
|
|
@@ -804,6 +830,243 @@ export class InsertAffiliate {
|
|
|
804
830
|
}
|
|
805
831
|
}
|
|
806
832
|
|
|
833
|
+
// ---------------------------------------------------------------------------
|
|
834
|
+
// In-app referrals: make the app's own user an affiliate and read their stats
|
|
835
|
+
// ---------------------------------------------------------------------------
|
|
836
|
+
|
|
837
|
+
private static async referralCompanyId(): Promise<string | null> {
|
|
838
|
+
try {
|
|
839
|
+
return this.companyCode || await getValue('companyCode');
|
|
840
|
+
} catch {
|
|
841
|
+
return this.companyCode;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* The device id in this browser's "{shortCode}-{deviceId}" identifier, so the
|
|
847
|
+
* server can tell a referrer apart from the friends they refer.
|
|
848
|
+
* Null when storage is unavailable.
|
|
849
|
+
*/
|
|
850
|
+
private static async referralDeviceId(): Promise<string | null> {
|
|
851
|
+
try {
|
|
852
|
+
return await this.getOrCreateUserID();
|
|
853
|
+
} catch {
|
|
854
|
+
return null;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
private static notInitializedResult(): AffiliateEnrolmentResult {
|
|
859
|
+
this.verboseLog('Cannot use referrals: no company code available. Call initialize first.');
|
|
860
|
+
return {
|
|
861
|
+
status: 'error',
|
|
862
|
+
errorCode: 'NOT_INITIALIZED',
|
|
863
|
+
errorMessage: 'The Insert Affiliate SDK is not initialized with a company code.',
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Makes the app's user an affiliate (a referrer) of this company.
|
|
869
|
+
* A new email is created straight away and this device is connected.
|
|
870
|
+
* An email that is already an affiliate is sent a 6-digit code instead:
|
|
871
|
+
* the result is `verificationRequired`; finish with verifyAffiliateCode.
|
|
872
|
+
* @param email The user's email (usually the app's logged-in user)
|
|
873
|
+
* @param name The user's display name
|
|
874
|
+
* @param options The user's own accounts (RevenueCat / Adapty app user id,
|
|
875
|
+
* Google Play purchase token), used to grant their referral rewards
|
|
876
|
+
*/
|
|
877
|
+
static async createAffiliateForUser(
|
|
878
|
+
email: string,
|
|
879
|
+
name: string,
|
|
880
|
+
options?: ReferrerAccountOptions
|
|
881
|
+
): Promise<AffiliateEnrolmentResult> {
|
|
882
|
+
this.verboseLog('Creating affiliate for app user...');
|
|
883
|
+
const companyId = await this.referralCompanyId();
|
|
884
|
+
if (!companyId) return this.notInitializedResult();
|
|
885
|
+
|
|
886
|
+
const result = await postEnrolment('enrol', companyId, {
|
|
887
|
+
email: (email || '').trim(),
|
|
888
|
+
name: (name || '').trim(),
|
|
889
|
+
...await this.referrerAccountFields(options),
|
|
890
|
+
}, (message) => this.verboseLog(message));
|
|
891
|
+
if (result.status !== 'error') this.lastReferralDetails = null;
|
|
892
|
+
return result;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Finishes connecting this device with the 6-digit code emailed by
|
|
897
|
+
* createAffiliateForUser. On success the device is connected.
|
|
898
|
+
* @param email The same email passed to createAffiliateForUser
|
|
899
|
+
* @param code The 6-digit code from the email
|
|
900
|
+
* @param name Optional display name, used if the affiliate is created now
|
|
901
|
+
* @param options The user's own accounts, as for createAffiliateForUser
|
|
902
|
+
*/
|
|
903
|
+
static async verifyAffiliateCode(
|
|
904
|
+
email: string,
|
|
905
|
+
code: string,
|
|
906
|
+
name?: string,
|
|
907
|
+
options?: ReferrerAccountOptions
|
|
908
|
+
): Promise<AffiliateEnrolmentResult> {
|
|
909
|
+
this.verboseLog('Verifying affiliate code...');
|
|
910
|
+
const companyId = await this.referralCompanyId();
|
|
911
|
+
if (!companyId) return this.notInitializedResult();
|
|
912
|
+
|
|
913
|
+
const result = await postEnrolment('verify', companyId, {
|
|
914
|
+
email: (email || '').trim(),
|
|
915
|
+
code: normalizeVerificationCode(code),
|
|
916
|
+
name: (name || '').trim(),
|
|
917
|
+
...await this.referrerAccountFields(options),
|
|
918
|
+
}, (message) => this.verboseLog(message));
|
|
919
|
+
if (result.status !== 'error') this.lastReferralDetails = null;
|
|
920
|
+
return result;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
private static async referrerAccountFields(options?: ReferrerAccountOptions): Promise<Record<string, string | undefined>> {
|
|
924
|
+
return {
|
|
925
|
+
deviceId: (await this.referralDeviceId()) || undefined,
|
|
926
|
+
appUserId: options && options.appUserId,
|
|
927
|
+
playPurchaseToken: options && options.playPurchaseToken,
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Saves the connected referrer's own accounts, for apps whose user
|
|
933
|
+
* subscribes or logs in after joining. The server then grants any
|
|
934
|
+
* rewards that were waiting for them.
|
|
935
|
+
* @param options The RevenueCat / Adapty app user id and/or the Google Play purchase token
|
|
936
|
+
* @returns True when saved; false when no referrer is connected on this device
|
|
937
|
+
* or the request failed. A revoked connection is cleared.
|
|
938
|
+
*/
|
|
939
|
+
static async setReferrerAccount(options: ReferrerAccountOptions): Promise<boolean> {
|
|
940
|
+
this.verboseLog('Saving referrer account...');
|
|
941
|
+
const companyId = await this.referralCompanyId();
|
|
942
|
+
const token = companyId ? readReferrerToken(companyId) : null;
|
|
943
|
+
if (!companyId || !token) {
|
|
944
|
+
this.verboseLog('Cannot save referrer account: no referrer connected on this device');
|
|
945
|
+
return false;
|
|
946
|
+
}
|
|
947
|
+
const saved = await postReferrerIdentity(
|
|
948
|
+
companyId,
|
|
949
|
+
token,
|
|
950
|
+
await this.referrerAccountFields(options),
|
|
951
|
+
(message) => this.verboseLog(message)
|
|
952
|
+
);
|
|
953
|
+
if (!readReferrerToken(companyId)) this.lastReferralDetails = null;
|
|
954
|
+
this.verboseLog(`Referrer account saved: ${saved}`);
|
|
955
|
+
return saved;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
private static async loadMyAffiliateDetails(): Promise<MyDetailsFetch> {
|
|
959
|
+
const companyId = await this.referralCompanyId();
|
|
960
|
+
const token = companyId ? readReferrerToken(companyId) : null;
|
|
961
|
+
if (!companyId || !token) {
|
|
962
|
+
this.verboseLog('No referrer connected on this device');
|
|
963
|
+
this.lastReferralDetails = null;
|
|
964
|
+
return { kind: 'signedOut' };
|
|
965
|
+
}
|
|
966
|
+
const loaded = await fetchMyAffiliateDetails(companyId, token, (message) => this.verboseLog(message));
|
|
967
|
+
if (loaded.kind === 'ok') this.lastReferralDetails = loaded.details;
|
|
968
|
+
if (loaded.kind === 'signedOut') this.lastReferralDetails = null;
|
|
969
|
+
return loaded;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* The connected user's affiliate details and referral stats.
|
|
974
|
+
* Values are for display: grant anything valuable from your server.
|
|
975
|
+
* @returns The details, or null when this device has no referrer connected
|
|
976
|
+
* (or the request failed). A revoked connection is cleared.
|
|
977
|
+
*/
|
|
978
|
+
static async getMyAffiliateDetails(): Promise<MyAffiliateDetails | null> {
|
|
979
|
+
this.verboseLog('Getting my affiliate details...');
|
|
980
|
+
const loaded = await this.loadMyAffiliateDetails();
|
|
981
|
+
return loaded.kind === 'ok' ? loaded.details : null;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* Whether this device has a referrer connected for this company.
|
|
986
|
+
* Local check only, no network.
|
|
987
|
+
*/
|
|
988
|
+
static async isUserAnAffiliate(): Promise<boolean> {
|
|
989
|
+
const companyId = await this.referralCompanyId();
|
|
990
|
+
return !!companyId && !!readReferrerToken(companyId);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/** Disconnects the referrer from this device (call on app logout). The affiliate account is kept. */
|
|
994
|
+
static async signOutAffiliate(): Promise<void> {
|
|
995
|
+
this.verboseLog('Signing out referrer on this device');
|
|
996
|
+
const companyId = await this.referralCompanyId();
|
|
997
|
+
if (companyId) clearReferrerToken(companyId);
|
|
998
|
+
this.lastReferralDetails = null;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/** The company's in-app referral settings (program on/off, copy, colour), or null if unavailable. */
|
|
1002
|
+
static async getReferralProgramConfig(): Promise<ReferralProgramConfig | null> {
|
|
1003
|
+
this.verboseLog('Getting referral program config...');
|
|
1004
|
+
const companyId = await this.referralCompanyId();
|
|
1005
|
+
if (!companyId) {
|
|
1006
|
+
this.verboseLog('Cannot get referral config: no company code available');
|
|
1007
|
+
return null;
|
|
1008
|
+
}
|
|
1009
|
+
const config = await fetchReferralProgramConfig(companyId, (message) => this.verboseLog(message));
|
|
1010
|
+
if (config) this.lastReferralConfig = config;
|
|
1011
|
+
return config;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* Shares the connected user's referral link with the system share sheet,
|
|
1016
|
+
* or copies it to the clipboard where sharing is unavailable.
|
|
1017
|
+
* Call from a click handler. Browsers only allow sharing and copying
|
|
1018
|
+
* shortly after the tap, so this never waits on the network: it uses the
|
|
1019
|
+
* details from the last getMyAffiliateDetails call and the app name from
|
|
1020
|
+
* the last getReferralProgramConfig call. Load both before the user taps.
|
|
1021
|
+
* Without loaded details it returns 'failed' and starts loading them, so a
|
|
1022
|
+
* later tap can share.
|
|
1023
|
+
* @param message Optional message. May use {link} and {code} placeholders.
|
|
1024
|
+
*/
|
|
1025
|
+
static async shareReferralLink(message?: string): Promise<ReferralShareOutcome> {
|
|
1026
|
+
this.verboseLog('Sharing referral link...');
|
|
1027
|
+
const details = this.lastReferralDetails;
|
|
1028
|
+
const config = this.lastReferralConfig;
|
|
1029
|
+
if (!config) void this.getReferralProgramConfig();
|
|
1030
|
+
if (!details) {
|
|
1031
|
+
this.verboseLog('Cannot share: referrer details not loaded. Call getMyAffiliateDetails before the tap');
|
|
1032
|
+
void this.getMyAffiliateDetails();
|
|
1033
|
+
return 'failed';
|
|
1034
|
+
}
|
|
1035
|
+
const text = buildReferralShareText(details, config ? config.companyName : '', message);
|
|
1036
|
+
const outcome = await shareText(text);
|
|
1037
|
+
this.verboseLog(`Share outcome: ${outcome}`);
|
|
1038
|
+
return outcome;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
|
|
1042
|
+
/**
|
|
1043
|
+
* Presents the drop-in "Refer a friend" modal. Handles enrolment, the
|
|
1044
|
+
* email code step, sharing and stats. Browser only. Only one modal is
|
|
1045
|
+
* shown at a time: calling this while it is open focuses the open one.
|
|
1046
|
+
* @returns A handle whose close() dismisses the modal
|
|
1047
|
+
*/
|
|
1048
|
+
static showReferAFriend(options: ReferAFriendOptions = {}): ReferAFriendHandle {
|
|
1049
|
+
if (typeof document === 'undefined' || !document.body) {
|
|
1050
|
+
console.error('[Insert Affiliate] showReferAFriend needs a browser document.');
|
|
1051
|
+
return { close: () => undefined };
|
|
1052
|
+
}
|
|
1053
|
+
if (!this.companyCode) {
|
|
1054
|
+
this.verboseLog('showReferAFriend called before initialize; using the stored company code if any');
|
|
1055
|
+
}
|
|
1056
|
+
const account: ReferrerAccountOptions = {
|
|
1057
|
+
appUserId: options.appUserId,
|
|
1058
|
+
playPurchaseToken: options.playPurchaseToken,
|
|
1059
|
+
};
|
|
1060
|
+
return presentReferAFriend(options, {
|
|
1061
|
+
hasToken: () => this.isUserAnAffiliate(),
|
|
1062
|
+
loadConfig: () => this.getReferralProgramConfig(),
|
|
1063
|
+
loadDetails: () => this.loadMyAffiliateDetails(),
|
|
1064
|
+
enrol: (email, name) => this.createAffiliateForUser(email, name, account),
|
|
1065
|
+
verify: (email, code, name) => this.verifyAffiliateCode(email, code, name, account),
|
|
1066
|
+
saveAccount: () => this.setReferrerAccount(account),
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
|
|
807
1070
|
private static async getOrCreateUserID(): Promise<string> {
|
|
808
1071
|
this.verboseLog('Getting or creating user ID...');
|
|
809
1072
|
let id = await getValue('userId');
|