Ctrl K
ComunidadGuías…

src/services/nativeNotifications.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import type { CapgoBackgroundNotificationEvent, CapgoBackgroundNotificationResult, CapgoNotificationInstallMode } from '@capgo/capacitor-notifications'
import { App } from '@capacitor/app'
import { Capacitor } from '@capacitor/core'
import { CapgoNotifications } from '@capgo/capacitor-notifications'
import { invokeCapgoApi } from '~/services/capgoApi'
import { defaultApiHost } from '~/services/console'

interface SelfProofResponse {
  appId: string
  externalId: string
  identityProof: string
}

const UPDATE_CHECK_ACTIONS = new Set(['update_check', 'capgo_update_check'])

let setupPromise: Promise<string | null> | null = null
let registeredUserId: string | null = null
let registeringUserId: string | null = null
// Bumped on sign-out so an in-flight registration for the previous user is discarded.
let registrationGeneration = 0
let nativeNotificationOpQueue: Promise<unknown> = Promise.resolve()

function enqueueNativeNotificationOp<T>(op: () => Promise<T>): Promise<T> {
  const next = nativeNotificationOpQueue.then(op, op)
  nativeNotificationOpQueue = next.catch(() => undefined)
  return next
}

function getStringData(data: Record<string, unknown> | undefined, ...keys: string[]) {
  for (const key of keys) {
    const value = data?.[key]
    if (typeof value === 'string' && value.trim())
      return value.trim()
  }
  return ''
}

function isUpdateCheckNotification(data: Record<string, unknown> | undefined) {
  return UPDATE_CHECK_ACTIONS.has(getStringData(data, 'capgoAction', 'capgo_action'))
}

/**
 * Never reload the WebView under a user who is looking at the app: queue the
 * bundle for the next background instead. When the app was woken in the
 * background (or the user is in another app), apply it right away so the next
 * open already runs the new version.
 */
export function resolveUpdateInstallMode(requested: string, appIsActive: boolean): CapgoNotificationInstallMode {
  if (appIsActive)
    return 'next'
  return requested === 'next' ? 'next' : 'set'
}

async function isAppActive() {
  try {
    return (await App.getState()).isActive
  }
  catch {
    return true
  }
}

async function handleBackgroundNotification({ notification, finish }: CapgoBackgroundNotificationEvent) {
  let result: CapgoBackgroundNotificationResult = 'noData'
  try {
    if (!isUpdateCheckNotification(notification.data))
      return
    // Plugin 8.x runs the updater natively and reports it here: nothing left to do.
    const nativeStatus = getStringData(notification.data, 'capgoNativeUpdateCheck')
    if (nativeStatus && nativeStatus !== 'unsupported') {
      if (nativeStatus === 'queued')
        result = 'newData'
      else if (nativeStatus === 'failed')
        result = 'failed'
      return
    }
    const requested = getStringData(notification.data, 'capgoUpdateInstallMode', 'capgo_update_install_mode')
    // The channel in the push is ignored on purpose: the update server resolves
    // this device's own channel, so a push for another channel is a no-op here.
    const update = await CapgoNotifications.runUpdateCheck({
      enabled: true,
      installMode: resolveUpdateInstallMode(requested, await isAppActive()),
    })
    if (update.status === 'installed')
      result = 'newData'
    else if (update.status === 'failed')
      result = 'failed'
    if (update.error && update.status === 'failed')
      console.warn('Capgo push update check failed', update.error)
  }
  catch (error) {
    result = 'failed'
    console.warn('Capgo push update check failed', error)
  }
  finally {
    await finish(result).catch(() => undefined)
  }
}

async function setup(): Promise<string | null> {
  // Update checks are handled here instead of by the plugin so the device's own
  // channel is used and an active user is never interrupted by a reload. Turn
  // the plugin's handler off and attach ours before configure() starts the
  // native bridge, so a push that woke the app is never handled by the plugin.
  await CapgoNotifications.enableUpdaterIntegration({ enabled: false })
  await CapgoNotifications.addListener('backgroundNotification', (event) => {
    void handleBackgroundNotification(event)
  })
  const { id: appId } = await App.getInfo()
  await CapgoNotifications.configure({ appId, serverUrl: defaultApiHost, autoUpdater: false })
  return appId
}

/**
 * Bind native notification listeners as early as possible: a silent push can
 * cold-start the app in the background, and the listener must be attached
 * before the plugin hands the queued event to JavaScript.
 */
export function setupNativeNotifications(): Promise<string | null> {
  if (!Capacitor.isNativePlatform())
    return Promise.resolve(null)
  setupPromise ??= setup().catch((error) => {
    console.warn('Capgo notifications setup failed', error)
    setupPromise = null
    return null
  })
  return setupPromise
}

/** Register this device with Capgo native notifications for the signed-in user. */
export async function registerNativeNotifications(userId: string) {
  if (!Capacitor.isNativePlatform() || registeredUserId === userId || registeringUserId === userId)
    return
  registeringUserId = userId
  const generation = registrationGeneration
  return enqueueNativeNotificationOp(async () => {
    try {
      const appId = await setupNativeNotifications()
      if (!appId || generation !== registrationGeneration)
        return
      const { data, error } = await invokeCapgoApi<SelfProofResponse>('private/notification_self_proof', {
        method: 'POST',
        body: { appId },
      })
      if (generation !== registrationGeneration)
        return
      if (error || !data?.identityProof || data.externalId !== userId)
        throw error ?? new Error('Missing notification identity proof')
      await CapgoNotifications.register({
        appId,
        serverUrl: defaultApiHost,
        externalId: data.externalId,
        identityProof: data.identityProof,
        consent: true,
      })
      if (generation !== registrationGeneration) {
        // Signed out while registering: undo the registration that just landed.
        await CapgoNotifications.unregister()
        return
      }
      registeredUserId = userId
    }
    catch (error) {
      console.warn('Capgo notifications registration failed', error)
    }
    finally {
      if (registeringUserId === userId)
        registeringUserId = null
    }
  })
}

/** Stop pushes for the signed-out user: deactivate the server registration and forget the local identity. */
export async function unregisterNativeNotifications(force = false) {
  if (!Capacitor.isNativePlatform())
    return
  registrationGeneration++
  const hadRegistration = registeredUserId !== null || registeringUserId !== null
  registeredUserId = null
  registeringUserId = null
  if (!hadRegistration && !force)
    return
  return enqueueNativeNotificationOp(async () => {
    try {
      await setupNativeNotifications()
      await CapgoNotifications.unregister()
    }
    catch (error) {
      console.warn('Capgo notifications unregister failed', error)
    }
  })
}