Search code examples
typescriptfirebasegoogle-cloud-functionsgoogle-cloud-pubsubrevenuecat

How do I properly set up a webhook endpoint API to handle RevenueCat events with Firebase Functions?


I'm having trouble setting up a webhook endpoint API directly from RevenueCat's documentation.

My code is almost exactly like from the documentation so I don't know why this error is being triggered, and I'm not experienced enough with this kind of stuff to fix the issue. Here is the error I'm getting:

(parameter) res: functions.Response<any>

Argument of type '(req: Request, res: Response<any>) => Response<any> | Promise<void | Response<any>>' is not assignable to parameter of type '(req: Request, resp: Response<any>) => void | Promise<void>'.
  Type 'Response<any> | Promise<void | Response<any>>' is not assignable to type 'void | Promise<void>'.
    Type 'Response<any>' is not assignable to type 'void | Promise<void>'.
      Type 'Response<any>' is missing the following properties from type 'Promise<void>': then, catch, finally, [Symbol.toStringTag]ts(2345)

Honestly, I'm not sure what its asking me to change. Any ideas? And here is my code:

import * as functions from 'firebase-functions'
import { PubSub } from '@google-cloud/pubsub'

const pubsubClient = new PubSub({projectId: '<PROJ_ID>'})

function isAuthorized(req: functions.https.Request) { 
    // Check authorization header
    if (!req.headers.authorization || !req.headers.authorization.startsWith('Bearer ')) { 
        return false
    }

    const authToken = req.headers.authorization.split('Bearer ')[1]
    if (authToken !== '<MY_AUTH_TOKEN>') { 
        return false
    }

    return true
}

// Respond to incoming message
export const revenueCatApi = functions.https.onRequest((req, res) => { // *** ERROR DETECTED HERE

    // Only allow POST request
    if (req.method !== 'POST') { 
        return res.status(403).send('Forbidden')
    }

    // Make sure the auth key matches what we set in the Revenue Cat dashboard
    if (!isAuthorized(req)) { 
        return res.status(401).send('Unauthorized')
    }

    const rc = req.body as RCEvent
    var topic: RCTopic = ''

    switch (rc.event.type) { 
        case 'INITIAL_PURCHASE':
            topic = 'rc-initial-purchase'
            break
        case 'NON_RENEWING_PURCHASE':
            topic = 'rc-non-renewing-purchase'
            break
        case 'RENEWAL':
            topic = 'rc-renewal'
            break
        case 'PRODUCT_CHANGE':
            topic = 'rc-product-change'
            break
        case 'CANCELLATION':
            topic = 'rc-cancellation'
            break
        case 'BILLING_ISSUE':
            topic = 'rc-billing-issue'
            break
        case 'SUBSCRIBER_ALIAS':
            topic = 'rc-subscriber-alias'
            break
        default:
            console.log('Unhandled event type: ', rc.event.type)
            return res.sendStatus(200)
    }

    // Set the pub/sub data to the event body
    const dataBuffer = Buffer.from(JSON.stringify(rc))

    // Publishes a message
    return pubsubClient.topic(topic)
        .publish(dataBuffer)
        .then(() => res.sendStatus(200))
        .catch(err => { 
            console.error(err)
            res.sendStatus(500)
            return Promise.reject(err)
        })
})

exports.handleInitialPurchase = functions.pubsub
    .topic('rc-initial-purchase')
    .onPublish(async (message, context) => {
        ...
    })

/* Other pubsub functions below */

RCEvent:

interface RCEvent { 
    api_version: string
    event: { 
        aliases: string[]
        app_id: string
        app_user_id: string
        country_code: string
        currency: string
        entitlement_id: string
        entitlement_ids: string[]
        environment: string
        event_timestamp_ms: number
        expiration_at_ms: number
        id: string
        is_family_share: boolean
        offer_code?: string
        original_app_user_id: string
        original_transaction_id: string
        period_type: string
        presented_offering_id: string
        price: number
        price_in_purchased_currency: number
        product_id: string
        purchased_at_ms: number
        store: string
        subscriber_attributes: any
        takehome_percentage: number
        transaction_id: string
        type: string
    }
}

Solution

  • The error message is telling you that TypeScript has determined that the signature of your function is this:

    (req: Request, res: Response<any>) => Response<any> | Promise<void | Response<any>>
    

    That means it takes a Request and Response as arguments, and can return either one of Response<any> or Promise<void | Response<any>>. That's not compatible with the requirement that it return only void or Promise<void>.

    Your function possibly returns three things:

    return res.status(403).send('Forbidden')
    
    return res.status(401).send('Unauthorized')
    
    return pubsubClient.topic(topic)
    

    The first two things you are not really trying to return. You are just trying to send a response and end early. The third thing is a promise.

    Don't return the result of res.status(403).send('Forbidden'). Just return null if you want to terminate the function early without doing any further work.

    res.status(403).send('Forbidden')
    return null
    

    Do this for both of the returns where you have no promise to wait on. This will make your function state that it only returns a promise or null, which is compatible with the TypeScript requirements for the function.