Skip to content

Instantly share code, notes, and snippets.

@Pieeer1
Last active October 8, 2025 19:41
Show Gist options
  • Select an option

  • Save Pieeer1/34152312ca6269cda81fb68214863049 to your computer and use it in GitHub Desktop.

Select an option

Save Pieeer1/34152312ca6269cda81fb68214863049 to your computer and use it in GitHub Desktop.
Expo Modules Voip Push Token

Introduction

This gist shows the very basic bare minimum to get a voip push token in expo 53.

Getting Started

  1. Navigate to your project root
  2. Run the following command: npx create-expo-module --local expo-voip-push-token
  3. Remove all of the View and Web based files, we will not be needing those.
  4. Replace the Relevant Files with the ones in the gist. Podfile, Gradle Files, and Manifests are not modified.
export interface VoipToken {
voipToken: string;
}
export type ExpoVoipPushTokenModuleEvents = {
onRegistration: (params: VoipToken) => void;
notification: (params: { payload: Record<string, any> }) => void;
onCallAccepted: (params: { callUUID: string }) => void;
onCallEnded: (params: { callUUID: string }) => void;
};
package expo.modules.voippushtoken
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import java.net.URL
class ExpoVoipPushTokenModule : Module() {
override fun definition() = ModuleDefinition {
Name("ExpoVoipPushToken")
Events("onRegistration", "notification")
Function("registerVoipPushToken") {
// do nothing, as this is technically IOS only
}
}
}
import ExpoModulesCore
import Foundation
import PushKit
import CallKit
class VoipPushDelegate: NSObject, PKPushRegistryDelegate, CXProviderDelegate {
var onTokenReceived: ((String) -> Void)?
var onIncomingPush: ((PKPushPayload) -> Void)?
var onCallAccepted: ((UUID) -> Void)?
var onCallEnded: ((UUID) -> Void)?
var voipRegistrationToken: String?
var pushRegistry: PKPushRegistry?
private let callProvider: CXProvider
private let callController = CXCallController()
override init() {
let config = CXProviderConfiguration(localizedName: "App Name")
config.supportsVideo = true
config.maximumCallsPerCallGroup = 1
config.supportedHandleTypes = [.generic]
pushRegistry = PKPushRegistry(queue: DispatchQueue.main)
callProvider = CXProvider(configuration: config)
super.init()
pushRegistry?.delegate = self
pushRegistry?.desiredPushTypes = [.voIP]
callProvider.setDelegate(self, queue: nil)
}
func pushRegistry(
_ registry: PKPushRegistry,
didUpdate pushCredentials: PKPushCredentials,
for type: PKPushType
) {
guard type == .voIP else { return }
let tokenData = pushCredentials.token
let tokenParts = tokenData.map { String(format: "%02x", $0) }
voipRegistrationToken = tokenParts.joined()
onTokenReceived?(voipRegistrationToken ?? "")
}
func pushRegistry(
_ registry: PKPushRegistry,
didReceiveIncomingPushWith payload: PKPushPayload,
for type: PKPushType,
completion: @escaping () -> Void
) {
guard type == .voIP else {
completion()
return
}
// CallKit expects us to call this immediately
let update = CXCallUpdate()
update.hasVideo = true
update.remoteHandle = CXHandle(
type: .generic,
value: (payload.dictionaryPayload["callerName"] as? String ?? "Unknown Caller")
)
let uuid = payload.dictionaryPayload["uuid"] as? String
callProvider.reportNewIncomingCall(with: UUID(uuidString: uuid ?? "") ?? UUID(), update: update) { error in
if let error = error {
print("Error reporting call: \(error)")
} else {
self.onIncomingPush?(payload)
}
completion()
}
}
func startCall(callUUID: String, handle: String, callerName: String) {
let callUUIDObj = UUID(uuidString: callUUID) ?? UUID()
let handle = CXHandle(type: .generic, value: handle)
let callAction = CXStartCallAction(call: callUUIDObj, handle: handle)
callAction.isVideo = true
let transaction = CXTransaction(action: callAction)
self.callController.request(transaction, completion: { error in})
}
func endCall(callUUID: String) {
let callUUIDObj = UUID(uuidString: callUUID) ?? UUID()
let action = CXEndCallAction(call: callUUIDObj)
let transaction = CXTransaction(action: action)
self.callController.request(transaction, completion: { error in})
}
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
// Handle the action, e.g., start audio, setup media, etc.
let callUUID = action.callUUID
self.onCallAccepted?(callUUID)
action.fulfill()
}
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
let callUUID = action.callUUID
self.onCallEnded?(callUUID)
action.fulfill()
}
func providerDidReset(_ provider: CXProvider) {
// Handle provider reset if needed
}
}
public final class ExpoVoipPushTokenModule: Module {
private let delegate = VoipPushDelegate()
private func initializePushDelegate() {
delegate.onTokenReceived = { [weak self] token in
self?.sendEvent("onRegistration", ["voipToken": token])
}
delegate.onIncomingPush = { [weak self] payload in
let payloadDict = payload.dictionaryPayload
self?.sendEvent("notification", ["payload": payloadDict])
}
delegate.onCallAccepted = { [weak self] uuid in
self?.sendEvent("onCallAccepted", ["callUUID": uuid.uuidString])
}
delegate.onCallEnded = { [weak self] uuid in
self?.sendEvent("onCallEnded", ["callUUID": uuid.uuidString])
}
}
private func startCall(callUUID: String, handle: String, callerName: String) {
delegate.startCall(callUUID: callUUID, handle: handle, callerName: callerName)
}
private func endCall(callUUID: String) {
delegate.endCall(callUUID: callUUID)
}
public func definition() -> ModuleDefinition {
Name("ExpoVoipPushToken")
Events("onRegistration", "notification", "onCallAccepted", "onCallEnded")
OnCreate(){
self.initializePushDelegate()
}
Function("startCall") { (callUUID: String, handle: String, callerName: String) -> Void in
self.startCall(callUUID: callUUID, handle: handle, callerName: callerName)
}
Function("endCall") { (callUUID: String) -> Void in
self.endCall(callUUID: callUUID)
}
Function("registerVoipPushToken") {
self.sendEvent("onRegistration", ["voipToken": self.delegate.voipRegistrationToken ?? ""])
}
}
}
import { NativeModule, requireNativeModule } from 'expo';
import { ExpoVoipPushTokenModuleEvents } from './ExpoVoipPushToken.types';
declare class ExpoVoipPushTokenModule extends NativeModule<ExpoVoipPushTokenModuleEvents> {
registerVoipPushToken(): void;
startCall(callUUID: string, handle: string, callerName: string): void;
endCall(callUUID: string): void;
}
// This call loads the native module object from the JSI.
export default requireNativeModule<ExpoVoipPushTokenModule>('ExpoVoipPushToken');
// Reexport the native module. On web, it will be resolved to ExpoVoipPushTokenModule.web.ts
// and on native platforms to ExpoVoipPushTokenModule.ts
export { default } from './src/ExpoVoipPushTokenModule';
export * from './src/ExpoVoipPushToken.types';
@Pieeer1
Copy link
Author

