diff --git a/background/main.ts b/background/main.ts index 0a3ea23de..be26f8c40 100644 --- a/background/main.ts +++ b/background/main.ts @@ -328,6 +328,9 @@ export default class Main extends BaseService { internalEthereumProviderService, preferenceService, ) + + const notificationsService = NotificationsService.create(preferenceService) + const islandService = IslandService.create(chainService, indexingService) const telemetryService = TelemetryService.create() @@ -353,11 +356,6 @@ export default class Main extends BaseService { ledgerService, ) - const notificationsService = NotificationsService.create( - preferenceService, - islandService, - ) - const walletConnectService = isEnabled(FeatureFlags.SUPPORT_WALLET_CONNECT) ? WalletConnectService.create( providerBridgeService, @@ -669,6 +667,7 @@ export default class Main extends BaseService { this.connectWalletConnectService() this.connectAbilitiesService() this.connectNFTsService() + this.connectNotificationsService() await this.connectChainService() @@ -1592,6 +1591,13 @@ export default class Main extends BaseService { }, ) + this.preferenceService.emitter.on( + "initializeNotificationsPreferences", + async (isPermissionGranted) => { + this.store.dispatch(toggleNotifications(isPermissionGranted)) + }, + ) + this.preferenceService.emitter.on( "dismissableItemMarkedAsShown", async (dismissableItem) => { @@ -1756,6 +1762,12 @@ export default class Main extends BaseService { }) } + connectNotificationsService(): void { + this.islandService.emitter.on("newXpDrop", () => { + this.notificationsService.notifyXPDrop() + }) + } + async unlockInternalSigners(password: string): Promise { return this.internalSignerService.unlock(password) } diff --git a/background/services/island/index.ts b/background/services/island/index.ts index 8a3f8604d..05bc6c8af 100644 --- a/background/services/island/index.ts +++ b/background/services/island/index.ts @@ -36,6 +36,7 @@ interface Events extends ServiceLifecycleEvents { newEligibility: Eligible newReferral: { referrer: AddressOnNetwork } & ReferrerStats monitoringTestnetAsset: SmartContractFungibleAsset + newXpDrop: void } /* @@ -147,6 +148,9 @@ export default class IslandService extends BaseService { ) if (realmXpAsset !== undefined) { this.emitter.emit("monitoringTestnetAsset", realmXpAsset) + realmContract.on(realmContract.filters.XpDistributed(), () => { + this.checkXPDrop() + }) } }), ) @@ -225,6 +229,10 @@ export default class IslandService extends BaseService { return this.db.getReferrerStats(referrer) } + private checkXPDrop() { + this.emitter.emit("newXpDrop", undefined) + } + private async trackReferrals({ address, network, diff --git a/background/services/notifications/index.ts b/background/services/notifications/index.ts index 108a80ed3..4543b015b 100644 --- a/background/services/notifications/index.ts +++ b/background/services/notifications/index.ts @@ -1,8 +1,12 @@ import { uniqueId } from "lodash" +import browser from "webextension-polyfill" import BaseService from "../base" -import IslandService from "../island" import PreferenceService from "../preferences" import { ServiceCreatorFunction, ServiceLifecycleEvents } from "../types" +import { HOUR } from "../../constants" + +const TAHO_ICON_URL = + "https://taho.xyz/icons/icon-144x144.png?v=41306c4d4e6795cdeaecc31bd794f68e" type Events = ServiceLifecycleEvents & { notificationDisplayed: string @@ -11,6 +15,8 @@ type Events = ServiceLifecycleEvents & { type NotificationClickHandler = (() => Promise) | (() => void) +const NOTIFICATIONS_XP_DROP_THRESHOLD = 24 * HOUR + /** * The NotificationService manages all notifications for the extension. It is * charged both with managing the actual notification lifecycle (notification @@ -31,6 +37,8 @@ export default class NotificationsService extends BaseService { [notificationId: string]: NotificationClickHandler } = {} + private lastXpDropNotificationInMs?: number + /* * Create a new NotificationsService. The service isn't initialized until * startService() is called and resolved. @@ -38,14 +46,10 @@ export default class NotificationsService extends BaseService { static create: ServiceCreatorFunction< Events, NotificationsService, - [Promise, Promise] - > = async (preferenceService, islandService) => - new this(await preferenceService, await islandService) - - private constructor( - private preferenceService: PreferenceService, - private islandService: IslandService, - ) { + [Promise] + > = async (preferenceService) => new this(await preferenceService) + + private constructor(private preferenceService: PreferenceService) { super() } @@ -63,28 +67,33 @@ export default class NotificationsService extends BaseService { // browser notifications permission has been granted. The preferences service // does guard this, but if that ends up not being true, browser.notifications // will be undefined and all of this will explode. - this.isPermissionGranted = - await this.preferenceService.getShouldShowNotifications() + + this.preferenceService.emitter.on( + "initializeNotificationsPreferences", + async (isPermissionGranted) => { + this.isPermissionGranted = isPermissionGranted + }, + ) this.preferenceService.emitter.on( "setNotificationsPermission", (isPermissionGranted) => { - if (typeof browser !== "undefined") { - if (isPermissionGranted) { - browser.notifications.onClicked.addListener( - boundHandleNotificationClicks, - ) - browser.notifications.onClosed.addListener( - boundCleanUpNotificationClickHandler, - ) - } else { - browser.notifications.onClicked.removeListener( - boundHandleNotificationClicks, - ) - browser.notifications.onClosed.removeListener( - boundCleanUpNotificationClickHandler, - ) - } + this.isPermissionGranted = isPermissionGranted + + if (this.isPermissionGranted) { + browser.notifications.onClicked.addListener( + boundHandleNotificationClicks, + ) + browser.notifications.onClosed.addListener( + boundCleanUpNotificationClickHandler, + ) + } else { + browser.notifications.onClicked.removeListener( + boundHandleNotificationClicks, + ) + browser.notifications.onClosed.removeListener( + boundCleanUpNotificationClickHandler, + ) } }, ) @@ -95,23 +104,8 @@ export default class NotificationsService extends BaseService { boundCleanUpNotificationClickHandler, ) } - - /* - * FIXME add below - this.islandService.emitter.on("xpDropped", this.notifyXpDrop.bind(this)) - */ } - // TODO: uncomment when the XP drop is ready - // protected async notifyDrop(/* xpInfos: XpInfo[] */): Promise { - // const callback = () => { - // browser.tabs.create({ - // url: "dapp url for realm claim, XpInfo must include realm id, ideally some way to communicate if the address is right as well", - // }) - // } - // this.notify({ callback }) - // } - // Fires the click handler for the given notification id. protected handleNotificationClicks(notificationId: string): void { this.clickHandlers?.[notificationId]() @@ -127,15 +121,16 @@ export default class NotificationsService extends BaseService { * The click action, if specified, will be fired when the user clicks on the * notification. */ - protected async notify({ - title = "", - message = "", - contextMessage = "", + public notify({ + options, callback, }: { - title?: string - message?: string - contextMessage?: string + options: { + title: string + message: string + contextMessage?: string + type?: browser.Notifications.TemplateType + } callback?: () => void }) { if (!this.isPermissionGranted) { @@ -143,12 +138,32 @@ export default class NotificationsService extends BaseService { } const notificationId = uniqueId("notification-") - await browser.notifications.create(notificationId, { - type: "basic", - title, - message, - contextMessage, - isClickable: !!callback, - }) + const notificationOptions = { + type: "basic" as browser.Notifications.TemplateType, + iconUrl: TAHO_ICON_URL, + ...options, + } + + if (typeof callback === "function") { + this.clickHandlers[notificationId] = callback + } + + browser.notifications.create(notificationId, notificationOptions) + } + + public notifyXPDrop(callback?: () => void): void { + const shouldShowXpDropNotifications = this.lastXpDropNotificationInMs + ? Date.now() > + this.lastXpDropNotificationInMs + NOTIFICATIONS_XP_DROP_THRESHOLD + : true + + if (shouldShowXpDropNotifications) { + this.lastXpDropNotificationInMs = Date.now() + const options = { + title: "Weekly XP distributed", + message: "Visit Subscape to see if you are eligible", + } + this.notify({ options, callback }) + } } } diff --git a/background/services/preferences/index.ts b/background/services/preferences/index.ts index 300757a6f..f55e24fd0 100644 --- a/background/services/preferences/index.ts +++ b/background/services/preferences/index.ts @@ -1,3 +1,4 @@ +import browser from "webextension-polyfill" import { FiatCurrency } from "../../assets" import { AddressOnNetwork, NameOnNetwork } from "../../accounts" import { ServiceLifecycleEvents, ServiceCreatorFunction } from "../types" @@ -108,6 +109,7 @@ interface Events extends ServiceLifecycleEvents { initializeDefaultWallet: boolean initializeSelectedAccount: AddressOnNetwork initializeShownDismissableItems: DismissableItem[] + initializeNotificationsPreferences: boolean updateAnalyticsPreferences: AnalyticsPreferences addressBookEntryModified: AddressBookEntry updatedSignerSettings: AccountSignerSettings[] @@ -156,6 +158,11 @@ export default class PreferenceService extends BaseService { "initializeShownDismissableItems", await this.getShownDismissableItems(), ) + + this.emitter.emit( + "initializeNotificationsPreferences", + await this.getShouldShowNotificationsPreferences(), + ) } protected override async internalStopService(): Promise { @@ -265,32 +272,23 @@ export default class PreferenceService extends BaseService { this.emitter.emit("updateAnalyticsPreferences", analytics) } - async getShouldShowNotifications(): Promise { + async getShouldShowNotificationsPreferences(): Promise { return (await this.db.getPreferences()).shouldShowNotifications } async setShouldShowNotifications(shouldShowNotifications: boolean) { - const permissionRequest: Promise = new Promise((resolve) => { - if (shouldShowNotifications) { - chrome.permissions.request( - { - permissions: ["notifications"], - }, - (granted) => { - resolve(granted) - }, - ) - } else { - resolve(false) - } - }) - - return permissionRequest.then(async (granted) => { + if (shouldShowNotifications) { + const granted = await browser.permissions.request({ + permissions: ["notifications"], + }) + await this.db.setShouldShowNotifications(granted) this.emitter.emit("setNotificationsPermission", granted) return granted - }) + } + + return false } async getAccountSignerSettings(): Promise {