@bringweb3/chrome-extension-kit 1.0.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.
@@ -0,0 +1,8 @@
1
+ # Changesets
2
+
3
+ Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4
+ with multi-package repos, or single-package repos to help you version and publish your code. You can
5
+ find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6
+
7
+ We have a quick list of common questions to get you started engaging with this project in
8
+ [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
@@ -0,0 +1,11 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@changesets/config@3.0.2/schema.json",
3
+ "changelog": "@changesets/cli/changelog",
4
+ "commit": false,
5
+ "fixed": [],
6
+ "linked": [],
7
+ "access": "restricted",
8
+ "baseBranch": "main",
9
+ "updateInternalDependencies": "patch",
10
+ "ignore": []
11
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # @bringweb3/chrome-extension-kit
2
+
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - fb4c76e: Initial release of @bringweb3/chrome-extension-kit - Chrome Extension Kit from Bringweb3
package/README.md ADDED
@@ -0,0 +1,166 @@
1
+ <img src="https://avatars.githubusercontent.com/u/122225882?s=96&v=4"/>
2
+ <br><br>
3
+ <h1 align="center">@bringweb3/chrome-extension-kit</h1>
4
+
5
+ ## Table of content
6
+ - [Table of content](#table-of-content)
7
+ - [Description](#description)
8
+ - [Prerequisites](#prerequisites)
9
+ - [Installing](#installing)
10
+ - [Package](#package)
11
+ - [Manifest](#manifest)
12
+ - [Importing](#importing)
13
+ - [Example](#example)
14
+ - [background.js](#backgroundjs)
15
+ - [contentScript.js](#contentscriptjs)
16
+
17
+ ## Description
18
+ This integration kit is designed to enhance existing Chrome extensions by adding functionality that enables automatic crypto cashback on online purchases.
19
+
20
+ This kit consists of a set of JavaScript files that crypto outlets can integrate into their crypto wallet extensions. This integration facilitates a seamless addition of cashback features, leveraging cryptocurrency transactions in the context of online shopping.
21
+
22
+ When a user visits supported online retailer websites, the Crypto Cashback system determines eligibility for cashback offers based on the user's location and the website's relevance.
23
+
24
+ ## Prerequisites
25
+
26
+ - Node.js >= 14
27
+ - Chrome extension manifest >= V3 with required permissions
28
+ - Obtain an identifier key from Bringweb3
29
+ - Provide a specific logo for the specific outlet
30
+
31
+ ## Installing
32
+
33
+ ### Package
34
+ Using npm:
35
+ ```bash
36
+ $ npm install @bringweb3/chrome-extension-kit
37
+ ```
38
+
39
+ Using yarn:
40
+
41
+ ```bash
42
+ $ yarn add @bringweb3/chrome-extension-kit
43
+ ```
44
+
45
+ Using pnpm:
46
+
47
+ ```bash
48
+ $ pnpm add @bringweb3/chrome-extension-kit
49
+ ```
50
+
51
+ ### Manifest
52
+
53
+ Include this configuration inside your `manifest.json` file:
54
+
55
+ ```json
56
+ "permissions": [
57
+ "storage",
58
+ "tabs",
59
+ "alarms"
60
+ ],
61
+ "content_scripts": [
62
+ {
63
+ "matches": [
64
+ "<all_urls>"
65
+ ],
66
+ "js": [
67
+ "contentScript.js" // The name of the file importing the bringContentScriptInit
68
+ ]
69
+ }
70
+ ],
71
+ "host_permissions": [
72
+ "https://*.bringweb3.io/*"
73
+ ]
74
+ ```
75
+
76
+ Once the package is installed, you can import the library using `import` or `require` approach:
77
+
78
+
79
+ ## Importing
80
+
81
+ ```js
82
+ import { bringInitBackground } from '@bringweb3/chrome-extension-kit';
83
+ ```
84
+
85
+ ## Example
86
+
87
+ ### background.js
88
+
89
+ ```js
90
+
91
+ import { bringInitBackground } from '@bringweb3/chrome-extension-kit';
92
+
93
+ bringInitBackground({
94
+ identifier: process.env.PLATFORM_IDENTIFIER, // The identifier key you obtained from Bringweb3
95
+ apiEndpoint: 'sandbox', // 'sandbox' || 'prod'
96
+ cashbackPagePath: '/wallet/cashback' // The relative path to your Cashback Dashboard if you have one inside your extension
97
+ })
98
+ ```
99
+
100
+ ### contentScript.js
101
+
102
+ ```js
103
+ import { bringInitContentScript } from "@bringweb3/chrome-extension-kit";
104
+
105
+ bringInitContentScript({
106
+ iframeEndpoint: process.env.IFRAME_ENDPOINT,
107
+ getWalletAddress: async () => await new Promise(resolve => setTimeout(() => resolve('<USER_WALLET_ADDRESS>'), 200)),// Async function that returns the current user's wallet address
108
+ promptLogin: () => {...}, // Function that prompts a UI element asking the user to login
109
+ walletAddressListeners: ["customEvent:addressChanged"], // A list of custom events that dispatched when the user's wallet address had changed
110
+ customTheme: { // All optional
111
+ fontUrl: 'https://fonts.googleapis.com/css2?family=Matemasie&display=swap',
112
+ fontFamily: "'Matemasie', system-ui",
113
+
114
+ popupBg: "#192E34",
115
+ popupShadow: "",
116
+
117
+ primaryBtnBg: "linear-gradient(135deg, #5DEB5A 0%, #FDFC47 100%)",
118
+ primaryBtnColor: "#041417",
119
+ primaryBtnBorderColor: "transparent",
120
+ primaryBtnBorderW: "0",
121
+ primaryBtnRadius: "8px",
122
+
123
+ secondaryBtnBg: "transparent",
124
+ secondaryBtnColor: "white",
125
+ secondaryBtnBorderColor: "rgba(149, 176, 178, 0.50)",
126
+ secondaryBtnBorderW: "2px",
127
+ secondaryBtnRadius: "8px",
128
+
129
+ markdownBg: "#07131766",
130
+ markdownColor: "#DADCE5",
131
+ markdownBorderW: "0",
132
+ markdownRadius: "4px",
133
+ markdownBorderColor: "black",
134
+ markdownScrollbarColor: "#DADCE5",
135
+
136
+ walletColor: "white",
137
+ walletBg: "#33535B",
138
+ walletBorderColor: "white",
139
+ walletBorderW: "0",
140
+ walletRadius: "4px",
141
+
142
+ detailsBg: "#33535B",
143
+ detailsColor: "white",
144
+ detailsRadius: "8px",
145
+ detailsBorderW: "0",
146
+ detailsBorderColor: "transparent",
147
+ detailsCashbackColor: "linear-gradient(135deg, #5DEB5A 0%, #FDFC47 100%)",
148
+
149
+ overlayBg: "#192E34E6",
150
+ overlayColor: "#DADCE5",
151
+
152
+ optoutBg: "#192E34",
153
+ optoutColor: "white",
154
+ optoutRadius: "56px",
155
+
156
+ closeColor: "#B9BBBF",
157
+
158
+ tokenBg: "transparent",
159
+ tokenColor: "#DADCE5",
160
+
161
+ notificationBtnColor: "#041417",
162
+ notificationBtnBg: "linear-gradient(135deg, #5DEB5A 0%, #FDFC47 100%)",
163
+ notificationBtnRadius: "8px"
164
+ }
165
+ });
166
+ ```
@@ -0,0 +1,301 @@
1
+ import fetchDomains from "./utils/api/fetchDomains.js"
2
+ import validateDomain from "./utils/api/validateDomain.js"
3
+ import checkEvents from "./utils/api/checkEvents.js"
4
+ import { ApiEndpoint } from "./utils/apiEndpoint.js"
5
+
6
+ import { UPDATE_CACHE_ALARM_NAME } from './utils/constants.js'
7
+ import storage from "./utils/storage.js"
8
+
9
+ const quietTime = 30 * 60 * 1000
10
+
11
+ const getWalletAddress = async (tabId?: number): Promise<WalletAddress> => {
12
+ let walletAddress: WalletAddress = await storage.get('walletAddress')
13
+
14
+ try {
15
+ if (!tabId) {
16
+ const tabs = await chrome.tabs.query({ active: true, currentWindow: true })
17
+ if (!tabs || !tabs[0] || !tabs[0].id) return walletAddress;
18
+ tabId = tabs[0].id
19
+ }
20
+
21
+ const res = await sendMessage(tabId, { action: 'GET_WALLET_ADDRESS' });
22
+
23
+ if (res?.walletAddress && walletAddress !== res?.walletAddress) {
24
+ walletAddress = res?.walletAddress
25
+ await storage.set('walletAddress', walletAddress as string)
26
+ }
27
+ } catch (error) {
28
+ // console.log("Can't update wallet address");
29
+ }
30
+
31
+ return walletAddress
32
+ }
33
+
34
+ const calcDelay = (timestamp: number) => {
35
+ const now = Date.now()
36
+ return (timestamp - now) / 1000 / 60 // milliseconds to minutes
37
+ }
38
+
39
+ const updateCache = async (apiKey: string) => {
40
+ const res = await fetchDomains(apiKey)
41
+
42
+ storage.set('relevantDomains', res.relevantDomains)
43
+
44
+ const { nextUpdateTimestamp } = res
45
+
46
+ const delay = calcDelay(nextUpdateTimestamp)
47
+
48
+ chrome.alarms.create(UPDATE_CACHE_ALARM_NAME, {
49
+ delayInMinutes: delay
50
+ })
51
+ }
52
+
53
+ const checkNotifications = async (apiKey: string, tabId: number, cashbackUrl: string | undefined, isAfterActivation?: boolean) => {
54
+ const falseReturn = { showNotification: false, token: '' };
55
+
56
+ const nextNotificationCheck = await storage.get('notificationCheck');
57
+
58
+ if (nextNotificationCheck?.check && Date.now() < nextNotificationCheck.check) return falseReturn;
59
+
60
+ const walletAddress = await getWalletAddress(tabId)
61
+
62
+ if (!walletAddress) return falseReturn;
63
+
64
+ const res = await checkEvents({ apiKey, walletAddress, cashbackUrl });
65
+ const notifications = {
66
+ check: isAfterActivation ? res.nextRequestTimestampActivated : res.nextRequestTimestampRegular,
67
+ nextRequestTimestampActivated: res.nextRequestTimestampActivated,
68
+ nextRequestTimestampRegular: res.nextRequestTimestampRegular
69
+ }
70
+ storage.set('notificationCheck', notifications);
71
+
72
+ return {
73
+ showNotification: res.showNotification as boolean,
74
+ token: res.token as string
75
+ };
76
+ }
77
+
78
+ const getDomain = (url: string) => {
79
+ return url.replace(/^(https?:\/\/)?(www\.)?/, '');
80
+ }
81
+
82
+ const getRelevantDomain = async (url: string | undefined) => {
83
+ const relevantDomains = await storage.get('relevantDomains')
84
+ if (!url || !relevantDomains || !relevantDomains.length) return ''
85
+ const domain = getDomain(url)
86
+ for (const relevantDomain of relevantDomains) {
87
+ if (domain.startsWith(relevantDomain)) {
88
+
89
+ const quietDomains = await storage.get('quietDomains')
90
+ if (quietDomains && quietDomains[relevantDomain] && Date.now() < quietDomains[relevantDomain]) {
91
+ return ''
92
+ }
93
+ return relevantDomain
94
+ }
95
+ }
96
+ return ''
97
+ }
98
+
99
+ const addQuietDomain = async (domain: string, time?: number) => {
100
+ if (!time) time = quietTime
101
+
102
+ let quietDomains = await storage.get('quietDomains')
103
+
104
+ if (typeof quietDomains === 'object') {
105
+ quietDomains[domain] = Date.now() + time
106
+ } else {
107
+ quietDomains = { [domain]: Date.now() + quietTime }
108
+ }
109
+ storage.set('quietDomains', quietDomains)
110
+ }
111
+
112
+ const getCashbackUrl = (cashbackUrl: string | undefined): string | undefined => {
113
+ return cashbackUrl ? chrome.runtime.getURL(cashbackUrl) : undefined;
114
+ }
115
+
116
+ interface Message {
117
+ action: 'INJECT' | 'GET_WALLET_ADDRESS'
118
+ domain?: string
119
+ token?: string
120
+ page?: string
121
+ }
122
+
123
+ const sendMessage = async (tabId: number, message: Message) => {
124
+ const maxRetries = 5;
125
+ const baseDelay = 1000; // 1 second
126
+
127
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
128
+ try {
129
+ // Check if tab still exists
130
+ const tabInfo = await chrome.tabs.get(tabId);
131
+ if (chrome.runtime.lastError) {
132
+ // console.warn("Tab no longer exists:", chrome.runtime.lastError);
133
+ return;
134
+ }
135
+
136
+ const res = await chrome.tabs.sendMessage(tabId, message);
137
+ // console.log("Message sent successfully");
138
+ return res;
139
+ } catch (error) {
140
+ // console.warn(`Attempt ${attempt + 1} failed:`, error);
141
+ if (attempt < maxRetries - 1) {
142
+ await new Promise(resolve => setTimeout(resolve, baseDelay * Math.pow(2, attempt)));
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ const showNotification = async (identifier: string, tabId: number, cashbackPagePath: string | undefined): Promise<void> => {
149
+ const notification = await checkNotifications(identifier, tabId, getCashbackUrl(cashbackPagePath))
150
+
151
+ if (!notification.showNotification) return;
152
+ await sendMessage(tabId, {
153
+ action: 'INJECT',
154
+ token: notification.token,
155
+ page: 'notification',
156
+ })
157
+ }
158
+
159
+ interface UrlDict {
160
+ [key: string]: string
161
+ }
162
+
163
+ const urlsDict: UrlDict = {}
164
+
165
+ interface Configuration {
166
+ identifier: string
167
+ apiEndpoint: string
168
+ cashbackPagePath?: string
169
+ }
170
+ /**
171
+ * Initializes the background script for the Bring extension.
172
+ *
173
+ * @async
174
+ * @function bringInitBackground
175
+ * @param {Object} configuration - The configuration object.
176
+ * @param {string} configuration.identifier - The identifier for the extension.
177
+ * @param {string} configuration.apiEndpoint - The API endpoint ('prod' or 'sandbox').
178
+ * @param {string} [configuration.cashbackPagePath] - Optional path to the cashback page.
179
+ * @throws {Error} Throws an error if identifier or apiEndpoint is missing, or if apiEndpoint is invalid.
180
+ * @returns {Promise<void>}
181
+ *
182
+ * @description
183
+ * This function sets up the background processes for the Bring extension. It initializes
184
+ * the API endpoint, sets up listeners for alarms, runtime messages, and tab updates.
185
+ * It handles various actions such as opting out, closing notifications, injecting content
186
+ * based on URL changes, and managing quiet domains.
187
+ *
188
+ * The function performs the following tasks:
189
+ * - Validates and sets the API endpoint
190
+ * - Updates the cache
191
+ * - Sets up listeners for alarms to update cache periodically
192
+ * - Handles runtime messages for opting out and closing notifications
193
+ * - Monitors tab updates to inject content or show notifications based on URL changes
194
+ * - Validates domains and manages quiet domains
195
+ *
196
+ * @example
197
+ * bringInitBackground({
198
+ * identifier: 'my-extension-id',
199
+ * apiEndpoint: 'sandbox',
200
+ * cashbackPagePath: '/cashback.html'
201
+ * });
202
+ */
203
+ const bringInitBackground = async ({ identifier, apiEndpoint, cashbackPagePath }: Configuration) => {
204
+ if (!identifier || !apiEndpoint) throw new Error('Missing configuration')
205
+ if (!['prod', 'sandbox'].includes(apiEndpoint)) throw new Error('unknown apiEndpoint')
206
+
207
+ ApiEndpoint.getInstance().setApiEndpoint(apiEndpoint)
208
+
209
+
210
+ updateCache(identifier)
211
+
212
+ chrome.alarms.onAlarm.addListener(async (alarm) => {
213
+ const { name } = alarm
214
+
215
+ switch (name) {
216
+ case UPDATE_CACHE_ALARM_NAME:
217
+ updateCache(identifier)
218
+ break;
219
+ default:
220
+ console.error('alarm with no use case:', name);
221
+ break;
222
+ }
223
+ })
224
+
225
+ chrome.runtime.onMessage.addListener(async (request, sender) => {
226
+ const { action, time } = request
227
+
228
+ switch (action) {
229
+ case 'ACTIVATE':
230
+ const notificationCheck = await storage.get('notificationCheck')
231
+ if (!notificationCheck.check || !notificationCheck.nextRequestTimestampActivated) break;
232
+ notificationCheck.check = notificationCheck.nextRequestTimestampActivated;
233
+ storage.set('notificationCheck', notificationCheck)
234
+ break;
235
+ case 'OPT_OUT':
236
+ storage.set('optOut', Date.now() + time)
237
+ break;
238
+ case 'CLOSE':
239
+ const domain = await getRelevantDomain(sender.tab?.url || sender.origin)
240
+ if (!domain) break;
241
+ addQuietDomain(domain, time)
242
+ break;
243
+ default:
244
+ console.warn(`Bring unknown action: ${action}`);
245
+ break;
246
+ }
247
+ })
248
+
249
+ chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
250
+ const optOut = await storage.get('optOut');
251
+
252
+ if (optOut && optOut > Date.now()) {
253
+ await showNotification(identifier, tabId, cashbackPagePath);
254
+ return;
255
+ }
256
+
257
+ if (!tab.url) return;
258
+
259
+ const urlObj = new URL(tab.url)
260
+ const url = `${urlObj.hostname.replace('www.', '')}${urlObj.pathname}`
261
+
262
+ const previousUrl = urlsDict[tabId];
263
+
264
+ if (changeInfo.status !== 'complete' || url === previousUrl) return;
265
+
266
+ urlsDict[tabId] = url
267
+
268
+ const match = await getRelevantDomain(tab.url);
269
+
270
+ if (!match || !match.length) {
271
+ await showNotification(identifier, tabId, cashbackPagePath)
272
+ return;
273
+ };
274
+
275
+ const address = await getWalletAddress(tabId);
276
+
277
+ const { token, isValid } = await validateDomain({
278
+ apiKey: identifier,
279
+ query: {
280
+ domain: match,
281
+ url: tab.url,
282
+ address
283
+ }
284
+ });
285
+
286
+ if (!isValid) {
287
+ addQuietDomain(match);
288
+ return;
289
+ }
290
+
291
+ sendMessage(tabId, {
292
+ action: 'INJECT',
293
+ token,
294
+ domain: url
295
+ });
296
+ })
297
+
298
+ chrome.tabs.onRemoved.addListener(tabId => delete urlsDict[tabId])
299
+ }
300
+
301
+ export default bringInitBackground
@@ -0,0 +1,101 @@
1
+ import injectIFrame from "./utils/contentScript/injectIFrame.js";
2
+ import handleIframeMessages from "./utils/contentScript/handleIframeMessages.js";
3
+ import startListenersForWalletAddress from "./utils/contentScript/startLIstenersForWalletAddress.js";
4
+
5
+ let iframeEl: IFrame = null
6
+ let isIframeOpen = false
7
+
8
+ interface Configuration {
9
+ iframeEndpoint: string
10
+ getWalletAddress: () => Promise<WalletAddress>
11
+ promptLogin: () => Promise<WalletAddress>
12
+ walletAddressListeners: string[]
13
+ customTheme?: Style
14
+ }
15
+
16
+ /**
17
+ * Initializes the content script for the Bring extension.
18
+ *
19
+ * @async
20
+ * @function bringInitContentScript
21
+ * @param {Object} configuration - The configuration object.
22
+ * @param {Function} configuration.getWalletAddress - A function that returns a Promise resolving to the wallet address.
23
+ * @param {Function} configuration.promptLogin - A function to prompt the user to login.
24
+ * @param {string[]} configuration.walletAddressListeners - An array of strings representing wallet address listeners.
25
+ * @param {Object} [configuration.customTheme] - Optional custom theme settings.
26
+ * @param {string} configuration.iframeEndpoint - The endpoint URL for the iframe.
27
+ * @throws {Error} Throws an error if any required configuration is missing.
28
+ * @returns {Promise<void>}
29
+ *
30
+ * @description
31
+ * This function sets up event listeners for wallet address changes, iframe messages,
32
+ * and Chrome runtime messages. It handles actions such as getting the wallet address
33
+ * and injecting iframes based on received messages.
34
+ *
35
+ * @example
36
+ * bringInitContentScript({
37
+ * getWalletAddress: async () => '0x1234...',
38
+ * promptLogin: () => { ... },
39
+ * walletAddressListeners: ["listener1", "listener2"],
40
+ * iframeEndpoint: 'https://example.com/iframe'
41
+ * });
42
+ */
43
+ const bringInitContentScript = async ({
44
+ getWalletAddress,
45
+ promptLogin,
46
+ walletAddressListeners,
47
+ customTheme,
48
+ iframeEndpoint
49
+ }: Configuration) => {
50
+ if (!getWalletAddress || !promptLogin || !walletAddressListeners?.length || !iframeEndpoint) throw new Error('Missing configuration')
51
+
52
+ startListenersForWalletAddress({
53
+ walletAddressListeners,
54
+ getWalletAddress,
55
+ iframeEl
56
+ })
57
+
58
+ window.addEventListener('message', (e) => handleIframeMessages({
59
+ event: e,
60
+ iframeEl,
61
+ promptLogin
62
+ }))
63
+
64
+ // Listen for message
65
+ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
66
+
67
+ const { action } = request
68
+
69
+ switch (action) {
70
+
71
+ case 'GET_WALLET_ADDRESS':
72
+ getWalletAddress()
73
+ .then(walletAddress => sendResponse({ status: 'success', walletAddress }))
74
+ .catch(err => sendResponse({ status: 'success', walletAddress: undefined }))
75
+ return true
76
+
77
+ case 'INJECT':
78
+ if (isIframeOpen) {
79
+ return
80
+ }
81
+ const { token, page } = request;
82
+ // console.log(`injecting to: ${request.domain}`);
83
+
84
+ iframeEl = injectIFrame({
85
+ query: { token },
86
+ iframeSrc: page === 'notification' ?
87
+ `${iframeEndpoint}notification` : iframeEndpoint,
88
+ theme: customTheme
89
+ });
90
+ isIframeOpen = true
91
+ sendResponse({ status: 'success' });
92
+ return true
93
+
94
+ default:
95
+ console.error(`Unknown action: ${action}`);
96
+ break;
97
+ }
98
+ });
99
+ }
100
+
101
+ export default bringInitContentScript;
package/config.ts ADDED
@@ -0,0 +1 @@
1
+ export const API_URL = 'https://sandbox-api.bringweb3.io/v1/extension'
@@ -0,0 +1,77 @@
1
+ interface Configuration$1 {
2
+ iframeEndpoint: string;
3
+ getWalletAddress: () => Promise<WalletAddress>;
4
+ promptLogin: () => Promise<WalletAddress>;
5
+ walletAddressListeners: string[];
6
+ customTheme?: Style;
7
+ }
8
+ /**
9
+ * Initializes the content script for the Bring extension.
10
+ *
11
+ * @async
12
+ * @function bringInitContentScript
13
+ * @param {Object} configuration - The configuration object.
14
+ * @param {Function} configuration.getWalletAddress - A function that returns a Promise resolving to the wallet address.
15
+ * @param {Function} configuration.promptLogin - A function to prompt the user to login.
16
+ * @param {string[]} configuration.walletAddressListeners - An array of strings representing wallet address listeners.
17
+ * @param {Object} [configuration.customTheme] - Optional custom theme settings.
18
+ * @param {string} configuration.iframeEndpoint - The endpoint URL for the iframe.
19
+ * @throws {Error} Throws an error if any required configuration is missing.
20
+ * @returns {Promise<void>}
21
+ *
22
+ * @description
23
+ * This function sets up event listeners for wallet address changes, iframe messages,
24
+ * and Chrome runtime messages. It handles actions such as getting the wallet address
25
+ * and injecting iframes based on received messages.
26
+ *
27
+ * @example
28
+ * bringInitContentScript({
29
+ * getWalletAddress: async () => '0x1234...',
30
+ * promptLogin: () => { ... },
31
+ * walletAddressListeners: ["listener1", "listener2"],
32
+ * iframeEndpoint: 'https://example.com/iframe'
33
+ * });
34
+ */
35
+ declare const bringInitContentScript: ({ getWalletAddress, promptLogin, walletAddressListeners, customTheme, iframeEndpoint }: Configuration$1) => Promise<void>;
36
+
37
+ interface Configuration {
38
+ identifier: string;
39
+ apiEndpoint: string;
40
+ cashbackPagePath?: string;
41
+ }
42
+ /**
43
+ * Initializes the background script for the Bring extension.
44
+ *
45
+ * @async
46
+ * @function bringInitBackground
47
+ * @param {Object} configuration - The configuration object.
48
+ * @param {string} configuration.identifier - The identifier for the extension.
49
+ * @param {string} configuration.apiEndpoint - The API endpoint ('prod' or 'sandbox').
50
+ * @param {string} [configuration.cashbackPagePath] - Optional path to the cashback page.
51
+ * @throws {Error} Throws an error if identifier or apiEndpoint is missing, or if apiEndpoint is invalid.
52
+ * @returns {Promise<void>}
53
+ *
54
+ * @description
55
+ * This function sets up the background processes for the Bring extension. It initializes
56
+ * the API endpoint, sets up listeners for alarms, runtime messages, and tab updates.
57
+ * It handles various actions such as opting out, closing notifications, injecting content
58
+ * based on URL changes, and managing quiet domains.
59
+ *
60
+ * The function performs the following tasks:
61
+ * - Validates and sets the API endpoint
62
+ * - Updates the cache
63
+ * - Sets up listeners for alarms to update cache periodically
64
+ * - Handles runtime messages for opting out and closing notifications
65
+ * - Monitors tab updates to inject content or show notifications based on URL changes
66
+ * - Validates domains and manages quiet domains
67
+ *
68
+ * @example
69
+ * bringInitBackground({
70
+ * identifier: 'my-extension-id',
71
+ * apiEndpoint: 'sandbox',
72
+ * cashbackPagePath: '/cashback.html'
73
+ * });
74
+ */
75
+ declare const bringInitBackground: ({ identifier, apiEndpoint, cashbackPagePath }: Configuration) => Promise<void>;
76
+
77
+ export { bringInitBackground, bringInitContentScript };
@@ -0,0 +1,77 @@
1
+ interface Configuration$1 {
2
+ iframeEndpoint: string;
3
+ getWalletAddress: () => Promise<WalletAddress>;
4
+ promptLogin: () => Promise<WalletAddress>;
5
+ walletAddressListeners: string[];
6
+ customTheme?: Style;
7
+ }
8
+ /**
9
+ * Initializes the content script for the Bring extension.
10
+ *
11
+ * @async
12
+ * @function bringInitContentScript
13
+ * @param {Object} configuration - The configuration object.
14
+ * @param {Function} configuration.getWalletAddress - A function that returns a Promise resolving to the wallet address.
15
+ * @param {Function} configuration.promptLogin - A function to prompt the user to login.
16
+ * @param {string[]} configuration.walletAddressListeners - An array of strings representing wallet address listeners.
17
+ * @param {Object} [configuration.customTheme] - Optional custom theme settings.
18
+ * @param {string} configuration.iframeEndpoint - The endpoint URL for the iframe.
19
+ * @throws {Error} Throws an error if any required configuration is missing.
20
+ * @returns {Promise<void>}
21
+ *
22
+ * @description
23
+ * This function sets up event listeners for wallet address changes, iframe messages,
24
+ * and Chrome runtime messages. It handles actions such as getting the wallet address
25
+ * and injecting iframes based on received messages.
26
+ *
27
+ * @example
28
+ * bringInitContentScript({
29
+ * getWalletAddress: async () => '0x1234...',
30
+ * promptLogin: () => { ... },
31
+ * walletAddressListeners: ["listener1", "listener2"],
32
+ * iframeEndpoint: 'https://example.com/iframe'
33
+ * });
34
+ */
35
+ declare const bringInitContentScript: ({ getWalletAddress, promptLogin, walletAddressListeners, customTheme, iframeEndpoint }: Configuration$1) => Promise<void>;
36
+
37
+ interface Configuration {
38
+ identifier: string;
39
+ apiEndpoint: string;
40
+ cashbackPagePath?: string;
41
+ }
42
+ /**
43
+ * Initializes the background script for the Bring extension.
44
+ *
45
+ * @async
46
+ * @function bringInitBackground
47
+ * @param {Object} configuration - The configuration object.
48
+ * @param {string} configuration.identifier - The identifier for the extension.
49
+ * @param {string} configuration.apiEndpoint - The API endpoint ('prod' or 'sandbox').
50
+ * @param {string} [configuration.cashbackPagePath] - Optional path to the cashback page.
51
+ * @throws {Error} Throws an error if identifier or apiEndpoint is missing, or if apiEndpoint is invalid.
52
+ * @returns {Promise<void>}
53
+ *
54
+ * @description
55
+ * This function sets up the background processes for the Bring extension. It initializes
56
+ * the API endpoint, sets up listeners for alarms, runtime messages, and tab updates.
57
+ * It handles various actions such as opting out, closing notifications, injecting content
58
+ * based on URL changes, and managing quiet domains.
59
+ *
60
+ * The function performs the following tasks:
61
+ * - Validates and sets the API endpoint
62
+ * - Updates the cache
63
+ * - Sets up listeners for alarms to update cache periodically
64
+ * - Handles runtime messages for opting out and closing notifications
65
+ * - Monitors tab updates to inject content or show notifications based on URL changes
66
+ * - Validates domains and manages quiet domains
67
+ *
68
+ * @example
69
+ * bringInitBackground({
70
+ * identifier: 'my-extension-id',
71
+ * apiEndpoint: 'sandbox',
72
+ * cashbackPagePath: '/cashback.html'
73
+ * });
74
+ */
75
+ declare const bringInitBackground: ({ identifier, apiEndpoint, cashbackPagePath }: Configuration) => Promise<void>;
76
+
77
+ export { bringInitBackground, bringInitContentScript };
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ "use strict";var h=Object.defineProperty;var W=Object.getOwnPropertyDescriptor;var j=Object.getOwnPropertyNames;var F=Object.prototype.hasOwnProperty;var K=(e,t)=>{for(var r in t)h(e,r,{get:t[r],enumerable:!0})},V=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of j(t))!F.call(e,s)&&s!==r&&h(e,s,{get:()=>t[s],enumerable:!(n=W(t,s))||n.enumerable});return e};var Q=e=>V(h({},"__esModule",{value:!0}),e);var le={};K(le,{bringInitBackground:()=>q,bringInitContentScript:()=>v});module.exports=Q(le);var B=e=>{let t=new URLSearchParams,{query:r,prefix:n}=e;return Object.entries(r).forEach(([s,o])=>{o&&(n&&(s=`${n}_${s}`),s==="url"?t.append(s,encodeURIComponent(o)):t.append(s,o))}),t.toString()},f=B;var G=({query:e,theme:t,iframeSrc:r})=>{let n=chrome.runtime.id,s=f({query:{...e,extensionId:n}}),o=t?`&${f({query:t,prefix:"t"})}`:"",i=document.createElement("iframe");return i.id=`bringweb3-iframe:${n}`,i.src=`${r}?${s}${o}`,i.setAttribute("sandbox","allow-popups allow-scripts allow-same-origin allow-top-navigation-by-user-activation"),i.style.position="fixed",i.scrolling="no",i.style.overflow="hidden",i.style.width="1px",i.style.height="1px",i.style.right="8px",i.style.borderRadius="10px",i.style.border="none",i.style.cssText+="z-index: 99999999999999 !important;",t?.popupShadow&&(i.style.boxShadow=t.popupShadow),document.documentElement.appendChild(i),i},b=G;var J=(e,t)=>{!e||!t||!Object.keys(t).length||Object.entries(t).forEach(([r,n])=>{r in e.style&&(e.style[r]=n)})},T=J;var H=e=>{if(!e||!e.length)return;let t=document.createElement("style");document.head.appendChild(t);let r=t.sheet;r?e.forEach(({name:n,rules:s})=>{r.insertRule(`@keyframes ${n} { ${s} }`,r.cssRules.length)}):console.error("Failed to create stylesheet")},k=H;var l={OPEN:"OPEN",CLOSE:"CLOSE",ACTIVATE:"ACTIVATE",PROMPT_LOGIN:"PROMPT_LOGIN",OPT_OUT:"OPT_OUT",ADD_KEYFRAMES:"ADD_KEYFRAMES"},Y=[l.ACTIVATE],z=({event:e,iframeEl:t,promptLogin:r})=>{let{data:n}=e,{from:s,action:o,style:i,keyFrames:a,time:c,extensionId:p}=n;if(s==="bringweb3"&&!(p!==chrome.runtime.id&&!Y.includes(o)))switch(o){case l.OPEN:T(t,i);break;case l.CLOSE:t&&t.parentNode?.removeChild(t),c&&chrome.runtime.sendMessage({action:o,time:c});break;case l.PROMPT_LOGIN:r();break;case l.ACTIVATE:chrome.runtime.sendMessage({action:o});break;case l.OPT_OUT:chrome.runtime.sendMessage({action:o,time:c});break;case l.ADD_KEYFRAMES:k(a);break;default:break}},x=z;var X=({walletAddressListeners:e,getWalletAddress:t,iframeEl:r})=>{for(let n=0;n<e.length;n++){let s=e[n];s&&window.addEventListener(s,async o=>{if(!r&&(r=document.querySelector(`#bringweb3-iframe:${chrome.runtime.id}`),!r)||!r.contentWindow)return;let i=await t();r.contentWindow.postMessage({action:"WALLET_ADDRESS_UPDATE",walletAddress:i},"*")})}},D=X;var w=null,C=!1,Z=async({getWalletAddress:e,promptLogin:t,walletAddressListeners:r,customTheme:n,iframeEndpoint:s})=>{if(!e||!t||!r?.length||!s)throw new Error("Missing configuration");D({walletAddressListeners:r,getWalletAddress:e,iframeEl:w}),window.addEventListener("message",o=>x({event:o,iframeEl:w,promptLogin:t})),chrome.runtime.onMessage.addListener((o,i,a)=>{let{action:c}=o;switch(c){case"GET_WALLET_ADDRESS":return e().then(g=>a({status:"success",walletAddress:g})).catch(g=>a({status:"success",walletAddress:void 0})),!0;case"INJECT":if(C)return;let{token:p,page:m}=o;return w=b({query:{token:p},iframeSrc:m==="notification"?`${s}notification`:s,theme:n}),C=!0,a({status:"success"}),!0;default:console.error(`Unknown action: ${c}`);break}})},v=Z;var u=class e{static instance=null;apiEndpoint="";constructor(){}static getInstance(){return e.instance||(e.instance=new e),e.instance}setApiEndpoint(t){this.apiEndpoint=t==="prod"?"https://api.bringweb3.io/v1/extension":"https://sandbox-api.bringweb3.io/v1/extension"}getApiEndpoint(){if(!this.apiEndpoint)throw new Error("API endpoint not set. Call setApiEndpoint first.");return this.apiEndpoint}};var ee=async e=>{let t=u.getInstance().getApiEndpoint();return await(await fetch(`${t}/domains?country=us`,{headers:{"x-api-key":e}})).json()},P=ee;var te=async({apiKey:e,query:t})=>{let r=u.getInstance().getApiEndpoint(),n=f({query:{...t,country:"us"}});return await(await fetch(`${r}/token?${n.toString()}`,{headers:{"x-api-key":e}})).json()},S=te;var ne=async({apiKey:e,walletAddress:t,cashbackUrl:r})=>{let n=u.getInstance().getApiEndpoint(),s={walletAddress:t,test:!0};return r&&(s.cashbackUrl=r),await(await fetch(`${n}/check-events`,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":e},body:JSON.stringify(s)})).json()},R=ne;var y="updateCache";var re=async(e,t)=>{chrome.storage.local.set({[e]:t})},se=async e=>(await chrome.storage.local.get(e))[e],ie=async e=>{chrome.storage.local.remove(e)},d={set:re,get:se,remove:ie};var O=30*60*1e3,_=async e=>{let t=await d.get("walletAddress");try{if(!e){let n=await chrome.tabs.query({active:!0,currentWindow:!0});if(!n||!n[0]||!n[0].id)return t;e=n[0].id}let r=await E(e,{action:"GET_WALLET_ADDRESS"});r?.walletAddress&&t!==r?.walletAddress&&(t=r?.walletAddress,await d.set("walletAddress",t))}catch{}return t},oe=e=>{let t=Date.now();return(e-t)/1e3/60},L=async e=>{let t=await P(e);d.set("relevantDomains",t.relevantDomains);let{nextUpdateTimestamp:r}=t,n=oe(r);chrome.alarms.create(y,{delayInMinutes:n})},ae=async(e,t,r,n)=>{let s={showNotification:!1,token:""},o=await d.get("notificationCheck");if(o?.check&&Date.now()<o.check)return s;let i=await _(t);if(!i)return s;let a=await R({apiKey:e,walletAddress:i,cashbackUrl:r}),c={check:n?a.nextRequestTimestampActivated:a.nextRequestTimestampRegular,nextRequestTimestampActivated:a.nextRequestTimestampActivated,nextRequestTimestampRegular:a.nextRequestTimestampRegular};return d.set("notificationCheck",c),{showNotification:a.showNotification,token:a.token}},ce=e=>e.replace(/^(https?:\/\/)?(www\.)?/,""),I=async e=>{let t=await d.get("relevantDomains");if(!e||!t||!t.length)return"";let r=ce(e);for(let n of t)if(r.startsWith(n)){let s=await d.get("quietDomains");return s&&s[n]&&Date.now()<s[n]?"":n}return""},M=async(e,t)=>{t||(t=O);let r=await d.get("quietDomains");typeof r=="object"?r[e]=Date.now()+t:r={[e]:Date.now()+O},d.set("quietDomains",r)},de=e=>e?chrome.runtime.getURL(e):void 0,E=async(e,t)=>{for(let s=0;s<5;s++)try{let o=await chrome.tabs.get(e);return chrome.runtime.lastError?void 0:await chrome.tabs.sendMessage(e,t)}catch{s<4&&await new Promise(i=>setTimeout(i,1e3*Math.pow(2,s)))}},N=async(e,t,r)=>{let n=await ae(e,t,de(r));n.showNotification&&await E(t,{action:"INJECT",token:n.token,page:"notification"})},A={},ue=async({identifier:e,apiEndpoint:t,cashbackPagePath:r})=>{if(!e||!t)throw new Error("Missing configuration");if(!["prod","sandbox"].includes(t))throw new Error("unknown apiEndpoint");u.getInstance().setApiEndpoint(t),L(e),chrome.alarms.onAlarm.addListener(async n=>{let{name:s}=n;switch(s){case y:L(e);break;default:console.error("alarm with no use case:",s);break}}),chrome.runtime.onMessage.addListener(async(n,s)=>{let{action:o,time:i}=n;switch(o){case"ACTIVATE":let a=await d.get("notificationCheck");if(!a.check||!a.nextRequestTimestampActivated)break;a.check=a.nextRequestTimestampActivated,d.set("notificationCheck",a);break;case"OPT_OUT":d.set("optOut",Date.now()+i);break;case"CLOSE":let c=await I(s.tab?.url||s.origin);if(!c)break;M(c,i);break;default:console.warn(`Bring unknown action: ${o}`);break}}),chrome.tabs.onUpdated.addListener(async(n,s,o)=>{let i=await d.get("optOut");if(i&&i>Date.now()){await N(e,n,r);return}if(!o.url)return;let a=new URL(o.url),c=`${a.hostname.replace("www.","")}${a.pathname}`,p=A[n];if(s.status!=="complete"||c===p)return;A[n]=c;let m=await I(o.url);if(!m||!m.length){await N(e,n,r);return}let g=await _(n),{token:U,isValid:$}=await S({apiKey:e,query:{domain:m,url:o.url,address:g}});if(!$){M(m);return}E(n,{action:"INJECT",token:U,domain:c})}),chrome.tabs.onRemoved.addListener(n=>delete A[n])},q=ue;0&&(module.exports={bringInitBackground,bringInitContentScript});
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ var q=e=>{let t=new URLSearchParams,{query:r,prefix:n}=e;return Object.entries(r).forEach(([s,o])=>{o&&(n&&(s=`${n}_${s}`),s==="url"?t.append(s,encodeURIComponent(o)):t.append(s,o))}),t.toString()},f=q;var U=({query:e,theme:t,iframeSrc:r})=>{let n=chrome.runtime.id,s=f({query:{...e,extensionId:n}}),o=t?`&${f({query:t,prefix:"t"})}`:"",i=document.createElement("iframe");return i.id=`bringweb3-iframe:${n}`,i.src=`${r}?${s}${o}`,i.setAttribute("sandbox","allow-popups allow-scripts allow-same-origin allow-top-navigation-by-user-activation"),i.style.position="fixed",i.scrolling="no",i.style.overflow="hidden",i.style.width="1px",i.style.height="1px",i.style.right="8px",i.style.borderRadius="10px",i.style.border="none",i.style.cssText+="z-index: 99999999999999 !important;",t?.popupShadow&&(i.style.boxShadow=t.popupShadow),document.documentElement.appendChild(i),i},E=U;var $=(e,t)=>{!e||!t||!Object.keys(t).length||Object.entries(t).forEach(([r,n])=>{r in e.style&&(e.style[r]=n)})},b=$;var W=e=>{if(!e||!e.length)return;let t=document.createElement("style");document.head.appendChild(t);let r=t.sheet;r?e.forEach(({name:n,rules:s})=>{r.insertRule(`@keyframes ${n} { ${s} }`,r.cssRules.length)}):console.error("Failed to create stylesheet")},T=W;var l={OPEN:"OPEN",CLOSE:"CLOSE",ACTIVATE:"ACTIVATE",PROMPT_LOGIN:"PROMPT_LOGIN",OPT_OUT:"OPT_OUT",ADD_KEYFRAMES:"ADD_KEYFRAMES"},j=[l.ACTIVATE],F=({event:e,iframeEl:t,promptLogin:r})=>{let{data:n}=e,{from:s,action:o,style:i,keyFrames:a,time:c,extensionId:p}=n;if(s==="bringweb3"&&!(p!==chrome.runtime.id&&!j.includes(o)))switch(o){case l.OPEN:b(t,i);break;case l.CLOSE:t&&t.parentNode?.removeChild(t),c&&chrome.runtime.sendMessage({action:o,time:c});break;case l.PROMPT_LOGIN:r();break;case l.ACTIVATE:chrome.runtime.sendMessage({action:o});break;case l.OPT_OUT:chrome.runtime.sendMessage({action:o,time:c});break;case l.ADD_KEYFRAMES:T(a);break;default:break}},k=F;var K=({walletAddressListeners:e,getWalletAddress:t,iframeEl:r})=>{for(let n=0;n<e.length;n++){let s=e[n];s&&window.addEventListener(s,async o=>{if(!r&&(r=document.querySelector(`#bringweb3-iframe:${chrome.runtime.id}`),!r)||!r.contentWindow)return;let i=await t();r.contentWindow.postMessage({action:"WALLET_ADDRESS_UPDATE",walletAddress:i},"*")})}},x=K;var h=null,D=!1,V=async({getWalletAddress:e,promptLogin:t,walletAddressListeners:r,customTheme:n,iframeEndpoint:s})=>{if(!e||!t||!r?.length||!s)throw new Error("Missing configuration");x({walletAddressListeners:r,getWalletAddress:e,iframeEl:h}),window.addEventListener("message",o=>k({event:o,iframeEl:h,promptLogin:t})),chrome.runtime.onMessage.addListener((o,i,a)=>{let{action:c}=o;switch(c){case"GET_WALLET_ADDRESS":return e().then(g=>a({status:"success",walletAddress:g})).catch(g=>a({status:"success",walletAddress:void 0})),!0;case"INJECT":if(D)return;let{token:p,page:m}=o;return h=E({query:{token:p},iframeSrc:m==="notification"?`${s}notification`:s,theme:n}),D=!0,a({status:"success"}),!0;default:console.error(`Unknown action: ${c}`);break}})},Q=V;var u=class e{static instance=null;apiEndpoint="";constructor(){}static getInstance(){return e.instance||(e.instance=new e),e.instance}setApiEndpoint(t){this.apiEndpoint=t==="prod"?"https://api.bringweb3.io/v1/extension":"https://sandbox-api.bringweb3.io/v1/extension"}getApiEndpoint(){if(!this.apiEndpoint)throw new Error("API endpoint not set. Call setApiEndpoint first.");return this.apiEndpoint}};var B=async e=>{let t=u.getInstance().getApiEndpoint();return await(await fetch(`${t}/domains?country=us`,{headers:{"x-api-key":e}})).json()},C=B;var G=async({apiKey:e,query:t})=>{let r=u.getInstance().getApiEndpoint(),n=f({query:{...t,country:"us"}});return await(await fetch(`${r}/token?${n.toString()}`,{headers:{"x-api-key":e}})).json()},v=G;var J=async({apiKey:e,walletAddress:t,cashbackUrl:r})=>{let n=u.getInstance().getApiEndpoint(),s={walletAddress:t,test:!0};return r&&(s.cashbackUrl=r),await(await fetch(`${n}/check-events`,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":e},body:JSON.stringify(s)})).json()},P=J;var w="updateCache";var H=async(e,t)=>{chrome.storage.local.set({[e]:t})},Y=async e=>(await chrome.storage.local.get(e))[e],z=async e=>{chrome.storage.local.remove(e)},d={set:H,get:Y,remove:z};var S=30*60*1e3,M=async e=>{let t=await d.get("walletAddress");try{if(!e){let n=await chrome.tabs.query({active:!0,currentWindow:!0});if(!n||!n[0]||!n[0].id)return t;e=n[0].id}let r=await A(e,{action:"GET_WALLET_ADDRESS"});r?.walletAddress&&t!==r?.walletAddress&&(t=r?.walletAddress,await d.set("walletAddress",t))}catch{}return t},X=e=>{let t=Date.now();return(e-t)/1e3/60},R=async e=>{let t=await C(e);d.set("relevantDomains",t.relevantDomains);let{nextUpdateTimestamp:r}=t,n=X(r);chrome.alarms.create(w,{delayInMinutes:n})},Z=async(e,t,r,n)=>{let s={showNotification:!1,token:""},o=await d.get("notificationCheck");if(o?.check&&Date.now()<o.check)return s;let i=await M(t);if(!i)return s;let a=await P({apiKey:e,walletAddress:i,cashbackUrl:r}),c={check:n?a.nextRequestTimestampActivated:a.nextRequestTimestampRegular,nextRequestTimestampActivated:a.nextRequestTimestampActivated,nextRequestTimestampRegular:a.nextRequestTimestampRegular};return d.set("notificationCheck",c),{showNotification:a.showNotification,token:a.token}},ee=e=>e.replace(/^(https?:\/\/)?(www\.)?/,""),O=async e=>{let t=await d.get("relevantDomains");if(!e||!t||!t.length)return"";let r=ee(e);for(let n of t)if(r.startsWith(n)){let s=await d.get("quietDomains");return s&&s[n]&&Date.now()<s[n]?"":n}return""},L=async(e,t)=>{t||(t=S);let r=await d.get("quietDomains");typeof r=="object"?r[e]=Date.now()+t:r={[e]:Date.now()+S},d.set("quietDomains",r)},te=e=>e?chrome.runtime.getURL(e):void 0,A=async(e,t)=>{for(let s=0;s<5;s++)try{let o=await chrome.tabs.get(e);return chrome.runtime.lastError?void 0:await chrome.tabs.sendMessage(e,t)}catch{s<4&&await new Promise(i=>setTimeout(i,1e3*Math.pow(2,s)))}},I=async(e,t,r)=>{let n=await Z(e,t,te(r));n.showNotification&&await A(t,{action:"INJECT",token:n.token,page:"notification"})},y={},ne=async({identifier:e,apiEndpoint:t,cashbackPagePath:r})=>{if(!e||!t)throw new Error("Missing configuration");if(!["prod","sandbox"].includes(t))throw new Error("unknown apiEndpoint");u.getInstance().setApiEndpoint(t),R(e),chrome.alarms.onAlarm.addListener(async n=>{let{name:s}=n;switch(s){case w:R(e);break;default:console.error("alarm with no use case:",s);break}}),chrome.runtime.onMessage.addListener(async(n,s)=>{let{action:o,time:i}=n;switch(o){case"ACTIVATE":let a=await d.get("notificationCheck");if(!a.check||!a.nextRequestTimestampActivated)break;a.check=a.nextRequestTimestampActivated,d.set("notificationCheck",a);break;case"OPT_OUT":d.set("optOut",Date.now()+i);break;case"CLOSE":let c=await O(s.tab?.url||s.origin);if(!c)break;L(c,i);break;default:console.warn(`Bring unknown action: ${o}`);break}}),chrome.tabs.onUpdated.addListener(async(n,s,o)=>{let i=await d.get("optOut");if(i&&i>Date.now()){await I(e,n,r);return}if(!o.url)return;let a=new URL(o.url),c=`${a.hostname.replace("www.","")}${a.pathname}`,p=y[n];if(s.status!=="complete"||c===p)return;y[n]=c;let m=await O(o.url);if(!m||!m.length){await I(e,n,r);return}let g=await M(n),{token:N,isValid:_}=await v({apiKey:e,query:{domain:m,url:o.url,address:g}});if(!_){L(m);return}A(n,{action:"INJECT",token:N,domain:c})}),chrome.tabs.onRemoved.addListener(n=>delete y[n])},re=ne;export{re as bringInitBackground,Q as bringInitContentScript};
package/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ import bringInitContentScript from "./bringInitContentScript.js"
2
+ import bringInitBackground from "./bringInitBackground.js"
3
+
4
+ export {
5
+ bringInitBackground,
6
+ bringInitContentScript
7
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@bringweb3/chrome-extension-kit",
3
+ "version": "1.0.0",
4
+ "main": "dist/index.js",
5
+ "module": "dist/index.mjs",
6
+ "types": "dist/index.d.ts",
7
+ "license": "MIT",
8
+ "private": false,
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "scripts": {
13
+ "watch": "tsup index.ts --format cjs,esm --dts --watch",
14
+ "build": "tsup index.ts --format cjs,esm --dts --minify",
15
+ "lint": "tsc",
16
+ "test": "vitest --coverage",
17
+ "release": "yarn build & changeset publish"
18
+ },
19
+ "devDependencies": {
20
+ "@changesets/cli": "^2.27.7",
21
+ "@types/chrome": "^0.0.268",
22
+ "@vitest/coverage-v8": "^2.0.5",
23
+ "tsup": "^8.2.1",
24
+ "typescript": "^5.5.3",
25
+ "vitest": "^2.0.5"
26
+ }
27
+ }
@@ -0,0 +1,23 @@
1
+ import getQueryParams from '../utils/getQueryParams'
2
+ import { describe, it, expect } from 'vitest'
3
+
4
+ describe('getQueryParams', () => {
5
+ it('return params', () => {
6
+
7
+ const query = { param1: 'value1', param2: 'value2' }
8
+ const params = getQueryParams({ query })
9
+
10
+ expect(params).toEqual('param1=value1&param2=value2')
11
+ })
12
+ it('return empty string', () => {
13
+ const params = getQueryParams({ query: {} })
14
+
15
+ expect(params).toEqual('')
16
+ })
17
+ it('adds prefix', () => {
18
+ const query = { param1: 'value1', param2: 'value2' }
19
+ const params = getQueryParams({ query, prefix: 't' })
20
+
21
+ expect(params).toEqual('t_param1=value1&t_param2=value2')
22
+ })
23
+ })
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "commonjs", /* Specify what module code is generated. */
4
+ "target": "es2023", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
5
+ "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
6
+ "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
7
+ "strict": true, /* Enable all strict type-checking options. */
8
+ "skipLibCheck": true, /* Skip type checking all .d.ts files. */
9
+ "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
10
+ "noEmit": true, /* Disable emitting files from a compilation. */
11
+ }
12
+ }
package/types.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ type WalletAddress = string | undefined
2
+
3
+ type Endpoint = 'sandbox' | 'prod';
4
+
5
+ type IFrame = HTMLIFrameElement | null
6
+
7
+ interface Style {
8
+ [key: string]: string
9
+ }
10
+
11
+ interface KeyFrame {
12
+ name: string
13
+ rules: string
14
+ }
15
+
16
+ interface BringEvent {
17
+ data: {
18
+ from: string
19
+ action: string
20
+ style?: Style[]
21
+ keyFrames?: KeyFrame[]
22
+ extensionId: string
23
+ time?: number
24
+ }
25
+ }
@@ -0,0 +1,33 @@
1
+ import { ApiEndpoint } from "../apiEndpoint";
2
+
3
+ interface CheckEventsProps {
4
+ apiKey: string;
5
+ walletAddress: string
6
+ cashbackUrl: string | undefined;
7
+ }
8
+
9
+ interface Body {
10
+ walletAddress: string;
11
+ cashbackUrl?: string;
12
+ test?: boolean
13
+ }
14
+
15
+ const checkEvents = async ({ apiKey, walletAddress, cashbackUrl }: CheckEventsProps) => {
16
+ const endpoint = ApiEndpoint.getInstance().getApiEndpoint()
17
+ const body: Body = { walletAddress, test: true }
18
+ if (cashbackUrl) body.cashbackUrl = cashbackUrl;
19
+
20
+ const res = await fetch(`${endpoint}/check-events`, {
21
+ method: 'POST',
22
+ headers: {
23
+ 'Content-Type': 'application/json',
24
+ 'x-api-key': apiKey
25
+ },
26
+ body: JSON.stringify(body)
27
+ })
28
+ const json = await res.json()
29
+
30
+ return json
31
+ }
32
+
33
+ export default checkEvents;
@@ -0,0 +1,16 @@
1
+ import { ApiEndpoint } from "../apiEndpoint"
2
+
3
+ const fetchDomains = async (apiKey: string) => {
4
+ const endpoint = ApiEndpoint.getInstance().getApiEndpoint()
5
+
6
+ const res = await fetch(`${endpoint}/domains?country=us`, {
7
+ headers: {
8
+ 'x-api-key': apiKey
9
+ }
10
+ })
11
+ const json = await res.json()
12
+ // console.log({ json });
13
+ return json
14
+ }
15
+
16
+ export default fetchDomains
@@ -0,0 +1,28 @@
1
+ import { ApiEndpoint } from "../apiEndpoint";
2
+ import getQueryParams from "../getQueryParams";
3
+
4
+ interface ValidateDomainProps {
5
+ apiKey: string,
6
+ query: {
7
+ url: string,
8
+ domain: string,
9
+ address: WalletAddress,
10
+ country?: string
11
+ }
12
+ }
13
+
14
+ const validateDomain = async ({ apiKey, query }: ValidateDomainProps) => {
15
+ const endpoint = ApiEndpoint.getInstance().getApiEndpoint()
16
+ const params = getQueryParams({ query: { ...query, country: 'us' } })
17
+
18
+ const res = await fetch(`${endpoint}/token?${params.toString()}`, {
19
+ headers: {
20
+ 'x-api-key': apiKey
21
+ }
22
+ })
23
+ const json = await res.json()
24
+ // console.log({ json });
25
+ return json
26
+ }
27
+
28
+ export default validateDomain;
@@ -0,0 +1,28 @@
1
+
2
+ export class ApiEndpoint {
3
+ private static instance: ApiEndpoint | null = null;
4
+ private apiEndpoint: string = '';
5
+
6
+ private constructor() {
7
+ }
8
+
9
+ public static getInstance(): ApiEndpoint {
10
+ if (!ApiEndpoint.instance) {
11
+ ApiEndpoint.instance = new ApiEndpoint();
12
+ }
13
+ return ApiEndpoint.instance;
14
+ }
15
+
16
+ public setApiEndpoint(endpoint: string): void {
17
+ this.apiEndpoint = endpoint === 'prod'
18
+ ? 'https://api.bringweb3.io/v1/extension'
19
+ : 'https://sandbox-api.bringweb3.io/v1/extension';
20
+ }
21
+
22
+ public getApiEndpoint(): string {
23
+ if (!this.apiEndpoint) {
24
+ throw new Error('API endpoint not set. Call setApiEndpoint first.');
25
+ }
26
+ return this.apiEndpoint;
27
+ }
28
+ }
@@ -0,0 +1,2 @@
1
+ export const UPDATE_CACHE_ALARM_NAME = 'updateCache'
2
+ export const CHECK_EVENTS_ALARM_NAME = 'checkEvents'
@@ -0,0 +1,20 @@
1
+ const addKeyframes = (keyFrames: KeyFrame[] | undefined): void => {
2
+
3
+ if (!keyFrames || !keyFrames.length) return
4
+
5
+ const style = document.createElement('style');
6
+
7
+ document.head.appendChild(style);
8
+
9
+ const sheet = style.sheet;
10
+
11
+ if (sheet) {
12
+ keyFrames.forEach(({ name, rules }) => {
13
+ sheet.insertRule(`@keyframes ${name} { ${rules} }`, sheet.cssRules.length);
14
+ })
15
+ } else {
16
+ console.error('Failed to create stylesheet');
17
+ }
18
+ }
19
+
20
+ export default addKeyframes;
@@ -0,0 +1,11 @@
1
+ const applyStyles = (element: IFrame, style: Style[] | undefined) => {
2
+ if (!element || !style || !Object.keys(style).length) return;
3
+
4
+ Object.entries(style).forEach(([key, value]) => {
5
+ if (key in element.style) {
6
+ (element.style as any)[key] = value;
7
+ }
8
+ });
9
+ }
10
+
11
+ export default applyStyles;
@@ -0,0 +1,56 @@
1
+ import applyStyles from "./applyStyles"
2
+ import addKeyframes from "./addKeyFrames"
3
+
4
+ interface Props {
5
+ event: BringEvent
6
+ iframeEl: IFrame
7
+ promptLogin: () => Promise<WalletAddress>
8
+ }
9
+
10
+ const ACTIONS = {
11
+ OPEN: 'OPEN',
12
+ CLOSE: 'CLOSE',
13
+ ACTIVATE: 'ACTIVATE',
14
+ PROMPT_LOGIN: 'PROMPT_LOGIN',
15
+ OPT_OUT: 'OPT_OUT',
16
+ ADD_KEYFRAMES: 'ADD_KEYFRAMES'
17
+ }
18
+
19
+ const UNION_ACTIONS = [ACTIONS.ACTIVATE]
20
+
21
+ const handleIframeMessages = ({ event, iframeEl, promptLogin }: Props) => {
22
+ const { data } = event
23
+ const { from, action, style, keyFrames, time, extensionId } = data
24
+
25
+ if (from !== 'bringweb3') return
26
+
27
+ // If the event comes from another extension that installed our package, ignore it (unless it ACTIVATE action)
28
+ if (extensionId !== chrome.runtime.id && !UNION_ACTIONS.includes(action)) return
29
+
30
+ switch (action) {
31
+ case ACTIONS.OPEN:
32
+ applyStyles(iframeEl, style)
33
+ break;
34
+ case ACTIONS.CLOSE:
35
+ if (iframeEl) iframeEl.parentNode?.removeChild(iframeEl)
36
+ if (time) chrome.runtime.sendMessage({ action, time })
37
+ break;
38
+ case ACTIONS.PROMPT_LOGIN:
39
+ promptLogin()
40
+ break;
41
+ case ACTIONS.ACTIVATE:
42
+ chrome.runtime.sendMessage({ action })
43
+ break;
44
+ case ACTIONS.OPT_OUT:
45
+ chrome.runtime.sendMessage({ action, time })
46
+ break;
47
+ case ACTIONS.ADD_KEYFRAMES:
48
+ addKeyframes(keyFrames)
49
+ break;
50
+ default:
51
+ // console.log('Non exist ACTION:', action);
52
+ break;
53
+ }
54
+ }
55
+
56
+ export default handleIframeMessages;
@@ -0,0 +1,36 @@
1
+ import getQueryParams from "../getQueryParams";
2
+
3
+ interface Query {
4
+ [key: string]: string
5
+
6
+ }
7
+
8
+ interface Props {
9
+ query: Query
10
+ theme?: Style
11
+ iframeSrc: string
12
+ }
13
+
14
+ const injectIFrame = ({ query, theme, iframeSrc }: Props): HTMLIFrameElement => {
15
+ const extensionId = chrome.runtime.id;
16
+ const params = getQueryParams({ query: { ...query, extensionId } })
17
+ const customStyles = theme ? `&${getQueryParams({ query: theme, prefix: 't' })}` : ''
18
+ const iframe = document.createElement('iframe');
19
+ iframe.id = `bringweb3-iframe:${extensionId}`;
20
+ iframe.src = `${iframeSrc}?${params}${customStyles}`;
21
+ iframe.setAttribute('sandbox', "allow-popups allow-scripts allow-same-origin allow-top-navigation-by-user-activation")
22
+ iframe.style.position = "fixed";
23
+ iframe.scrolling = "no";
24
+ iframe.style.overflow = "hidden";
25
+ iframe.style.width = "1px";
26
+ iframe.style.height = "1px";
27
+ iframe.style.right = "8px";
28
+ iframe.style.borderRadius = "10px";
29
+ iframe.style.border = "none";
30
+ iframe.style.cssText += `z-index: 99999999999999 !important;`;
31
+ if (theme?.popupShadow) iframe.style.boxShadow = theme.popupShadow;
32
+ document.documentElement.appendChild(iframe);
33
+ return iframe
34
+ }
35
+
36
+ export default injectIFrame;
@@ -0,0 +1,30 @@
1
+ interface Props {
2
+ walletAddressListeners: string[],
3
+ getWalletAddress: () => Promise<WalletAddress>
4
+ iframeEl: IFrame
5
+ }
6
+
7
+ const startListenersForWalletAddress = ({ walletAddressListeners, getWalletAddress, iframeEl }: Props) => {
8
+ for (let i = 0; i < walletAddressListeners.length; i++) {
9
+ const eventName = walletAddressListeners[i]
10
+ if (!eventName) continue
11
+ window.addEventListener(eventName, async (e) => {
12
+ // console.log('NEW EVENT');
13
+ // console.log(e.detail);
14
+
15
+
16
+ if (!iframeEl) {
17
+ iframeEl = document.querySelector(`#bringweb3-iframe:${chrome.runtime.id}`)
18
+ if (!iframeEl) return
19
+ }
20
+ if (!iframeEl.contentWindow) {
21
+ return
22
+ }
23
+ const address = await getWalletAddress()
24
+
25
+ iframeEl.contentWindow.postMessage({ action: 'WALLET_ADDRESS_UPDATE', walletAddress: address }, '*')
26
+ });
27
+ }
28
+ }
29
+
30
+ export default startListenersForWalletAddress;
@@ -0,0 +1,27 @@
1
+ interface Query {
2
+ [key: string]: string | undefined
3
+
4
+ }
5
+
6
+ interface props {
7
+ query: Query
8
+ prefix?: string
9
+ }
10
+
11
+ const getQueryParams = (props: props) => {
12
+ const params = new URLSearchParams()
13
+ const { query, prefix } = props
14
+
15
+ Object.entries(query).forEach(([key, value]) => {
16
+ if (!value) return
17
+ if (prefix) key = `${prefix}_${key}`
18
+ if (key === 'url') {
19
+ params.append(key, encodeURIComponent(value))
20
+ } else {
21
+ params.append(key, value)
22
+ }
23
+ })
24
+ return params.toString()
25
+ }
26
+
27
+ export default getQueryParams
@@ -0,0 +1,25 @@
1
+ const set = async (key: string, value: any) => {
2
+ chrome.storage.local.set({ [key]: value });
3
+ }
4
+
5
+ const get = async (key: string) => {
6
+ const data = await chrome.storage.local.get(key)
7
+ return data[key]
8
+ }
9
+
10
+ const remove = async (key: string) => {
11
+ chrome.storage.local.remove(key)
12
+ }
13
+
14
+ // const clear = async () => {
15
+ // chrome.storage.local.clear(() => {
16
+ // console.log('Cache cleared successfully')
17
+ // })
18
+ // }
19
+
20
+ export default {
21
+ set,
22
+ get,
23
+ remove,
24
+ // clear
25
+ }