Skip to content

Commit

Permalink
Monitoring XP drop (#3679)
Browse files Browse the repository at this point in the history
### What
Showing notifications at the time of XP drop.


### Prerequisite
The user allows the notification to be displayed.


### Testing
- [x] if he has not yet consented to notification: click the link in the
Subscape bubble / or switch Show Notification in the Settings.
- [x] Drop XP in contracts on your local machine


### Result
![Screenshot 2023-11-28 at 09 48
24](https://github.com/tahowallet/extension/assets/28560653/06c37f80-d095-46aa-8203-e0c16d9cf576)



### Important!
Do not merge! I'll do that, before merge we need to change notifiaction
time threshold from 30sec(for testing purposes) to 24h:
`NOTIFICATIONS_XP_DROP_THRESHOLD_MS_FOR_TESTING_PURPOSE` to
`NOTIFICATIONS_XP_DROP_THRESHOLD_MS`

Latest build:
[extension-builds-3679](https://github.com/tahowallet/extension/suites/18823274127/artifacts/1097695154)
(as of Wed, 06 Dec 2023 22:39:43 GMT).
  • Loading branch information
Shadowfiend authored Dec 7, 2023
2 parents 014a028 + f76595b commit c2f21bf
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 79 deletions.
22 changes: 17 additions & 5 deletions background/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,9 @@ export default class Main extends BaseService<never> {
internalEthereumProviderService,
preferenceService,
)

const notificationsService = NotificationsService.create(preferenceService)

const islandService = IslandService.create(chainService, indexingService)

const telemetryService = TelemetryService.create()
Expand All @@ -353,11 +356,6 @@ export default class Main extends BaseService<never> {
ledgerService,
)

const notificationsService = NotificationsService.create(
preferenceService,
islandService,
)

const walletConnectService = isEnabled(FeatureFlags.SUPPORT_WALLET_CONNECT)
? WalletConnectService.create(
providerBridgeService,
Expand Down Expand Up @@ -669,6 +667,7 @@ export default class Main extends BaseService<never> {
this.connectWalletConnectService()
this.connectAbilitiesService()
this.connectNFTsService()
this.connectNotificationsService()

await this.connectChainService()

Expand Down Expand Up @@ -1592,6 +1591,13 @@ export default class Main extends BaseService<never> {
},
)

this.preferenceService.emitter.on(
"initializeNotificationsPreferences",
async (isPermissionGranted) => {
this.store.dispatch(toggleNotifications(isPermissionGranted))
},
)

this.preferenceService.emitter.on(
"dismissableItemMarkedAsShown",
async (dismissableItem) => {
Expand Down Expand Up @@ -1756,6 +1762,12 @@ export default class Main extends BaseService<never> {
})
}

connectNotificationsService(): void {
this.islandService.emitter.on("newXpDrop", () => {
this.notificationsService.notifyXPDrop()
})
}

async unlockInternalSigners(password: string): Promise<boolean> {
return this.internalSignerService.unlock(password)
}
Expand Down
8 changes: 8 additions & 0 deletions background/services/island/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ interface Events extends ServiceLifecycleEvents {
newEligibility: Eligible
newReferral: { referrer: AddressOnNetwork } & ReferrerStats
monitoringTestnetAsset: SmartContractFungibleAsset
newXpDrop: void
}

/*
Expand Down Expand Up @@ -147,6 +148,9 @@ export default class IslandService extends BaseService<Events> {
)
if (realmXpAsset !== undefined) {
this.emitter.emit("monitoringTestnetAsset", realmXpAsset)
realmContract.on(realmContract.filters.XpDistributed(), () => {
this.checkXPDrop()
})
}
}),
)
Expand Down Expand Up @@ -225,6 +229,10 @@ export default class IslandService extends BaseService<Events> {
return this.db.getReferrerStats(referrer)
}

private checkXPDrop() {
this.emitter.emit("newXpDrop", undefined)
}

private async trackReferrals({
address,
network,
Expand Down
127 changes: 71 additions & 56 deletions background/services/notifications/index.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -11,6 +15,8 @@ type Events = ServiceLifecycleEvents & {

type NotificationClickHandler = (() => Promise<void>) | (() => 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
Expand All @@ -31,21 +37,19 @@ export default class NotificationsService extends BaseService<Events> {
[notificationId: string]: NotificationClickHandler
} = {}

private lastXpDropNotificationInMs?: number

/*
* Create a new NotificationsService. The service isn't initialized until
* startService() is called and resolved.
*/
static create: ServiceCreatorFunction<
Events,
NotificationsService,
[Promise<PreferenceService>, Promise<IslandService>]
> = async (preferenceService, islandService) =>
new this(await preferenceService, await islandService)

private constructor(
private preferenceService: PreferenceService,
private islandService: IslandService,
) {
[Promise<PreferenceService>]
> = async (preferenceService) => new this(await preferenceService)

private constructor(private preferenceService: PreferenceService) {
super()
}

Expand All @@ -63,28 +67,33 @@ export default class NotificationsService extends BaseService<Events> {
// 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,
)
}
},
)
Expand All @@ -95,23 +104,8 @@ export default class NotificationsService extends BaseService<Events> {
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<void> {
// 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]()
Expand All @@ -127,28 +121,49 @@ export default class NotificationsService extends BaseService<Events> {
* 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) {
return
}
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 })
}
}
}
34 changes: 16 additions & 18 deletions background/services/preferences/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import browser from "webextension-polyfill"
import { FiatCurrency } from "../../assets"
import { AddressOnNetwork, NameOnNetwork } from "../../accounts"
import { ServiceLifecycleEvents, ServiceCreatorFunction } from "../types"
Expand Down Expand Up @@ -108,6 +109,7 @@ interface Events extends ServiceLifecycleEvents {
initializeDefaultWallet: boolean
initializeSelectedAccount: AddressOnNetwork
initializeShownDismissableItems: DismissableItem[]
initializeNotificationsPreferences: boolean
updateAnalyticsPreferences: AnalyticsPreferences
addressBookEntryModified: AddressBookEntry
updatedSignerSettings: AccountSignerSettings[]
Expand Down Expand Up @@ -156,6 +158,11 @@ export default class PreferenceService extends BaseService<Events> {
"initializeShownDismissableItems",
await this.getShownDismissableItems(),
)

this.emitter.emit(
"initializeNotificationsPreferences",
await this.getShouldShowNotificationsPreferences(),
)
}

protected override async internalStopService(): Promise<void> {
Expand Down Expand Up @@ -265,32 +272,23 @@ export default class PreferenceService extends BaseService<Events> {
this.emitter.emit("updateAnalyticsPreferences", analytics)
}

async getShouldShowNotifications(): Promise<boolean> {
async getShouldShowNotificationsPreferences(): Promise<boolean> {
return (await this.db.getPreferences()).shouldShowNotifications
}

async setShouldShowNotifications(shouldShowNotifications: boolean) {
const permissionRequest: Promise<boolean> = 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<AccountSignerSettings[]> {
Expand Down

0 comments on commit c2f21bf

Please sign in to comment.