about summary refs log tree commit diff
path: root/src/lib/notifications/notifications.ts
blob: ab7fc570889654b58bfb27dcd4c412020b4b3823 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
import React from 'react'
import * as Notifications from 'expo-notifications'
import {getBadgeCountAsync, setBadgeCountAsync} from 'expo-notifications'
import {BskyAgent} from '@atproto/api'

import {logEvent} from '#/lib/statsig/statsig'
import {Logger} from '#/logger'
import {devicePlatform, isAndroid, isNative} from '#/platform/detection'
import {SessionAccount, useAgent, useSession} from '#/state/session'
import BackgroundNotificationHandler from '../../../modules/expo-background-notification-handler'

const SERVICE_DID = (serviceUrl?: string) =>
  serviceUrl?.includes('staging')
    ? 'did:web:api.staging.bsky.dev'
    : 'did:web:api.bsky.app'

const logger = Logger.create(Logger.Context.Notifications)

async function registerPushToken(
  agent: BskyAgent,
  account: SessionAccount,
  token: Notifications.DevicePushToken,
) {
  try {
    await agent.api.app.bsky.notification.registerPush({
      serviceDid: SERVICE_DID(account.service),
      platform: devicePlatform,
      token: token.data,
      appId: 'xyz.blueskyweb.app',
    })
    logger.debug('Notifications: Sent push token (init)', {
      tokenType: token.type,
      token: token.data,
    })
  } catch (error) {
    logger.error('Notifications: Failed to set push token', {message: error})
  }
}

async function getPushToken(skipPermissionCheck = false) {
  const granted =
    skipPermissionCheck || (await Notifications.getPermissionsAsync()).granted
  if (granted) {
    return Notifications.getDevicePushTokenAsync()
  }
}

export function useNotificationsRegistration() {
  const agent = useAgent()
  const {currentAccount} = useSession()

  React.useEffect(() => {
    if (!currentAccount) {
      return
    }

    // HACK - see https://github.com/bluesky-social/social-app/pull/4467
    // An apparent regression in expo-notifications causes `addPushTokenListener` to not fire on Android whenever the
    // token changes by calling `getPushToken()`. This is a workaround to ensure we register the token once it is
    // generated on Android.
    if (isAndroid) {
      ;(async () => {
        const token = await getPushToken()

        // Token will be undefined if we don't have notifications permission
        if (token) {
          registerPushToken(agent, currentAccount, token)
        }
      })()
    } else {
      getPushToken()
    }

    // According to the Expo docs, there is a chance that the token will change while the app is open in some rare
    // cases. This will fire `registerPushToken` whenever that happens.
    const subscription = Notifications.addPushTokenListener(async newToken => {
      registerPushToken(agent, currentAccount, newToken)
    })

    return () => {
      subscription.remove()
    }
  }, [currentAccount, agent])
}

export function useRequestNotificationsPermission() {
  const {currentAccount} = useSession()
  const agent = useAgent()

  return async (
    context: 'StartOnboarding' | 'AfterOnboarding' | 'Login' | 'Home',
  ) => {
    const permissions = await Notifications.getPermissionsAsync()

    if (
      !isNative ||
      permissions?.status === 'granted' ||
      (permissions?.status === 'denied' && !permissions.canAskAgain)
    ) {
      return
    }
    if (context === 'AfterOnboarding') {
      return
    }
    if (context === 'Home' && !currentAccount) {
      return
    }

    const res = await Notifications.requestPermissionsAsync()
    logEvent('notifications:request', {
      context: context,
      status: res.status,
    })

    if (res.granted) {
      // This will fire a pushTokenEvent, which will handle registration of the token
      const token = await getPushToken(true)

      // Same hack as above. We cannot rely on the `addPushTokenListener` to fire on Android due to an Expo bug, so we
      // will manually register it here. Note that this will occur only:
      // 1. right after the user signs in, leading to no `currentAccount` account being available - this will be instead
      // picked up from the useEffect above on `currentAccount` change
      // 2. right after onboarding. In this case, we _need_ this registration, since `currentAccount` will not change
      // and we need to ensure the token is registered right after permission is granted. `currentAccount` will already
      // be available in this case, so the registration will succeed.
      // We should remove this once expo-notifications (and possibly FCMv1) is fixed and the `addPushTokenListener` is
      // working again. See https://github.com/expo/expo/issues/28656
      if (isAndroid && currentAccount && token) {
        registerPushToken(agent, currentAccount, token)
      }
    }
  }
}

export async function decrementBadgeCount(by: number) {
  if (!isNative) return

  let count = await getBadgeCountAsync()
  count -= by
  if (count < 0) {
    count = 0
  }

  await BackgroundNotificationHandler.setBadgeCountAsync(count)
  await setBadgeCountAsync(count)
}

export async function resetBadgeCount() {
  await BackgroundNotificationHandler.setBadgeCountAsync(0)
  await setBadgeCountAsync(0)
}