Pieeer1 commented Sep 26, 2025

Not too sure, I am not running into that issue. I would make sure you have all the delegates registered in your app config as an invalid argument on the voip registration is screaming permission issue to me,

@Pieeer1
Copy link
Author

Pieeer1 commented Oct 2, 2025

are you two running in a simulator or a physical device?

@VladChernyak
Copy link

@Pieeer1
For some reason, the event listeners in my layout component aren’t working as expected.
I can successfully get a VoIP token when authorizing with ExpoVoipPushToken.registerVoipPushToken() and ExpoVoipPushToken.addListener("onRegistration", () => {...}).

However, the event listener ExpoVoipPushToken.addListener("notification", () => {...}) that I call inside useEffect in my layout component doesn’t fire — even though the VoIP push definitely arrives (I can see the incoming call).

Any idea what might be causing this?

@Pieeer1
Copy link
Author

Pieeer1 commented Oct 8, 2025

@Pieeer1 For some reason, the event listeners in my layout component aren’t working as expected. I can successfully get a VoIP token when authorizing with ExpoVoipPushToken.registerVoipPushToken() and ExpoVoipPushToken.addListener("onRegistration", () => {...}).

However, the event listener ExpoVoipPushToken.addListener("notification", () => {...}) that I call inside useEffect in my layout component doesn’t fire — even though the VoIP push definitely arrives (I can see the incoming call).

Any idea what might be causing this?

is it failing to push entirely or are you getting any error messages? what device are you using?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment