diff --git a/.github/actions/rn-bootstrap/action.yml b/.github/actions/rn-bootstrap/action.yml index d4697b972c..a7bf1199b1 100644 --- a/.github/actions/rn-bootstrap/action.yml +++ b/.github/actions/rn-bootstrap/action.yml @@ -33,7 +33,7 @@ runs: - uses: ruby/setup-ruby@v1 with: - ruby-version: 3.1 + ruby-version: 3.4 working-directory: sample-apps/react-native/dogfood bundler-cache: true diff --git a/package.json b/package.json index 2f36430665..01612be8e4 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "start:react-native:ios:dogfood": "yarn workspace @stream-io/video-react-native-dogfood run ios", "start:react-native:android:dogfood": "yarn workspace @stream-io/video-react-native-dogfood run android", "build:styling": "yarn workspace @stream-io/video-styling run build", + "build:react-native-callingx": "yarn workspace @stream-io/react-native-callingx run build", "start:styling": "yarn workspace @stream-io/video-styling run start", "build:client": "yarn workspace @stream-io/video-client run build", "start:client": "yarn workspace @stream-io/video-client run start", @@ -31,7 +32,7 @@ "build:video-filters-react-native": "yarn workspace @stream-io/video-filters-react-native run build", "build:noise-cancellation-react-native": "yarn workspace @stream-io/noise-cancellation-react-native run build", "build:react:deps": "yarn workspaces foreach -Apv --topological-dev --include 'packages/{client,react-{sdk,bindings},styling,{video,audio}-filters-web}' run build", - "build:react-native:deps": "yarn workspaces foreach -Apv --topological-dev --include 'packages/{client,react-bindings,audio-filters-web,{video-filters,noise-cancellation}-react-native,react-native-sdk}' run build", + "build:react-native:deps": "yarn workspaces foreach -Apv --topological-dev --include 'packages/{client,react-bindings,audio-filters-web,{video-filters,noise-cancellation}-react-native,react-native-sdk,react-native-callingx}' run build", "build:vercel": "yarn build:react:deps && yarn build:react:dogfood", "start:egress": "yarn workspace @stream-io/egress-composite start", "build:egress": "yarn workspace @stream-io/egress-composite build", @@ -58,10 +59,12 @@ "release:react-bindings": "yarn workspace @stream-io/video-react-bindings npm publish --access=public --tag=latest", "release:react-sdk": "yarn workspace @stream-io/video-react-sdk npm publish --access=public --tag=latest", "release:react-native-sdk": "yarn workspace @stream-io/video-react-native-sdk npm publish --access=public --tag=latest", + "release:react-native-sdk:beta": "node scripts/release-rn-sdk-beta.mjs", "release:audio-filters-web": "yarn workspace @stream-io/audio-filters-web npm publish --access=public --tag=latest", "release:video-filters-web": "yarn workspace @stream-io/video-filters-web npm publish --access=public --tag=latest", "release:video-filters-react-native": "yarn workspace @stream-io/video-filters-react-native npm publish --access=public --tag=latest", "release:noise-cancellation-react-native": "yarn workspace @stream-io/noise-cancellation-react-native npm publish --access=public --tag=latest", + "release:react-native-callingx": "yarn workspace @stream-io/react-native-callingx npm publish --access=public --tag=latest", "release:styling": "yarn workspace @stream-io/video-styling npm publish --access=public --tag=latest", "postinstall": "husky" }, diff --git a/packages/client/src/Call.ts b/packages/client/src/Call.ts index 92ac8c5a67..d35d4ce594 100644 --- a/packages/client/src/Call.ts +++ b/packages/client/src/Call.ts @@ -421,6 +421,7 @@ export class Call { const currentUserId = this.currentUserId; if (currentUserId && blockedUserIds.includes(currentUserId)) { this.logger.info('Leaving call because of being blocked'); + globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'restricted'); await this.leave({ message: 'user blocked' }).catch((err) => { this.logger.error('Error leaving call after being blocked', err); }); @@ -465,6 +466,10 @@ export class Call { (isAcceptedElsewhere || isRejectedByMe) && !hasPending(this.joinLeaveConcurrencyTag) ) { + globalThis.streamRNVideoSDK?.callingX?.endCall( + this, + isAcceptedElsewhere ? 'answeredElsewhere' : 'rejected', + ); this.leave().catch(() => { this.logger.error( 'Could not leave a call that was accepted or rejected elsewhere', @@ -480,6 +485,10 @@ export class Call { const receiver_id = this.clientStore.connectedUser?.id; const ended_at = callSession?.ended_at; const created_by_id = this.state.createdBy?.id; + + if (this.currentUserId && created_by_id === this.currentUserId) { + globalThis.streamRNVideoSDK?.callingX?.registerOutgoingCall(this); + } const rejected_by = callSession?.rejected_by; const accepted_by = callSession?.accepted_by; let leaveCallIdle = false; @@ -636,16 +645,30 @@ export class Call { if (callingState === CallingState.RINGING && reject !== false) { if (reject) { - await this.reject(reason ?? 'decline'); + const reasonToEndCallReason = { + timeout: 'missed', + cancel: 'canceled', + busy: 'busy', + decline: 'rejected', + } as const; + const rejectReason = reason ?? 'decline'; + const endCallReason = + reasonToEndCallReason[ + rejectReason as keyof typeof reasonToEndCallReason + ] ?? 'rejected'; + await this.reject(rejectReason); + globalThis.streamRNVideoSDK?.callingX?.endCall(this, endCallReason); } else { // if reject was undefined, we still have to cancel the call automatically // when I am the creator and everyone else left the call const hasOtherParticipants = this.state.remoteParticipants.length > 0; if (this.isCreatedByMe && !hasOtherParticipants) { await this.reject('cancel'); + globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'canceled'); } } } + globalThis.streamRNVideoSDK?.callingX?.endCall(this); this.statsReporter?.stop(); this.statsReporter = undefined; @@ -680,7 +703,9 @@ export class Call { this.cancelAutoDrop(); this.clientStore.unregisterCall(this); - globalThis.streamRNVideoSDK?.callManager.stop(); + globalThis.streamRNVideoSDK?.callManager.stop({ + isRingingTypeCall: this.ringing, + }); this.camera.dispose(); this.microphone.dispose(); @@ -720,7 +745,9 @@ export class Call { * A flag indicating whether the call was created by the current user. */ get isCreatedByMe() { - return this.state.createdBy?.id === this.currentUserId; + return ( + this.currentUserId && this.state.createdBy?.id === this.currentUserId + ); } /** @@ -766,6 +793,7 @@ export class Call { video?: boolean; }): Promise => { await this.setup(); + const response = await this.streamClient.get( this.streamClientBasePath, params, @@ -805,6 +833,7 @@ export class Call { */ getOrCreate = async (data?: GetOrCreateCallRequest) => { await this.setup(); + const response = await this.streamClient.post< GetOrCreateCallResponse, GetOrCreateCallRequest @@ -930,60 +959,73 @@ export class Call { joinResponseTimeout?: number; rpcRequestTimeout?: number; } = {}): Promise => { - await this.setup(); const callingState = this.state.callingState; if ([CallingState.JOINED, CallingState.JOINING].includes(callingState)) { throw new Error(`Illegal State: call.join() shall be called only once`); } + if (data?.ring) { + this.ringingSubject.next(true); + } + const callingX = globalThis.streamRNVideoSDK?.callingX; + if (callingX) { + // for Android/iOS, we need to start the call in the callingx library as soon as possible + await callingX.joinCall(this, this.clientStore.calls); + } + + await this.setup(); + this.joinResponseTimeout = joinResponseTimeout; this.rpcRequestTimeout = rpcRequestTimeout; - // we will count the number of join failures per SFU. // once the number of failures reaches 2, we will piggyback on the `migrating_from` // field to force the coordinator to provide us another SFU const sfuJoinFailures = new Map(); const joinData: JoinCallData = data; maxJoinRetries = Math.max(maxJoinRetries, 1); - for (let attempt = 0; attempt < maxJoinRetries; attempt++) { - try { - this.logger.trace(`Joining call (${attempt})`, this.cid); - await this.doJoin(data); - delete joinData.migrating_from; - delete joinData.migrating_from_list; - break; - } catch (err) { - this.logger.warn(`Failed to join call (${attempt})`, this.cid); - if ( - (err instanceof ErrorFromResponse && err.unrecoverable) || - (err instanceof SfuJoinError && err.unrecoverable) - ) { - // if the error is unrecoverable, we should not retry as that signals - // that connectivity is good, but the coordinator doesn't allow the user - // to join the call due to some reason (e.g., ended call, expired token...) - throw err; - } + try { + for (let attempt = 0; attempt < maxJoinRetries; attempt++) { + try { + this.logger.trace(`Joining call (${attempt})`, this.cid); + await this.doJoin(data); + delete joinData.migrating_from; + delete joinData.migrating_from_list; + break; + } catch (err) { + this.logger.warn(`Failed to join call (${attempt})`, this.cid); + if ( + (err instanceof ErrorFromResponse && err.unrecoverable) || + (err instanceof SfuJoinError && err.unrecoverable) + ) { + // if the error is unrecoverable, we should not retry as that signals + // that connectivity is good, but the coordinator doesn't allow the user + // to join the call due to some reason (e.g., ended call, expired token...) + throw err; + } - // immediately switch to a different SFU in case of recoverable join error - const switchSfu = - err instanceof SfuJoinError && - SfuJoinError.isJoinErrorCode(err.errorEvent); - - const sfuId = this.credentials?.server.edge_name || ''; - const failures = (sfuJoinFailures.get(sfuId) || 0) + 1; - sfuJoinFailures.set(sfuId, failures); - if (switchSfu || failures >= 2) { - joinData.migrating_from = sfuId; - joinData.migrating_from_list = Array.from(sfuJoinFailures.keys()); - } + // immediately switch to a different SFU in case of recoverable join error + const switchSfu = + err instanceof SfuJoinError && + SfuJoinError.isJoinErrorCode(err.errorEvent); + + const sfuId = this.credentials?.server.edge_name || ''; + const failures = (sfuJoinFailures.get(sfuId) || 0) + 1; + sfuJoinFailures.set(sfuId, failures); + if (switchSfu || failures >= 2) { + joinData.migrating_from = sfuId; + joinData.migrating_from_list = Array.from(sfuJoinFailures.keys()); + } - if (attempt === maxJoinRetries - 1) { - throw err; + if (attempt === maxJoinRetries - 1) { + throw err; + } } + await sleep(retryInterval(attempt)); } - - await sleep(retryInterval(attempt)); + } catch (error) { + callingX?.endCall(this, 'error'); + throw error; } }; @@ -1166,7 +1208,9 @@ export class Call { // re-apply them on later reconnections or server-side data fetches if (!this.deviceSettingsAppliedOnce && this.state.settings) { await this.applyDeviceConfig(this.state.settings, true, false); - globalThis.streamRNVideoSDK?.callManager.start(); + globalThis.streamRNVideoSDK?.callManager.start({ + isRingingTypeCall: this.ringing, + }); this.deviceSettingsAppliedOnce = true; } @@ -1711,6 +1755,7 @@ export class Call { if (SfuJoinError.isJoinErrorCode(e)) return; if (strategy === WebsocketReconnectStrategy.UNSPECIFIED) return; if (strategy === WebsocketReconnectStrategy.DISCONNECT) { + globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'error'); this.leave({ message: 'SFU instructed to disconnect' }).catch((err) => { this.logger.warn(`Can't leave call after disconnect request`, err); }); diff --git a/packages/client/src/devices/SpeakerManager.ts b/packages/client/src/devices/SpeakerManager.ts index 1d862470e4..1561e27150 100644 --- a/packages/client/src/devices/SpeakerManager.ts +++ b/packages/client/src/devices/SpeakerManager.ts @@ -85,6 +85,7 @@ export class SpeakerManager { this.defaultDevice = defaultDevice; globalThis.streamRNVideoSDK?.callManager.setup({ defaultDevice, + isRingingTypeCall: this.call.ringing, }); } } diff --git a/packages/client/src/events/call.ts b/packages/client/src/events/call.ts index d3fe41861d..14715d6577 100644 --- a/packages/client/src/events/call.ts +++ b/packages/client/src/events/call.ts @@ -69,6 +69,7 @@ export const watchCallRejected = (call: Call) => { } else { if (rejectedBy[eventCall.created_by.id]) { call.logger.info('call creator rejected, leaving call'); + globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote'); await call.leave({ message: 'ring: creator rejected' }); } } @@ -80,6 +81,7 @@ export const watchCallRejected = (call: Call) => { */ export const watchCallEnded = (call: Call) => { return function onCallEnded() { + globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote'); const { callingState } = call.state; if ( callingState !== CallingState.IDLE && @@ -113,6 +115,7 @@ export const watchSfuCallEnded = (call: Call) => { // update the call state to reflect the call has ended. call.state.setEndedAt(new Date()); const reason = CallEndedReason[e.reason]; + globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote'); await call.leave({ message: `callEnded received: ${reason}` }); } catch (err) { call.logger.error( diff --git a/packages/client/src/helpers/RNSpeechDetector.ts b/packages/client/src/helpers/RNSpeechDetector.ts index 237b8695b8..492dca7eb8 100644 --- a/packages/client/src/helpers/RNSpeechDetector.ts +++ b/packages/client/src/helpers/RNSpeechDetector.ts @@ -23,11 +23,15 @@ export class RNSpeechDetector { : await navigator.mediaDevices.getUserMedia({ audio: true }); this.audioStream = audioStream; - this.pc1.addEventListener('icecandidate', async (e) => { - await this.pc2.addIceCandidate(e.candidate); + this.pc1.addEventListener('icecandidate', (e) => { + this.pc2.addIceCandidate(e.candidate).catch(() => { + // do nothing + }); }); this.pc2.addEventListener('icecandidate', async (e) => { - await this.pc1.addIceCandidate(e.candidate); + this.pc1.addIceCandidate(e.candidate).catch(() => { + // do nothing + }); }); this.pc2.addEventListener('track', (e) => { e.streams[0].getTracks().forEach((track) => { diff --git a/packages/client/src/store/stateStore.ts b/packages/client/src/store/stateStore.ts index 8206a89649..e4c529e110 100644 --- a/packages/client/src/store/stateStore.ts +++ b/packages/client/src/store/stateStore.ts @@ -42,7 +42,7 @@ export class StreamVideoWriteableStateStore { * The currently connected user. */ get connectedUser(): OwnUserResponse | undefined { - return RxUtils.getCurrentValue(this.connectedUserSubject); + return this.connectedUserSubject.getValue(); } /** diff --git a/packages/client/src/types.ts b/packages/client/src/types.ts index ca834890c3..4c544ac1bb 100644 --- a/packages/client/src/types.ts +++ b/packages/client/src/types.ts @@ -23,6 +23,7 @@ import type { import type { Comparator } from './sorting'; import type { StreamVideoWriteableStateStore } from './store'; import { AxiosError } from 'axios'; +import type { Call } from './Call'; export type StreamReaction = Pick< ReactionResponse, @@ -392,26 +393,68 @@ export type StartCallRecordingFnType = { ): Promise; }; +type StreamRNVideoSDKCallManagerRingingParams = { + isRingingTypeCall: boolean; +}; + +type StreamRNVideoSDKCallManagerSetupParams = + StreamRNVideoSDKCallManagerRingingParams & { + defaultDevice: AudioSettingsRequestDefaultDeviceEnum; + }; + +type StreamRNVideoSDKEndCallReason = + /** Call ended by the local user (e.g., hanging up). */ + | 'local' + /** Call ended by the remote party, or outgoing call was not answered. */ + | 'remote' + /** Call was rejected/declined by the user. */ + | 'rejected' + /** Remote party was busy. */ + | 'busy' + /** Call was answered on another device. */ + | 'answeredElsewhere' + /** No response to an incoming call. */ + | 'missed' + /** Call failed due to an error (e.g., network issue). */ + | 'error' + /** Call was canceled before the remote party could answer. */ + | 'canceled' + /** Call restricted (e.g., airplane mode, dialing restrictions). */ + | 'restricted' + /** Unknown or unspecified disconnect reason. */ + | 'unknown'; + +type StreamRNVideoSDKCallingX = { + joinCall: (call: Call, activeCalls: Call[]) => Promise; + endCall: ( + call: Call, + reason?: StreamRNVideoSDKEndCallReason, + ) => Promise; + registerOutgoingCall: (call: Call) => Promise; +}; + export type StreamRNVideoSDKGlobals = { + callingX: StreamRNVideoSDKCallingX; callManager: { /** * Sets up the in call manager. */ setup({ defaultDevice, - }: { - defaultDevice: AudioSettingsRequestDefaultDeviceEnum; - }): void; + isRingingTypeCall, + }: StreamRNVideoSDKCallManagerSetupParams): void; /** * Starts the in call manager. */ - start(): void; + start({ + isRingingTypeCall, + }: StreamRNVideoSDKCallManagerRingingParams): void; /** * Stops the in call manager. */ - stop(): void; + stop({ isRingingTypeCall }: StreamRNVideoSDKCallManagerRingingParams): void; }; permissions: { /** diff --git a/packages/noise-cancellation-react-native/package.json b/packages/noise-cancellation-react-native/package.json index bae8db3ad2..45aa2edd3f 100644 --- a/packages/noise-cancellation-react-native/package.json +++ b/packages/noise-cancellation-react-native/package.json @@ -56,7 +56,7 @@ "typescript": "^5.9.3" }, "peerDependencies": { - "@stream-io/react-native-webrtc": ">=125.3.0", + "@stream-io/react-native-webrtc": ">=137.1.2", "react-native": "*" }, "react-native-builder-bob": { diff --git a/packages/react-native-callingx/.editorconfig b/packages/react-native-callingx/.editorconfig new file mode 100644 index 0000000000..65365be68e --- /dev/null +++ b/packages/react-native-callingx/.editorconfig @@ -0,0 +1,15 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] + +indent_style = space +indent_size = 2 + +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/packages/react-native-callingx/.gitattributes b/packages/react-native-callingx/.gitattributes new file mode 100644 index 0000000000..e27f70fa49 --- /dev/null +++ b/packages/react-native-callingx/.gitattributes @@ -0,0 +1,3 @@ +*.pbxproj -text +# specific for windows script files +*.bat text eol=crlf diff --git a/packages/react-native-callingx/.gitignore b/packages/react-native-callingx/.gitignore new file mode 100644 index 0000000000..d408ad5cf7 --- /dev/null +++ b/packages/react-native-callingx/.gitignore @@ -0,0 +1 @@ +/android/bin \ No newline at end of file diff --git a/packages/react-native-callingx/.nvmrc b/packages/react-native-callingx/.nvmrc new file mode 100644 index 0000000000..c004e356d6 --- /dev/null +++ b/packages/react-native-callingx/.nvmrc @@ -0,0 +1 @@ +v22.20.0 diff --git a/packages/react-native-callingx/.watchmanconfig b/packages/react-native-callingx/.watchmanconfig new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/packages/react-native-callingx/.watchmanconfig @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/packages/react-native-callingx/Callingx.podspec b/packages/react-native-callingx/Callingx.podspec new file mode 100644 index 0000000000..62a0c90f93 --- /dev/null +++ b/packages/react-native-callingx/Callingx.podspec @@ -0,0 +1,23 @@ +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "package.json"))) + +Pod::Spec.new do |s| + s.name = "Callingx" + s.version = package["version"] + s.summary = package["description"] + s.homepage = package["homepage"] + s.license = package["license"] + s.authors = package["author"] + + s.platforms = { :ios => min_ios_version_supported } + s.source = { :git => "https://github.com/GetStream/stream-video-js/tree/main/packages/react-native-callingx.git", :tag => "#{s.version}" } + + s.source_files = "ios/**/*.{h,m,mm,swift}" + s.public_header_files = "ios/CallingxPublic.h" + s.swift_version = "5.0" + + s.dependency "stream-react-native-webrtc" + + install_modules_dependencies(s) +end diff --git a/packages/react-native-callingx/LICENSE b/packages/react-native-callingx/LICENSE new file mode 100644 index 0000000000..f2d1eaf319 --- /dev/null +++ b/packages/react-native-callingx/LICENSE @@ -0,0 +1,219 @@ +SOURCE CODE LICENSE AGREEMENT + +IMPORTANT - READ THIS CAREFULLY BEFORE DOWNLOADING, INSTALLING, USING OR +ELECTRONICALLY ACCESSING THIS PROPRIETARY PRODUCT. + +THIS IS A LEGAL AGREEMENT BETWEEN STREAM.IO, INC. (“STREAM.IO”) AND THE +BUSINESS ENTITY OR PERSON FOR WHOM YOU (“YOU”) ARE ACTING (“CUSTOMER”) AS THE +LICENSEE OF THE PROPRIETARY SOFTWARE INTO WHICH THIS AGREEMENT HAS BEEN +INCLUDED (THE “AGREEMENT”). YOU AGREE THAT YOU ARE THE CUSTOMER, OR YOU ARE AN +EMPLOYEE OR AGENT OF CUSTOMER AND ARE ENTERING INTO THIS AGREEMENT FOR LICENSE +OF THE SOFTWARE BY CUSTOMER FOR CUSTOMER’S BUSINESS PURPOSES AS DESCRIBED IN +AND IN ACCORDANCE WITH THIS AGREEMENT. YOU HEREBY AGREE THAT YOU ENTER INTO +THIS AGREEMENT ON BEHALF OF CUSTOMER AND THAT YOU HAVE THE AUTHORITY TO BIND +CUSTOMER TO THIS AGREEMENT. + +STREAM.IO IS WILLING TO LICENSE THE SOFTWARE TO CUSTOMER ONLY ON THE FOLLOWING +CONDITIONS: (1) YOU ARE A CURRENT CUSTOMER OF STREAM.IO; (2) YOU ARE NOT A +COMPETITOR OF STREAM.IO; AND (3) THAT YOU ACCEPT ALL THE TERMS IN THIS +AGREEMENT. BY DOWNLOADING, INSTALLING, CONFIGURING, ACCESSING OR OTHERWISE +USING THE SOFTWARE, INCLUDING ANY UPDATES, UPGRADES, OR NEWER VERSIONS, YOU +REPRESENT, WARRANT AND ACKNOWLEDGE THAT (A) CUSTOMER IS A CURRENT CUSTOMER OF +STREAM.IO; (B) CUSTOMER IS NOT A COMPETITOR OF STREAM.IO; AND THAT (C) YOU HAVE +READ THIS AGREEMENT, UNDERSTAND THIS AGREEMENT, AND THAT CUSTOMER AGREES TO BE +BOUND BY ALL THE TERMS OF THIS AGREEMENT. + +IF YOU DO NOT AGREE TO ALL THE TERMS AND CONDITIONS OF THIS AGREEMENT, +STREAM.IO IS UNWILLING TO LICENSE THE SOFTWARE TO CUSTOMER, AND THEREFORE, DO +NOT COMPLETE THE DOWNLOAD PROCESS, ACCESS OR OTHERWISE USE THE SOFTWARE, AND +CUSTOMER SHOULD IMMEDIATELY RETURN THE SOFTWARE AND CEASE ANY USE OF THE +SOFTWARE. + +1. SOFTWARE. The Stream.io software accompanying this Agreement, may include +Source Code, Executable Object Code, associated media, printed materials and +documentation (collectively, the “Software”). The Software also includes any +updates or upgrades to or new versions of the original Software, if and when +made available to you by Stream.io. “Source Code” means computer programming +code in human readable form that is not suitable for machine execution without +the intervening steps of interpretation or compilation. “Executable Object +Code" means the computer programming code in any other form than Source Code +that is not readily perceivable by humans and suitable for machine execution +without the intervening steps of interpretation or compilation. “Site” means a +Customer location controlled by Customer. “Authorized User” means any employee +or contractor of Customer working at the Site, who has signed a written +confidentiality agreement with Customer or is otherwise bound in writing by +confidentiality and use obligations at least as restrictive as those imposed +under this Agreement. + +2. LICENSE GRANT. Subject to the terms and conditions of this Agreement, in +consideration for the representations, warranties, and covenants made by +Customer in this Agreement, Stream.io grants to Customer, during the term of +this Agreement, a personal, non-exclusive, non-transferable, non-sublicensable +license to: + +a. install and use Software Source Code on password protected computers at a Site, +restricted to Authorized Users; + +b. create derivative works, improvements (whether or not patentable), extensions +and other modifications to the Software Source Code (“Modifications”) to build +unique scalable newsfeeds, activity streams, and in-app messaging via Stream’s +application program interface (“API”); + +c. compile the Software Source Code to create Executable Object Code versions of +the Software Source Code and Modifications to build such newsfeeds, activity +streams, and in-app messaging via the API; + +d. install, execute and use such Executable Object Code versions solely for +Customer’s internal business use (including development of websites through +which data generated by Stream services will be streamed (“Apps”)); + +e. use and distribute such Executable Object Code as part of Customer’s Apps; and + +f. make electronic copies of the Software and Modifications as required for backup +or archival purposes. + +3. RESTRICTIONS. Customer is responsible for all activities that occur in +connection with the Software. Customer will not, and will not attempt to: (a) +sublicense or transfer the Software or any Source Code related to the Software +or any of Customer’s rights under this Agreement, except as otherwise provided +in this Agreement, (b) use the Software Source Code for the benefit of a third +party or to operate a service; (c) allow any third party to access or use the +Software Source Code; (d) sublicense or distribute the Software Source Code or +any Modifications in Source Code or other derivative works based on any part of +the Software Source Code; (e) use the Software in any manner that competes with +Stream.io or its business; or (e) otherwise use the Software in any manner that +exceeds the scope of use permitted in this Agreement. Customer shall use the +Software in compliance with any accompanying documentation any laws applicable +to Customer. + +4. OPEN SOURCE. Customer and its Authorized Users shall not use any software or +software components that are open source in conjunction with the Software +Source Code or any Modifications in Source Code or in any way that could +subject the Software to any open source licenses. + +5. CONTRACTORS. Under the rights granted to Customer under this Agreement, +Customer may permit its employees, contractors, and agencies of Customer to +become Authorized Users to exercise the rights to the Software granted to +Customer in accordance with this Agreement solely on behalf of Customer to +provide services to Customer; provided that Customer shall be liable for the +acts and omissions of all Authorized Users to the extent any of such acts or +omissions, if performed by Customer, would constitute a breach of, or otherwise +give rise to liability to Customer under, this Agreement. Customer shall not +and shall not permit any Authorized User to use the Software except as +expressly permitted in this Agreement. + +6. COMPETITIVE PRODUCT DEVELOPMENT. Customer shall not use the Software in any way +to engage in the development of products or services which could be reasonably +construed to provide a complete or partial functional or commercial alternative +to Stream.io’s products or services (a “Competitive Product”). Customer shall +ensure that there is no direct or indirect use of, or sharing of, Software +source code, or other information based upon or derived from the Software to +develop such products or services. Without derogating from the generality of +the foregoing, development of Competitive Products shall include having direct +or indirect access to, supervising, consulting or assisting in the development +of, or producing any specifications, documentation, object code or source code +for, all or part of a Competitive Product. + +7. LIMITATION ON MODIFICATIONS. Notwithstanding any provision in this Agreement, +Modifications may only be created and used by Customer as permitted by this +Agreement and Modification Source Code may not be distributed to third parties. +Customer will not assert against Stream.io, its affiliates, or their customers, +direct or indirect, agents and contractors, in any way, any patent rights that +Customer may obtain relating to any Modifications for Stream.io, its +affiliates’, or their customers’, direct or indirect, agents’ and contractors’ +manufacture, use, import, offer for sale or sale of any Stream.io products or +services. + +8. DELIVERY AND ACCEPTANCE. The Software will be delivered electronically pursuant +to Stream.io standard download procedures. The Software is deemed accepted upon +delivery. + +9. IMPLEMENTATION AND SUPPORT. Stream.io has no obligation under this Agreement to +provide any support or consultation concerning the Software. + +10. TERM AND TERMINATION. The term of this Agreement begins when the Software is +downloaded or accessed and shall continue until terminated. Either party may +terminate this Agreement upon written notice. This Agreement shall +automatically terminate if Customer is or becomes a competitor of Stream.io or +makes or sells any Competitive Products. Upon termination of this Agreement for +any reason, (a) all rights granted to Customer in this Agreement immediately +cease to exist, (b) Customer must promptly discontinue all use of the Software +and return to Stream.io or destroy all copies of the Software in Customer’s +possession or control. Any continued use of the Software by Customer or attempt +by Customer to exercise any rights under this Agreement after this Agreement +has terminated shall be considered copyright infringement and subject Customer +to applicable remedies for copyright infringement. Sections 2, 5, 6, 8 and 9 +shall survive expiration or termination of this Agreement for any reason. + +11. OWNERSHIP. As between the parties, the Software and all worldwide intellectual +property rights and proprietary rights relating thereto or embodied therein, +are the exclusive property of Stream.io and its suppliers. Stream.io and its +suppliers reserve all rights in and to the Software not expressly granted to +Customer in this Agreement, and no other licenses or rights are granted by +implication, estoppel or otherwise. + +12. WARRANTY DISCLAIMER. USE OF THIS SOFTWARE IS ENTIRELY AT YOURS AND CUSTOMER’S +OWN RISK. THE SOFTWARE IS PROVIDED “AS IS” WITHOUT ANY WARRANTY OF ANY KIND +WHATSOEVER. STREAM.IO DOES NOT MAKE, AND HEREBY DISCLAIMS, ANY WARRANTY OF ANY +KIND, WHETHER EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING WITHOUT +LIMITATION, THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE, TITLE, NON-INFRINGEMENT OF THIRD-PARTY RIGHTS, RESULTS, EFFORTS, +QUALITY OR QUIET ENJOYMENT. STREAM.IO DOES NOT WARRANT THAT THE SOFTWARE IS +ERROR-FREE, WILL FUNCTION WITHOUT INTERRUPTION, WILL MEET ANY SPECIFIC NEED +THAT CUSTOMER HAS, THAT ALL DEFECTS WILL BE CORRECTED OR THAT IT IS +SUFFICIENTLY DOCUMENTED TO BE USABLE BY CUSTOMER. TO THE EXTENT THAT STREAM.IO +MAY NOT DISCLAIM ANY WARRANTY AS A MATTER OF APPLICABLE LAW, THE SCOPE AND +DURATION OF SUCH WARRANTY WILL BE THE MINIMUM PERMITTED UNDER SUCH LAW. +CUSTOMER ACKNOWLEDGES THAT IT HAS RELIED ON NO WARRANTIES OTHER THAN THE +EXPRESS WARRANTIES IN THIS AGREEMENT. + +13. LIMITATION OF LIABILITY. TO THE FULLEST EXTENT PERMISSIBLE BY LAW, STREAM.IO’S +TOTAL LIABILITY FOR ALL DAMAGES ARISING OUT OF OR RELATED TO THE SOFTWARE OR +THIS AGREEMENT, WHETHER IN CONTRACT, TORT (INCLUDING NEGLIGENCE) OR OTHERWISE, +SHALL NOT EXCEED $100. IN NO EVENT WILL STREAM.IO BE LIABLE FOR ANY INDIRECT, +CONSEQUENTIAL, EXEMPLARY, PUNITIVE, SPECIAL OR INCIDENTAL DAMAGES OF ANY KIND +WHATSOEVER, INCLUDING ANY LOST DATA AND LOST PROFITS, ARISING FROM OR RELATING +TO THE SOFTWARE EVEN IF STREAM.IO HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. CUSTOMER ACKNOWLEDGES THAT THIS PROVISION REFLECTS THE AGREED UPON +ALLOCATION OF RISK FOR THIS AGREEMENT AND THAT STREAM.IO WOULD NOT ENTER INTO +THIS AGREEMENT WITHOUT THESE LIMITATIONS ON ITS LIABILITY. + +14. General. Customer may not assign or transfer this Agreement, by operation of +law or otherwise, or any of its rights under this Agreement (including the +license rights granted to Customer) to any third party without Stream.io’s +prior written consent, which consent will not be unreasonably withheld or +delayed. Stream.io may assign this Agreement, without consent, including, but +limited to, affiliate or any successor to all or substantially all its business +or assets to which this Agreement relates, whether by merger, sale of assets, +sale of stock, reorganization or otherwise. Any attempted assignment or +transfer in violation of the foregoing will be null and void. Stream.io shall +not be liable hereunder by reason of any failure or delay in the performance of +its obligations hereunder for any cause which is beyond the reasonable control. +All notices, consents, and approvals under this Agreement must be delivered in +writing by courier, by electronic mail, or by certified or registered mail, +(postage prepaid and return receipt requested) to the other party at the +address set forth in the customer agreement between Stream.io and Customer and +will be effective upon receipt or when delivery is refused. This Agreement will +be governed by and interpreted in accordance with the laws of the State of +Colorado, without reference to its choice of laws rules. The United Nations +Convention on Contracts for the International Sale of Goods does not apply to +this Agreement. Any action or proceeding arising from or relating to this +Agreement shall be brought in a federal or state court in Denver, Colorado, and +each party irrevocably submits to the jurisdiction and venue of any such court +in any such action or proceeding. All waivers must be in writing. Any waiver or +failure to enforce any provision of this Agreement on one occasion will not be +deemed a waiver of any other provision or of such provision on any other +occasion. If any provision of this Agreement is unenforceable, such provision +will be changed and interpreted to accomplish the objectives of such provision +to the greatest extent possible under applicable law and the remaining +provisions will continue in full force and effect. Customer shall not violate +any applicable law, rule or regulation, including those regarding the export of +technical data. The headings of Sections of this Agreement are for convenience +and are not to be used in interpreting this Agreement. As used in this +Agreement, the word “including” means “including but not limited to.” This +Agreement (including all exhibits and attachments) constitutes the entire +agreement between the parties regarding the subject hereof and supersedes all +prior or contemporaneous agreements, understandings and communication, whether +written or oral. This Agreement may be amended only by a written document +signed by both parties. The terms of any purchase order or similar document +submitted by Customer to Stream.io will have no effect. diff --git a/packages/react-native-callingx/README.md b/packages/react-native-callingx/README.md new file mode 100644 index 0000000000..e549f5d000 --- /dev/null +++ b/packages/react-native-callingx/README.md @@ -0,0 +1,395 @@ +# react-native-callingx + +A React Native Turbo Module for seamless native calling integration. This library provides a unified API to integrate with **CallKit** on iOS and the **Telecom/ConnectionService** API on Android, enabling your app to display system-level calling UI and interact with native call controls. + +## Features + +- 📞 **Incoming call UI** — Display native incoming call screens (even when the app is killed) +- 📲 **Outgoing call registration** — Register outgoing calls with the system +- 🎛️ **Call controls** — Mute, hold, end calls with native system integration +- 🔔 **Custom notifications** — Configurable Android notification channels +- ⚡ **Turbo Module** — Built with the New Architecture for optimal performance +- 📱 **Background support** — Handle calls when the app is backgrounded or killed + +## Requirements + +- React Native 0.73+ (New Architecture / Turbo Modules) +- iOS 13.0+ +- Android API 26+ (Android 8.0 Oreo) + +## Installation + +```sh +npm install @stream-io/react-native-callingx +# or +yarn add @stream-io/react-native-callingx +``` + +### iOS Setup + +1. Add the required background modes to your `Info.plist`: + +```xml +UIBackgroundModes + + voip + audio + +``` + +2. Run pod install: + +```sh +cd ios && pod install +``` + +3. For VoIP push notifications, configure your `AppDelegate` to report incoming calls: + +```objc +#import + +- (void)pushRegistry:(PKPushRegistry *)registry +didReceiveIncomingPushWithPayload:(PKPushPayload *)payload + forType:(PKPushType)type +withCompletionHandler:(void (^)(void))completion { + + // Extract call information from payload + NSString *callId = payload.dictionaryPayload[@"call_id"]; + NSString *callerName = payload.dictionaryPayload[@"caller_name"]; + NSString *handle = payload.dictionaryPayload[@"handle"]; + BOOL hasVideo = [payload.dictionaryPayload[@"has_video"] boolValue]; + + [Callingx reportNewIncomingCall:callId + handle:handle + handleType:@"generic" + hasVideo:hasVideo + localizedCallerName:callerName + supportsHolding:YES + supportsDTMF:NO + supportsGrouping:NO + supportsUngrouping:NO + payload:payload.dictionaryPayload + withCompletionHandler:completion]; +} +``` + +### Android Setup + +## Usage + +### Setup + +Initialize the module with platform-specific configuration: + +```typescript +import { CallingxModule } from 'react-native-callingx'; + +// Setup must be called before any other method +CallingxModule.setup({ + ios: { + appName: 'My App', + supportsVideo: true, + maximumCallsPerCallGroup: 1, + maximumCallGroups: 1, + handleType: 'generic', // 'generic' | 'number' | 'phone' | 'email' + }, + android: { + incomingChannel: { + id: 'incoming_calls', + name: 'Incoming Calls', + sound: 'ringtone', // optional custom sound + vibration: true, + }, + outgoingChannel: { + id: 'ongoing_calls', + name: 'Ongoing Calls', + }, + // Optional: transform display text + titleTransformer: (name) => `Call from ${name}`, + subtitleTransformer: (phoneNumber) => phoneNumber, + }, +}); +``` + +### Request Permissions + +Before displaying calls, request the required permissions: + +```typescript +const permissions = await CallingxModule.requestPermissions(); +console.log('Audio permission:', permissions.recordAudio); +console.log('Notification permission:', permissions.postNotifications); +``` + +### Display Incoming Call + +Show the native incoming call UI: + +```typescript +await CallingxModule.displayIncomingCall( + 'unique-call-id', + '+1234567890', // phone number / handle + 'John Doe', // caller name + true, // has video +); +``` + +### Start Outgoing Call + +Register an outgoing call with the system: + +```typescript +await CallingxModule.startCall( + 'unique-call-id', + '+1234567890', + 'John Doe', + false, // audio only +); +``` + +### Answer Call + +Answer an incoming call programmatically: + +```typescript +await CallingxModule.answerIncomingCall('unique-call-id'); +``` + +### Activate Call + +Mark a call as active (connected): + +```typescript +await CallingxModule.setCurrentCallActive('unique-call-id'); +``` + +### End Call + +End a call with a specific reason: + +```typescript +import type { EndCallReason } from 'react-native-callingx'; + +// Available reasons: +// 'local' | 'remote' | 'rejected' | 'busy' | 'answeredElsewhere' | +// 'missed' | 'error' | 'canceled' | 'restricted' | 'unknown' +await CallingxModule.endCallWithReason('unique-call-id', 'remote'); +``` + +### Mute/Unmute + +Toggle call mute state: + +```typescript +await CallingxModule.setMutedCall('unique-call-id', true); // mute +await CallingxModule.setMutedCall('unique-call-id', false); // unmute +``` + +### Hold/Unhold + +Toggle call hold state: + +```typescript +await CallingxModule.setOnHoldCall('unique-call-id', true); // hold +await CallingxModule.setOnHoldCall('unique-call-id', false); // unhold +``` + +### Update Display + +Update the caller information during a call: + +```typescript +await CallingxModule.updateDisplay( + 'unique-call-id', + '+1234567890', + 'Updated Name', +); +``` + +### Event Listeners + +Subscribe to call events: + +```typescript +import { CallingxModule } from 'react-native-callingx'; +import type { EventName } from 'react-native-callingx'; + +// Answer event - user answered from system UI +const answerSubscription = CallingxModule.addEventListener( + 'answerCall', + (params) => { + console.log('Call answered:', params.callId); + }, +); + +// End event - call ended +const endSubscription = CallingxModule.addEventListener('endCall', (params) => { + console.log('Call ended:', params.callId, 'Cause:', params.cause); +}); + +// Hold toggle event +const holdSubscription = CallingxModule.addEventListener( + 'didToggleHoldCallAction', + (params) => { + console.log('Hold toggled:', params.callId, 'On hold:', params.hold); + }, +); + +// Mute toggle event +const muteSubscription = CallingxModule.addEventListener( + 'didPerformSetMutedCallAction', + (params) => { + console.log('Mute toggled:', params.callId, 'Muted:', params.muted); + }, +); + +// Start call action (outgoing call initiated from system) +const startSubscription = CallingxModule.addEventListener( + 'didReceiveStartCallAction', + (params) => { + console.log('Start call action:', params.callId); + }, +); + +// Clean up when done +answerSubscription.remove(); +endSubscription.remove(); +// ... remove other subscriptions +``` + +### Handle Initial Events + +When the app is launched from a killed state by a call action, retrieve queued events: + +```typescript +// Get events that occurred before the module was initialized +const initialEvents = CallingxModule.getInitialEvents(); +initialEvents.forEach((event) => { + console.log('Initial event:', event.eventName, event.params); +}); + +// Clear initial events after processing +await CallingxModule.clearInitialEvents(); +``` + +### Background Tasks (Android) + +Run background tasks for call-related operations: + +```typescript +// Start a managed background task +await CallingxModule.startBackgroundTask(async (taskData, stopTask) => { + try { + // Perform background work (e.g., connect to call server) + await connectToCallServer(); + } finally { + stopTask(); // Always call when done + } +}); + +// Or stop manually +await CallingxModule.stopBackgroundTask(); +``` + +## API Reference + +### CallingxModule + +| Method | Description | +| ---------------------------------------------------------------- | ---------------------------------------------------- | +| `setup(options)` | Initialize the module with platform-specific options | +| `requestPermissions()` | Request required permissions (audio, notifications) | +| `checkPermissions()` | Check current permission status | +| `displayIncomingCall(callId, phoneNumber, callerName, hasVideo)` | Display incoming call UI | +| `answerIncomingCall(callId)` | Answer an incoming call | +| `startCall(callId, phoneNumber, callerName, hasVideo)` | Register an outgoing call | +| `setCurrentCallActive(callId)` | Mark call as active/connected | +| `updateDisplay(callId, phoneNumber, callerName)` | Update caller display info | +| `endCallWithReason(callId, reason)` | End call with specified reason | +| `setMutedCall(callId, isMuted)` | Toggle call mute state | +| `setOnHoldCall(callId, isOnHold)` | Toggle call hold state | +| `addEventListener(eventName, callback)` | Subscribe to call events | +| `getInitialEvents()` | Get queued events from app launch | +| `clearInitialEvents()` | Clear queued initial events | +| `startBackgroundTask(taskProvider)` | Start Android background task | +| `stopBackgroundTask()` | Stop Android background task | +| `log(message, level)` | Log message to native console | + +### Events + +| Event | Parameters | Description | +| ------------------------------ | ------------------- | --------------------------------- | +| `answerCall` | `{ callId }` | User answered call from system UI | +| `endCall` | `{ callId, cause }` | Call ended | +| `didToggleHoldCallAction` | `{ callId, hold }` | Hold state changed | +| `didPerformSetMutedCallAction` | `{ callId, muted }` | Mute state changed | +| `didReceiveStartCallAction` | `{ callId }` | Outgoing call action received | + +### Types + +```typescript +type EndCallReason = + | 'local' // Call ended by the local user (e.g., hanging up) + | 'remote' // Call ended by the remote party, or outgoing not answered + | 'rejected' // Call was rejected/declined + | 'busy' // Remote party was busy + | 'answeredElsewhere' // Answered on another device + | 'missed' // No response to an incoming call + | 'error' // Call failed due to an error (e.g., network issue) + | 'canceled' // Call canceled before the remote party could answer + | 'restricted' // Call restricted (e.g., airplane mode) + | 'unknown'; // Unknown or unspecified disconnect reason + +type CallingExpiOSOptions = { + appName: string; + supportsVideo?: boolean; + maximumCallsPerCallGroup?: number; + maximumCallGroups?: number; + handleType?: 'generic' | 'number' | 'phone' | 'email'; +}; + +type CallingExpAndroidOptions = { + incomingChannel?: { + id: string; + name: string; + sound?: string; + vibration?: boolean; + }; + outgoingChannel?: { + id: string; + name: string; + sound?: string; + vibration?: boolean; + }; +}; + +type PermissionsResult = { + recordAudio: boolean; + postNotifications: boolean; +}; +``` + +## Troubleshooting + +### iOS + +- **Incoming call not showing**: Ensure `voip` background mode is enabled and VoIP push certificate is configured +- **CallKit errors**: Check that `appName` is set in setup options +- **Audio issues**: The module automatically configures the audio session, but ensure no conflicts with other audio libraries + +### Android + +- **Notifications not showing**: Check POST_NOTIFICATIONS permission on Android 13+ +- **Call not answered on tap**: Ensure `handleCallingIntent` is called in both `onCreate` and `onNewIntent` in your MainActivity + +## Contributing + +See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow. + +## License + +MIT + +--- + +Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob) diff --git a/packages/react-native-callingx/android/build.gradle b/packages/react-native-callingx/android/build.gradle new file mode 100644 index 0000000000..5383b4184e --- /dev/null +++ b/packages/react-native-callingx/android/build.gradle @@ -0,0 +1,96 @@ +buildscript { + ext.getExtOrDefault = {name -> + return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['Callingx_' + name] + } + + repositories { + google() + mavenCentral() + } + + dependencies { + classpath "com.android.tools.build:gradle:8.7.2" + // noinspection DifferentKotlinGradleVersion + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}" + } +} + + +apply plugin: "com.android.library" +apply plugin: "kotlin-android" +apply plugin: "kotlin-parcelize" + +apply plugin: "com.facebook.react" + +def getExtOrIntegerDefault(name) { + return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["Callingx_" + name]).toInteger() +} + +def isNewArchitectureEnabled() { + return rootProject.hasProperty("newArchEnabled") && rootProject.newArchEnabled == "true" +} + +android { + namespace "io.getstream.rn.callingx" + + compileSdkVersion getExtOrIntegerDefault("compileSdkVersion") + + defaultConfig { + minSdkVersion getExtOrIntegerDefault("minSdkVersion") + targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") + } + + buildFeatures { + buildConfig true + } + + buildTypes { + release { + minifyEnabled false + } + } + + lint { + disable "GradleCompatible" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + + sourceSets { + main { + java.srcDirs += [ + "generated/java", + "generated/jni", + "build/generated/source/codegen/java", + "build/generated/source/codegen/jni", + ] + if (isNewArchitectureEnabled()) { + java.srcDirs += ["src/newarch"] + } else { + java.srcDirs += ["src/oldarch"] + } + } + } +} + +repositories { + mavenCentral() + google() +} + +def kotlin_version = getExtOrDefault("kotlinVersion") + +dependencies { + implementation "com.facebook.react:react-android" + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation "androidx.core:core-telecom:1.0.1" + // Compile-time only dependency so StreamMessagingService can reference RemoteMessage. + // The consuming app (via @react-native-firebase/messaging) must provide the actual runtime version. + compileOnly "com.google.firebase:firebase-messaging-ktx:24.1.2" + // Optional: use Firebase native code when the app has react-native-firebase installed (peer deps) + implementation project(':react-native-firebase_app') + implementation project(':react-native-firebase_messaging') +} diff --git a/packages/react-native-callingx/android/gradle.properties b/packages/react-native-callingx/android/gradle.properties new file mode 100644 index 0000000000..6567d6ce3d --- /dev/null +++ b/packages/react-native-callingx/android/gradle.properties @@ -0,0 +1,5 @@ +Callingx_kotlinVersion=2.0.21 +Callingx_minSdkVersion=24 +Callingx_targetSdkVersion=34 +Callingx_compileSdkVersion=35 +Callingx_ndkVersion=27.1.12297006 diff --git a/packages/react-native-callingx/android/src/main/AndroidManifest.xml b/packages/react-native-callingx/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..ec64fde05d --- /dev/null +++ b/packages/react-native-callingx/android/src/main/AndroidManifest.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/CallEventBroadcastReceiver.kt b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/CallEventBroadcastReceiver.kt new file mode 100644 index 0000000000..cd7bbe8dd1 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/CallEventBroadcastReceiver.kt @@ -0,0 +1,17 @@ +package io.getstream.rn.callingx + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Bundle + +class CallEventBroadcastReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + val action = intent.action ?: return + val extras = intent.extras ?: Bundle() + + CallEventBus.publish(CallEvent(action = action, extras = extras)) + } +} + diff --git a/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/CallRegistrationStore.kt b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/CallRegistrationStore.kt new file mode 100644 index 0000000000..f8db2871a9 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/CallRegistrationStore.kt @@ -0,0 +1,145 @@ +package io.getstream.rn.callingx + +import android.os.Handler +import android.os.Looper +import com.facebook.react.bridge.Promise +import io.getstream.rn.callingx.model.CallAction +import java.util.Collections +import java.util.concurrent.ConcurrentHashMap +import kotlin.collections.emptyList + +object CallRegistrationStore { + + private const val TAG = "[Callingx] CallRegistrationStore" + private const val DISPLAY_TIMEOUT_MS = 10_000L + + private val trackedCallIds: MutableSet = ConcurrentHashMap.newKeySet() + + /** Pending actions per callId, queued until the call is registered in Telecom. */ + private val pendingActionsByCallId = ConcurrentHashMap>() + + // Per-callId pending promises for displayIncomingCall awaiting CALL_REGISTERED_INCOMING_ACTION + private val pendingPromises = mutableMapOf() + private val pendingTimeouts = mutableMapOf() + private val mainHandler = Handler(Looper.getMainLooper()) + + fun trackCallRegistration(callId: String, promise: Promise?) { + debugLog( + TAG, + "[store] trackCallRegistration: Tracking call registration for callId: $callId" + ) + trackedCallIds.add(callId) + + if (promise == null) return + + synchronized(pendingPromises) { + // Cancel any existing timeout for this callId to avoid a stale runnable + // rejecting the new promise after it overwrites the old one. + pendingTimeouts.remove(callId)?.let { mainHandler.removeCallbacks(it) } + + pendingPromises[callId] = promise + + val timeoutRunnable = Runnable { + synchronized(pendingPromises) { + pendingPromises + .remove(callId) + ?.reject("TIMEOUT", "Timed out waiting for call registration: $callId") + pendingTimeouts.remove(callId) + trackedCallIds.remove(callId) + } + } + pendingTimeouts[callId] = timeoutRunnable + mainHandler.postDelayed(timeoutRunnable, DISPLAY_TIMEOUT_MS) + } + } + + fun onRegistrationSuccess(callId: String) { + synchronized(pendingPromises) { + pendingTimeouts.remove(callId)?.let { mainHandler.removeCallbacks(it) } + pendingPromises.remove(callId)?.resolve(true) + } + } + + fun onRegistrationFailed(callId: String) { + reportRegistrationFail( + callId, + "REGISTRATION_FAILED", + "Failed to register call with telecom: $callId", + null + ) + } + + fun reportRegistrationFail( + callId: String, + code: String, + message: String?, + throwable: Throwable? + ) { + trackedCallIds.remove(callId) + + synchronized(pendingPromises) { + pendingTimeouts.remove(callId)?.let { mainHandler.removeCallbacks(it) } + val promise = pendingPromises.remove(callId) + pendingActionsByCallId.remove(callId) + if (promise != null) { + if (throwable != null) { + promise.reject(code, message, throwable) + } else if (message != null) { + promise.reject(code, message) + } else { + promise.reject(code, "Unknown error") + } + } + } + } + + fun addTrackedCall(callId: String) { + debugLog(TAG, "[store] addTrackedCall: Adding tracked call: $callId") + trackedCallIds.add(callId) + } + + fun removeTrackedCall(callId: String) { + debugLog(TAG, "[store] removeTrackedCall: Removing tracked call: $callId") + trackedCallIds.remove(callId) + } + + fun isCallTracked(callId: String): Boolean { + val isTracked = trackedCallIds.contains(callId) + debugLog(TAG, "[store] isCallTracked: Is call $callId tracked: $isTracked") + return isTracked + } + + fun hasRegisteredCall(): Boolean { + return trackedCallIds.isNotEmpty() + } + + /** + * Queues an action for a call that is not yet registered. + * Pending actions are drained and executed once registration completes. + */ + fun addPendingAction(callId: String, action: CallAction) { + debugLog(TAG, "[store] addPendingAction: callId=$callId action=${action::class.simpleName}") + pendingActionsByCallId + .computeIfAbsent(callId) { Collections.synchronizedList(mutableListOf()) } + .add(action) + } + + /** + * Returns and removes all queued actions for this call. + * Used once a call is registered so the service can replay pending actions. + */ + fun takePendingActions(callId: String): List { + val list = pendingActionsByCallId.remove(callId) ?: return emptyList() + synchronized(list) { return list.toList() } + } + + fun clearAll() { + synchronized(pendingPromises) { + pendingTimeouts.values.forEach { mainHandler.removeCallbacks(it) } + pendingTimeouts.clear() + pendingPromises.clear() + } + trackedCallIds.clear() + pendingActionsByCallId.clear() + } +} diff --git a/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/CallService.kt b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/CallService.kt new file mode 100644 index 0000000000..27778022c3 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/CallService.kt @@ -0,0 +1,650 @@ +package io.getstream.rn.callingx + +import android.app.Notification +import android.app.Service +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.ServiceInfo +import android.net.Uri +import android.os.Binder +import android.os.Build +import android.os.Bundle +import android.os.IBinder +import android.telecom.DisconnectCause +import android.util.Log +import androidx.core.content.ContextCompat +import androidx.core.net.toUri +import io.getstream.rn.callingx.model.Call +import io.getstream.rn.callingx.model.CallAction +import io.getstream.rn.callingx.notifications.CallNotificationManager +import io.getstream.rn.callingx.notifications.NotificationChannelsManager +import io.getstream.rn.callingx.notifications.NotificationsConfig +import io.getstream.rn.callingx.repo.CallRepository +import io.getstream.rn.callingx.repo.CallRepositoryFactory +import io.getstream.rn.callingx.utils.SettingsStore +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch + +/** + * This service handles the app call logic (show notification, record mic, display audio, etc..). It + * can get started by the user or by an upcoming push notification to start a call. + * + * It holds the call scope used to register a call with the Telecom SDK in our + * TelecomCallRepository. + * + * When registering a call with the Telecom SDK and displaying a CallStyle notification, the SDK + * will grant you foreground service delegation so there is no need to make this a FGS. + * + * Note: you could potentially make this service run in a different process since audio or video + * calls can consume significant memory, although that would require more complex setup to make it + * work across multiple process. + */ +class CallService : Service(), CallRepository.Listener { + + companion object { + private const val TAG = "[Callingx] CallService" + + internal const val DEFAULT_DISPLAY_NAME = "Unknown Caller" + + internal const val EXTRA_CALL_ID = "extra_call_id" + internal const val EXTRA_NAME = "extra_name" + internal const val EXTRA_URI = "extra_uri" + internal const val EXTRA_IS_VIDEO = "extra_is_video" + internal const val EXTRA_DISPLAY_TITLE = "displayTitle" + internal const val EXTRA_DISPLAY_OPTIONS = "display_options" + internal const val EXTRA_ACTION = "action_name" + // Background task extras + internal const val EXTRA_TASK_NAME = "task_name" + internal const val EXTRA_TASK_DATA = "task_data" + internal const val EXTRA_TASK_TIMEOUT = "task_timeout" + + internal const val ACTION_INCOMING_CALL = "incoming_call" + internal const val ACTION_OUTGOING_CALL = "outgoing_call" + internal const val ACTION_UPDATE_CALL = "update_call" + internal const val ACTION_START_BACKGROUND_TASK = "start_background_task" + internal const val ACTION_STOP_BACKGROUND_TASK = "stop_background_task" + internal const val ACTION_STOP_SERVICE = "stop_service" + internal const val ACTION_PROCESS_ACTION = "execute_action" + internal const val ACTION_REGISTRATION_FAILED = "registration_failed" + + fun startIncomingCallFromPush(context: Context, data: Map) { + debugLog(TAG, "[service] startIncomingCallFromPush: Starting incoming call from push") + + // Check if we are allowed to post call notifications (moved from JS layer). + val notificationsConfig = NotificationsConfig.loadNotificationsConfig(context) + val notificationChannelsManager = + NotificationChannelsManager(context).apply { + setNotificationsConfig(notificationsConfig) + } + val notificationStatus = notificationChannelsManager.getNotificationStatus() + if (!notificationStatus.canPost) { + debugLog( + TAG, + "[service] startIncomingCallFromPush: Cannot post notifications, skipping incoming call" + ) + return + } + + val shouldRejectCallWhenBusy = SettingsStore.shouldRejectCallWhenBusy(context) + if (shouldRejectCallWhenBusy && CallRegistrationStore.hasRegisteredCall()) { + debugLog( + TAG, + "[service] startIncomingCallFromPush: Registered call found and rejectCallWhenBusy is enabled, skipping incoming call" + ) + return + } + + val callCid = data["call_cid"] + if (callCid.isNullOrEmpty()) { + debugLog( + TAG, + "[service] startIncomingCallFromPush: Call CID is null or empty, skipping" + ) + return + } + + val createdById = data["created_by_id"] + val createdName = data["created_by_display_name"].orEmpty() + val displayName = data["call_display_name"].orEmpty() + val callDisplayName = displayName.ifEmpty { createdName.ifEmpty { DEFAULT_DISPLAY_NAME } } + + val isVideo = data["video"] == "true" + + CallRegistrationStore.trackCallRegistration(callCid, null) + + val intent = + Intent(context, CallService::class.java).apply { + action = ACTION_INCOMING_CALL + putExtra(EXTRA_CALL_ID, callCid) + putExtra(EXTRA_URI, createdById?.toUri() ?: callDisplayName.toUri()) + putExtra(EXTRA_NAME, callDisplayName) + putExtra(EXTRA_IS_VIDEO, isVideo) + } + + ContextCompat.startForegroundService(context, intent) + } + } + + inner class CallServiceBinder : Binder() { + fun getService(): CallService = this@CallService + } + + private lateinit var headlessJSManager: HeadlessTaskManager + private lateinit var notificationManager: CallNotificationManager + private lateinit var callRepository: CallRepository + + private val binder = CallServiceBinder() + private val scope: CoroutineScope = CoroutineScope(SupervisorJob()) + private val actionProcessingLock = Object() + + private var isInForeground = false + + private val optimisticNotificationReceiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val callId = intent.getStringExtra(CallingxModuleImpl.EXTRA_CALL_ID) ?: return + when (intent.action) { + CallingxModuleImpl.CALL_OPTIMISTIC_ACCEPT_ACTION -> { + debugLog( + TAG, + "[service] optimisticReceiver: Optimistic accept for $callId" + ) + notificationManager.stopRingtone() + notificationManager.setOptimisticState( + callId, + CallNotificationManager.OptimisticState.ACCEPTING + ) + val call = callRepository.getCall(callId) + if (call != null) { + notificationManager.updateCallNotification(callId, call) + } + } + CallingxModuleImpl.CALL_END_ACTION -> { + val source = intent.getStringExtra(CallingxModuleImpl.EXTRA_SOURCE) + val cause = + intent.getStringExtra(CallingxModuleImpl.EXTRA_DISCONNECT_CAUSE) + val rejectedCause = + getDisconnectCauseString( + DisconnectCause(DisconnectCause.REJECTED) + ) + val call = callRepository.getCall(callId) + + val isSysSource = + source == CallRepository.EventSource.SYS.name.lowercase() + + // we handle optimistic updates only if incoming call (non-answered) was rejected within notification action + if (!isSysSource || + cause != rejectedCause || + call == null || + !call.isIncoming() || + call.isActive + ) { + debugLog( + TAG, + "[service] optimisticReceiver: Skipping optimistic reject for $callId" + ) + return + } + + debugLog( + TAG, + "[service] optimisticReceiver: Optimistic reject for $callId" + ) + notificationManager.stopRingtone() + notificationManager.setOptimisticState( + callId, + CallNotificationManager.OptimisticState.REJECTING + ) + notificationManager.updateCallNotification(callId, call) + } + } + } + } + + override fun onCreate() { + super.onCreate() + debugLog(TAG, "[service] onCreate: TelecomCallService created") + + notificationManager = CallNotificationManager(applicationContext) + headlessJSManager = HeadlessTaskManager(applicationContext) + callRepository = CallRepositoryFactory.create(applicationContext) + callRepository.setListener(this) + + val filter = + IntentFilter().apply { + addAction(CallingxModuleImpl.CALL_OPTIMISTIC_ACCEPT_ACTION) + addAction(CallingxModuleImpl.CALL_END_ACTION) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(optimisticNotificationReceiver, filter, Context.RECEIVER_NOT_EXPORTED) + } else { + @Suppress("UnspecifiedRegisterReceiverFlag") + registerReceiver(optimisticNotificationReceiver, filter) + } + } + + override fun onDestroy() { + super.onDestroy() + debugLog(TAG, "[service] onDestroy: TelecomCallService destroyed") + + unregisterReceiver(optimisticNotificationReceiver) + + notificationManager.cancelAllNotifications() + notificationManager.stopRingtone() + callRepository.release() + headlessJSManager.release() + + if (isInForeground) { + stopForeground(STOP_FOREGROUND_REMOVE) + isInForeground = false + } + + scope.cancel() + } + + override fun onTaskRemoved(rootIntent: Intent?) { + super.onTaskRemoved(rootIntent) + debugLog(TAG, "[service] onTaskRemoved: Task removed") + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + debugLog(TAG, "[service] onStartCommand: Received intent with action: ${intent?.action}") + + if (intent == null || intent.action == null) { + Log.w(TAG, "[service] onStartCommand: Intent is null, returning START_NOT_STICKY") + return START_NOT_STICKY + } + + when (intent.action) { + ACTION_INCOMING_CALL -> { + registerCall(intent, true) + } + ACTION_OUTGOING_CALL -> { + registerCall(intent, false) + } + ACTION_START_BACKGROUND_TASK -> { + startBackgroundTask(intent) + return START_NOT_STICKY + } + ACTION_STOP_BACKGROUND_TASK -> { + stopBackgroundTask() + return START_NOT_STICKY + } + ACTION_UPDATE_CALL -> { + updateCall(intent) + } + ACTION_PROCESS_ACTION -> { + processAction(intent) + } + ACTION_STOP_SERVICE -> { + if (isInForeground) { + stopForeground(STOP_FOREGROUND_REMOVE) + isInForeground = false + } + notificationManager.cancelAllNotifications() + notificationManager.stopRingtone() + stopSelf() + } + else -> { + Log.e(TAG, "[service] onStartCommand: Unknown action: ${intent.action}") + stopSelf() + return START_NOT_STICKY + } + } + + return START_STICKY + } + + override fun onBind(intent: Intent): IBinder? = binder + + override fun onUnbind(intent: Intent): Boolean { + debugLog(TAG, "[service] onUnbind: Service unbound") + return super.onUnbind(intent) + } + + override fun onCallStateChanged(callId: String, call: Call) { + debugLog( + TAG, + "[service] onCallStateChanged[$callId]: Call state changed: ${call::class.simpleName}" + ) + when (call) { + is Call.Registered -> { + debugLog( + TAG, + "[service] onCallStateChanged[$callId]: Call registered - Active: ${call.isActive}, OnHold: ${call.isOnHold}, Muted: ${call.isMuted}" + ) + + val shouldStopExecution = processPendingActions(call) + if (shouldStopExecution) { + return + } + + if (call.isIncoming()) { + // Play ringtone only if there is no active call + if (!call.isActive && !callRepository.hasActiveCall(excludeCallId = callId)) { + notificationManager.startRingtone() + } else { + notificationManager.stopRingtone() + } + } + // Update the call notification + val notificationId = notificationManager.getOrCreateNotificationId(callId) + if (isInForeground) { + notificationManager.updateCallNotification(callId, call) + } else { + debugLog( + TAG, + "[service] onCallStateChanged[$callId]: Starting foreground for call" + ) + notificationManager.resetOptimisticState(callId) + val notification = notificationManager.createNotification(callId, call) + startForegroundSafely(notificationId, notification) + } + } + is Call.None, is Call.Unregistered -> { + repromoteForegroundIfNeeded(callId) + if (!callRepository.hasRingingCall()) notificationManager.stopRingtone() + + // Stop service only when no calls remain + if (!callRepository.hasAnyCalls()) { + debugLog( + TAG, + "[service] onCallStateChanged[$callId]: No more calls, stopping service" + ) + if (isInForeground) { + stopForeground(STOP_FOREGROUND_REMOVE) + isInForeground = false + } + stopSelf() + } + } + } + } + + override fun onIsCallAnswered(callId: String, source: CallRepository.EventSource) { + sendBroadcastEvent(CallingxModuleImpl.CALL_ANSWERED_ACTION) { + putExtra(CallingxModuleImpl.EXTRA_CALL_ID, callId) + putExtra(CallingxModuleImpl.EXTRA_SOURCE, source.name.lowercase()) + } + } + + override fun onIsCallDisconnected( + callId: String?, + cause: DisconnectCause, + source: CallRepository.EventSource + ) { + sendBroadcastEvent(CallingxModuleImpl.CALL_END_ACTION) { + if (callId != null) { + putExtra(CallingxModuleImpl.EXTRA_CALL_ID, callId) + } + putExtra(CallingxModuleImpl.EXTRA_DISCONNECT_CAUSE, getDisconnectCauseString(cause)) + putExtra(CallingxModuleImpl.EXTRA_SOURCE, source.name.lowercase()) + } + } + + override fun onIsCallInactive(callId: String) { + sendBroadcastEvent(CallingxModuleImpl.CALL_INACTIVE_ACTION) { + putExtra(CallingxModuleImpl.EXTRA_CALL_ID, callId) + } + } + + override fun onIsCallActive(callId: String) { + sendBroadcastEvent(CallingxModuleImpl.CALL_ACTIVE_ACTION) { + putExtra(CallingxModuleImpl.EXTRA_CALL_ID, callId) + } + } + + override fun onCallRegistered(callId: String, incoming: Boolean) { + if (incoming) { + sendBroadcastEvent(CallingxModuleImpl.CALL_REGISTERED_INCOMING_ACTION) { + putExtra(CallingxModuleImpl.EXTRA_CALL_ID, callId) + } + } else { + sendBroadcastEvent(CallingxModuleImpl.CALL_REGISTERED_ACTION) { + putExtra(CallingxModuleImpl.EXTRA_CALL_ID, callId) + } + } + } + + override fun onMuteCallChanged(callId: String, isMuted: Boolean) { + sendBroadcastEvent(CallingxModuleImpl.CALL_MUTED_ACTION) { + putExtra(CallingxModuleImpl.EXTRA_CALL_ID, callId) + putExtra(CallingxModuleImpl.EXTRA_MUTED, isMuted) + } + } + + override fun onCallEndpointChanged(callId: String, endpoint: String) { + sendBroadcastEvent(CallingxModuleImpl.CALL_ENDPOINT_CHANGED_ACTION) { + putExtra(CallingxModuleImpl.EXTRA_CALL_ID, callId) + putExtra(CallingxModuleImpl.EXTRA_AUDIO_ENDPOINT, endpoint) + } + } + + fun processAction(intent: Intent) { + val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: return + val action = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra(EXTRA_ACTION, CallAction::class.java) + } else { + @Suppress("DEPRECATION") intent.getParcelableExtra(EXTRA_ACTION) + } ?: return + + processAction(callId, action) + } + + fun processAction(callId: String, action: CallAction) { + debugLog( + TAG, + "[service] processAction[$callId]: Processing action: ${action::class.simpleName}" + ) + synchronized(actionProcessingLock) { + val call = callRepository.getCall(callId) + if (call != null && !call.isPending) { + call.processAction(action) + } else { + // this solves race condition, when action is requested before the call is + // registered in Telecom + debugLog( + TAG, + "[service] processAction: Add pending action for ${call?.id} to queue" + ) + CallRegistrationStore.addPendingAction(callId, action) + } + } + } + + fun startBackgroundTask(intent: Intent) { + val taskName = intent.getStringExtra(EXTRA_TASK_NAME)!! + val data = intent.getBundleExtra(EXTRA_TASK_DATA)!! + val timeout = intent.getLongExtra(EXTRA_TASK_TIMEOUT, 0) + headlessJSManager.startHeadlessTask(taskName, data, timeout) + } + + fun stopBackgroundTask() { + headlessJSManager.stopHeadlessTask() + } + + private fun registerCall(intent: Intent, incoming: Boolean) { + debugLog(TAG, "[service] registerCall: ${if (incoming) "in" else "out"} call") + + val callInfo = extractIntentParams(intent) + + // If this specific call is already registered, just notify + val existingCall = callRepository.getCall(callInfo.callId) + if (existingCall != null) { + Log.w( + TAG, + "[service] registerCall: Call ${callInfo.callId} already registered, notifying" + ) + if (incoming) { + sendBroadcastEvent(CallingxModuleImpl.CALL_REGISTERED_INCOMING_ACTION) { + putExtra(CallingxModuleImpl.EXTRA_CALL_ID, callInfo.callId) + } + } else { + sendBroadcastEvent(CallingxModuleImpl.CALL_REGISTERED_ACTION) { + putExtra(CallingxModuleImpl.EXTRA_CALL_ID, callInfo.callId) + } + } + return + } + + startForegroundForCall(callInfo, incoming) + + scope.launch { + try { + callRepository.registerCall( + callInfo.callId, + callInfo.name, + callInfo.uri, + incoming, + callInfo.isVideo, + callInfo.displayOptions, + ) + } catch (e: Exception) { + Log.e(TAG, "[service] registerCall: Error registering call: ${e.message}") + + sendBroadcastEvent(CallingxModuleImpl.CALL_REGISTRATION_FAILED_ACTION) { + putExtra(CallingxModuleImpl.EXTRA_CALL_ID, callInfo.callId) + } + + repromoteForegroundIfNeeded(callInfo.callId) + + // Only stop foreground/service when no other calls remain + if (!callRepository.hasAnyCalls()) { + if (isInForeground) { + stopForeground(STOP_FOREGROUND_REMOVE) + isInForeground = false + } + notificationManager.stopRingtone() + stopSelf() + } + } + } + } + + private fun processPendingActions(call: Call.Registered): Boolean { + synchronized(actionProcessingLock) { + val pendingActions = CallRegistrationStore.takePendingActions(call.id) + + val disconnectAction = pendingActions.find { it is CallAction.Disconnect } + if (disconnectAction != null) { + // if queue contains Disconnect, execute it and ignore rest of the queue + debugLog(TAG, "[service] processPendingActions: Executing pending disconnect for ${call.id}") + call.processAction(disconnectAction) + return true + } + + // process pending actions in the order they were added + for (action in pendingActions) { + call.processAction(action) + debugLog( + TAG, + "[service] processPendingActions: Executing pending action: $action for ${call.id}" + ) + } + + return false + } + } + + private fun startForegroundSafely(notificationId: Int, notification: Notification) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startForeground( + notificationId, + notification, + ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL + ) + } else { + startForeground(notificationId, notification) + } + isInForeground = true + } catch (e: Exception) { + Log.e( + TAG, + "[service] startForegroundSafely: Failed to start foreground service: ${e.message}", + e + ) + } + } + + /** + * Cancels the notification for [callId]. If that notification was the foreground one + * and other calls remain, re-promotes the service with the next call's notification. + */ + private fun repromoteForegroundIfNeeded(callId: String) { + val newForegroundNotificationId = notificationManager.cancelNotification(callId) + if (newForegroundNotificationId != null && isInForeground) { + val newForegroundCallId = notificationManager.getForegroundCallId() + val call = if (newForegroundCallId != null) callRepository.getCall(newForegroundCallId) else null + if (call != null && newForegroundCallId != null) { + debugLog(TAG, "[service] repromoteForegroundIfNeeded: Re-promoting with call $newForegroundCallId (notificationId=$newForegroundNotificationId)") + val notification = notificationManager.createNotification(newForegroundCallId, call) + startForegroundSafely(newForegroundNotificationId, notification) + } + } + } + + private fun startForegroundForCall(callInfo: CallInfo, incoming: Boolean) { + val tempCall = callRepository.getTempCall(callInfo, incoming) + val notificationId = notificationManager.getOrCreateNotificationId(callInfo.callId) + if (!isInForeground) { + debugLog( + TAG, + "[service] registerCall: Starting foreground for call: ${callInfo.callId}" + ) + val notification = notificationManager.createNotification(callInfo.callId, tempCall) + startForegroundSafely(notificationId, notification) + } else { + // Already in foreground from another call — just post the notification + val notification = notificationManager.createNotification(callInfo.callId, tempCall) + notificationManager.postNotification(callInfo.callId, notification) + } + } + + private fun updateCall(intent: Intent) { + val callInfo = extractIntentParams(intent) + callRepository.updateCall( + callInfo.callId, + callInfo.name, + callInfo.uri, + callInfo.isVideo, + callInfo.displayOptions + ) + } + + private fun extractIntentParams(intent: Intent): CallInfo { + val callId = intent.getStringExtra(EXTRA_CALL_ID)!! + val name = intent.getStringExtra(EXTRA_NAME)!! + val uri = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra(EXTRA_URI, Uri::class.java)!! + } else { + @Suppress("DEPRECATION") intent.getParcelableExtra(EXTRA_URI)!! + } + val isVideo = intent.getBooleanExtra(EXTRA_IS_VIDEO, false) + val displayOptions = intent.getBundleExtra(EXTRA_DISPLAY_OPTIONS) + + return CallInfo(callId, name, uri, isVideo, displayOptions) + } + + private fun sendBroadcastEvent(action: String, applyParams: Intent.() -> Unit = {}) { + val intent = + Intent(action).apply { + setPackage(packageName) + applyParams(this) + } + sendBroadcast(intent) + } + + data class CallInfo( + val callId: String, + val name: String, + val uri: Uri, + val isVideo: Boolean, + val displayOptions: Bundle?, + ) +} diff --git a/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/CallingxEventEmitterAdapter.kt b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/CallingxEventEmitterAdapter.kt new file mode 100644 index 0000000000..37c50974df --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/CallingxEventEmitterAdapter.kt @@ -0,0 +1,7 @@ +package io.getstream.rn.callingx + +import com.facebook.react.bridge.WritableMap + +interface CallingxEventEmitterAdapter { + fun emitNewEvent(value: WritableMap) +} diff --git a/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/CallingxModuleImpl.kt b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/CallingxModuleImpl.kt new file mode 100644 index 0000000000..f47188c861 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/CallingxModuleImpl.kt @@ -0,0 +1,493 @@ +package io.getstream.rn.callingx + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Build +import android.os.Bundle +import android.telecom.DisconnectCause +import android.util.Log +import androidx.core.content.ContextCompat +import androidx.core.net.toUri +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.LifecycleEventListener +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.WritableArray +import com.facebook.react.bridge.WritableMap +import com.facebook.react.bridge.WritableNativeArray +import com.facebook.react.modules.core.DeviceEventManagerModule +import io.getstream.rn.callingx.model.CallAction +import io.getstream.rn.callingx.notifications.NotificationChannelsManager +import io.getstream.rn.callingx.notifications.NotificationsConfig +import io.getstream.rn.callingx.utils.SettingsStore + +class CallingxModuleImpl( + private val reactApplicationContext: ReactApplicationContext, + private val eventEmitter: CallingxEventEmitterAdapter +) : CallEventBus.Listener { + + companion object { + const val TAG = "[Callingx] CallingxModule" + const val NAME = "Callingx" + + const val EXTRA_CALL_ID = "call_id" + const val EXTRA_MUTED = "is_muted" + const val EXTRA_ON_HOLD = "hold" + const val EXTRA_DISCONNECT_CAUSE = "disconnect_cause" + const val EXTRA_AUDIO_ENDPOINT = "audio_endpoint" + const val EXTRA_SOURCE = "source" + const val EXTRA_ACTION = "action_name" + + // Action names must match intent-filter entries in AndroidManifest.xml + const val CALL_REGISTERED_ACTION = "io.getstream.CALL_REGISTERED" + const val CALL_REGISTERED_INCOMING_ACTION = "io.getstream.CALL_REGISTERED_INCOMING" + const val CALL_ANSWERED_ACTION = "io.getstream.CALL_ANSWERED" + const val CALL_INACTIVE_ACTION = "io.getstream.CALL_INACTIVE" + const val CALL_ACTIVE_ACTION = "io.getstream.CALL_ACTIVE" + const val CALL_MUTED_ACTION = "io.getstream.CALL_MUTED" + const val CALL_ENDPOINT_CHANGED_ACTION = "io.getstream.CALL_ENDPOINT_CHANGED" + const val CALL_END_ACTION = "io.getstream.CALL_END" + const val CALL_REGISTRATION_FAILED_ACTION = "io.getstream.CALL_REGISTRATION_FAILED" + const val CALL_OPTIMISTIC_ACCEPT_ACTION = "io.getstream.ACCEPT_CALL_OPTIMISTIC" + // Background task name + const val HEADLESS_TASK_NAME = "HandleCallBackgroundState" + } + + private var delayedEvents = WritableNativeArray() + private var isModuleInitialized = false + private var canSendEvents = false + + private val notificationChannelsManager = NotificationChannelsManager(reactApplicationContext) + + init { + CallEventBus.subscribe(this) + } + + fun initialize() { + debugLog(TAG, "[module] initialize: Initializing module") + } + + fun invalidate() { + debugLog(TAG, "[module] invalidate: Invalidating module") + + CallRegistrationStore.clearAll() + CallEventBus.unsubscribe(this) + + isModuleInitialized = false + } + + fun setShouldRejectCallWhenBusy(shouldReject: Boolean) { + debugLog( + TAG, + "[module] setShouldRejectCallWhenBusy: Updating rejectCallWhenBusy to $shouldReject" + ) + SettingsStore.setShouldRejectCallWhenBusy(reactApplicationContext, shouldReject) + } + + fun setupAndroid(options: ReadableMap) { + debugLog(TAG, "[module] setupAndroid: Setting up Android: $options") + val notificationsConfig = + NotificationsConfig.saveNotificationsConfig(reactApplicationContext, options) + notificationChannelsManager.setNotificationsConfig(notificationsConfig) + notificationChannelsManager.createNotificationChannels() + + val notificationTexts = options.getMap("notificationTexts") + if (notificationTexts != null) { + val acceptingText = notificationTexts.getString("accepting") + val rejectingText = notificationTexts.getString("rejecting") + debugLog(TAG, "[module] $acceptingText $rejectingText") + SettingsStore.setOptimisticTexts( + reactApplicationContext, + acceptingText, + rejectingText, + ) + } + + isModuleInitialized = true + } + + fun canPostNotifications(): Boolean { + return notificationChannelsManager.getNotificationStatus().canPost + } + + fun stopService(promise: Promise) { + debugLog(TAG, "[module] stopService: Stopping CallService explicitly from JS") + try { + Intent(reactApplicationContext, CallService::class.java) + .apply { action = CallService.ACTION_STOP_SERVICE } + .also { reactApplicationContext.startService(it) } + + promise.resolve(true) + } catch (e: Exception) { + Log.e(TAG, "[module] stopService: Failed to stop service: ${e.message}", e) + promise.reject("STOP_SERVICE_ERROR", e.message, e) + } + } + + fun getInitialEvents(): WritableArray { + CallEventBus.drainPendingEvents().forEach { onCallEvent(it) } + + // NOTE: writable native array can be consumed only once, think of getting rid from clear + // event and clear it immediately after getting initial events + val events = delayedEvents + debugLog(TAG, "[module] getInitialEvents: Getting initial events: $events") + delayedEvents = WritableNativeArray() + canSendEvents = true + CallEventBus.markJsReady() + return events + } + + fun setCurrentCallActive(callId: String, promise: Promise) { + debugLog(TAG, "[module] activateCall: Activating call: $callId") + executeServiceAction(callId, CallAction.Activate, promise) + } + + fun displayIncomingCall( + callId: String, + phoneNumber: String, + callerName: String, + hasVideo: Boolean, + displayOptions: ReadableMap?, + promise: Promise + ) { + debugLog( + TAG, + "[module] displayIncomingCall: Displaying incoming call: $callId, $phoneNumber, $callerName, $hasVideo" + ) + if (!notificationChannelsManager.getNotificationStatus().canPost) { + promise.reject("ERROR", "Cannot post notifications") + return + } + + CallRegistrationStore.trackCallRegistration(callId, promise) + + try { + startCallService( + CallService.ACTION_INCOMING_CALL, + callId, + callerName, + phoneNumber, + hasVideo, + displayOptions + ) + } catch (e: Exception) { + Log.e(TAG, "[module] displayIncomingCall: Failed to start foreground service: ${e.message}", e) + CallRegistrationStore.reportRegistrationFail( + callId, + "START_FOREGROUND_SERVICE_ERROR", + e.message, + e + ) + } + } + + fun answerIncomingCall(callId: String, promise: Promise) { + debugLog(TAG, "[module] answerIncomingCall: Answering call: $callId") + // TODO: get the call type from the call attributes + val isAudioCall = true // TODO: get the call type from the call attributes + // registeredCall.callAttributes.callType == + // CallAttributesCompat.CALL_TYPE_AUDIO_CALL + // currentCall?.processAction(TelecomCallAction.Answer(isAudioCall)) + executeServiceAction(callId, CallAction.Answer(isAudioCall), promise) + } + + fun startCall( + callId: String, + phoneNumber: String, + callerName: String, + hasVideo: Boolean, + displayOptions: ReadableMap?, + promise: Promise + ) { + debugLog( + TAG, + "[module] startCall: Starting outgoing call: $callId, $phoneNumber, $callerName, $hasVideo, $displayOptions" + ) + if (!notificationChannelsManager.getNotificationStatus().canPost) { + promise.reject("ERROR", "Cannot post notifications") + return + } + + CallRegistrationStore.trackCallRegistration(callId, promise) + + try { + startCallService( + CallService.ACTION_OUTGOING_CALL, + callId, + callerName, + phoneNumber, + hasVideo, + displayOptions + ) + } catch (e: Exception) { + Log.e(TAG, "[module] startCall: Failed to start foreground service: ${e.message}", e) + CallRegistrationStore.reportRegistrationFail( + callId, + "START_FOREGROUND_SERVICE_ERROR", + e.message, + e + ) + } + } + + fun updateDisplay( + callId: String, + phoneNumber: String, + callerName: String, + displayOptions: ReadableMap?, + promise: Promise + ) { + debugLog(TAG, "[module] updateDisplay: Updating display: $callId, $phoneNumber, $callerName") + if (!notificationChannelsManager.getNotificationStatus().canPost) { + promise.reject("ERROR", "Cannot post notifications") + return + } + + // for now only display options will be updated, rest of the parameters will be ignored + try { + startCallService( + CallService.ACTION_UPDATE_CALL, + callId, + callerName, + phoneNumber, + true, + displayOptions, + ) + promise.resolve(true) + } catch (e: Exception) { + Log.e(TAG, "[module] updateDisplay: Failed to start foreground service: ${e.message}", e) + promise.reject("START_FOREGROUND_SERVICE_ERROR", e.message, e) + } + } + + fun endCallWithReason(callId: String, reason: Double, promise: Promise) { + debugLog(TAG, "[module] endCallWithReason: Ending call: $callId, $reason") + CallRegistrationStore.removeTrackedCall(callId) + val action = CallAction.Disconnect(DisconnectCause(reason.toInt())) + executeServiceAction(callId, action, promise) + } + + fun endCall(callId: String, promise: Promise) { + debugLog(TAG, "[module] endCall: Ending call: $callId") + CallRegistrationStore.removeTrackedCall(callId) + val action = CallAction.Disconnect(DisconnectCause(DisconnectCause.LOCAL)) + executeServiceAction(callId, action, promise) + } + + fun isCallTracked(callId: String): Boolean { + return CallRegistrationStore.isCallTracked(callId) + } + + fun hasRegisteredCall(): Boolean { + return CallRegistrationStore.hasRegisteredCall() + } + + fun setMutedCall(callId: String, isMuted: Boolean, promise: Promise) { + debugLog(TAG, "[module] setMutedCall: Setting muted call: $callId, $isMuted") + val action = CallAction.ToggleMute(isMuted) + executeServiceAction(callId, action, promise) + } + + fun setOnHoldCall(callId: String, isOnHold: Boolean, promise: Promise) { + debugLog(TAG, "[module] setOnHoldCall: Setting on hold call: $callId, $isOnHold") + val action = if (isOnHold) CallAction.Hold else CallAction.Activate + executeServiceAction(callId, action, promise) + } + + fun startBackgroundTask(taskName: String, timeout: Double, promise: Promise) { + try { + Intent(reactApplicationContext, CallService::class.java) + .apply { + this.action = CallService.ACTION_START_BACKGROUND_TASK + putExtra(CallService.EXTRA_TASK_NAME, taskName) + putExtra(CallService.EXTRA_TASK_DATA, Bundle()) + putExtra(CallService.EXTRA_TASK_TIMEOUT, timeout.toLong()) + } + .also { reactApplicationContext.startService(it) } + + promise.resolve(true) + } catch (e: Exception) { + Log.e(TAG, "[module] startBackgroundTask: Failed to start service: ${e.message}", e) + promise.reject("START_SERVICE_ERROR", e.message, e) + } + } + + fun stopBackgroundTask(taskName: String, promise: Promise) { + try { + Intent(reactApplicationContext, CallService::class.java) + .apply { + this.action = CallService.ACTION_STOP_BACKGROUND_TASK + putExtra(CallService.EXTRA_TASK_NAME, taskName) + } + .also { reactApplicationContext.startService(it) } + + promise.resolve(true) + } catch (e: Exception) { + Log.e(TAG, "[module] stopBackgroundTask: Failed to start service: ${e.message}", e) + promise.reject("START_SERVICE_ERROR", e.message, e) + } + } + + fun registerBackgroundTaskAvailable() { + debugLog(TAG, "[module] registerBackgroundTaskAvailable: Headless task registered") + } + + + fun fulfillAnswerCallAction(callId: String, didFail: Boolean) { + // no-op: Android Telecom doesn't require explicit action fulfillment + } + + fun fulfillEndCallAction(callId: String, didFail: Boolean) { + // no-op: Android Telecom doesn't require explicit action fulfillment + } + + fun log(message: String, level: String) { + when (level) { + "debug" -> debugLog(TAG, "[module] log: $message") + "info" -> Log.i(TAG, "[module] log: $message") + "warn" -> Log.w(TAG, "[module] log: $message") + "error" -> Log.e(TAG, "[module] log: $message") + } + } + + private fun startCallService( + action: String, + callId: String, + callerName: String, + phoneNumber: String, + hasVideo: Boolean, + displayOptions: ReadableMap? + ) { + Intent(reactApplicationContext, CallService::class.java) + .apply { + this.action = action + putExtra(CallService.EXTRA_CALL_ID, callId) + putExtra(CallService.EXTRA_NAME, callerName) + putExtra(CallService.EXTRA_URI, phoneNumber.toUri()) + putExtra(CallService.EXTRA_IS_VIDEO, hasVideo) + putExtra(CallService.EXTRA_DISPLAY_OPTIONS, Arguments.toBundle(displayOptions)) + } + .also { ContextCompat.startForegroundService(reactApplicationContext, it) } + } + + private fun executeServiceAction(callId: String, action: CallAction, promise: Promise) { + debugLog(TAG, "[module] executeServiceAction: Executing service action: $action") + Intent(reactApplicationContext, CallService::class.java) + .apply { + this.action = CallService.ACTION_PROCESS_ACTION + putExtra(CallService.EXTRA_CALL_ID, callId) + putExtra(CallService.EXTRA_ACTION, action) + } + .also { reactApplicationContext.startService(it) } + .also { promise.resolve(true) } + } + + private fun sendJSEvent(eventName: String, params: WritableMap? = null) { + if (isModuleInitialized && reactApplicationContext.hasActiveReactInstance() && canSendEvents + ) { + val paramsMap = + Arguments.createMap().apply { + params?.let { + it.toHashMap().forEach { key, value -> + if (value is Boolean) { + putBoolean(key, value) + } else { + putString(key, value.toString()) + } + } + } + } + reactApplicationContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit(eventName, params) + + val value = + Arguments.createMap().apply { + putString("eventName", eventName) + putMap("params", paramsMap) + } + eventEmitter.emitNewEvent(value) + } else { + debugLog(TAG, "[module] sendJSEvent: Queueing event: $eventName, $params") + Arguments.createMap() + .apply { + putString("eventName", eventName) + putMap("params", params) + } + .also { delayedEvents.pushMap(it) } + } + } + + override fun onCallEvent(event: CallEvent) { + val action = event.action + val extras = event.extras + val callId = extras.getString(EXTRA_CALL_ID) + + val params = Arguments.createMap() + if (callId != null) { + params.putString("callId", callId) + } + + when (action) { + CALL_REGISTERED_ACTION -> { + sendJSEvent("didReceiveStartCallAction", params) + if (callId != null) { + CallRegistrationStore.onRegistrationSuccess(callId) + } + } + CALL_REGISTERED_INCOMING_ACTION -> { + if (callId != null) { + CallRegistrationStore.onRegistrationSuccess(callId) + } + sendJSEvent("didDisplayIncomingCall", params) + } + CALL_REGISTRATION_FAILED_ACTION -> { + if (callId != null) { + CallRegistrationStore.onRegistrationFailed(callId) + } + } + CALL_ANSWERED_ACTION -> { + if (extras.containsKey(EXTRA_SOURCE)) { + params.putString("source", extras.getString(EXTRA_SOURCE)) + } + sendJSEvent("answerCall", params) + } + CALL_END_ACTION -> { + val source = extras.getString(EXTRA_SOURCE) + if (source != null) { + params.putString("source", source) + } + if (source == "app") { + if (callId != null) { + CallRegistrationStore.removeTrackedCall(callId) + } + } + params.putString("cause", extras.getString(EXTRA_DISCONNECT_CAUSE)) + sendJSEvent("endCall", params) + } + CALL_INACTIVE_ACTION -> { + params.putBoolean("hold", true) + sendJSEvent("didToggleHoldCallAction", params) + } + CALL_ACTIVE_ACTION -> { + params.putBoolean("hold", false) + sendJSEvent("didToggleHoldCallAction", params) + } + CALL_MUTED_ACTION -> { + if (extras.containsKey(EXTRA_MUTED)) { + params.putBoolean("muted", extras.getBoolean(EXTRA_MUTED, false)) + } + sendJSEvent("didPerformSetMutedCallAction", params) + } + CALL_ENDPOINT_CHANGED_ACTION -> { + if (extras.containsKey(EXTRA_AUDIO_ENDPOINT)) { + params.putString("output", extras.getString(EXTRA_AUDIO_ENDPOINT)) + } + sendJSEvent("didChangeAudioRoute", params) + } + } + } +} diff --git a/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/HeadlessTaskManager.kt b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/HeadlessTaskManager.kt new file mode 100644 index 0000000000..7d61a21153 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/HeadlessTaskManager.kt @@ -0,0 +1,164 @@ +package io.getstream.rn.callingx + +import android.content.Context +import android.os.Bundle +import android.util.Log +import com.facebook.react.ReactApplication +import com.facebook.react.ReactHost +import com.facebook.react.ReactInstanceEventListener +import com.facebook.react.ReactNativeHost +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.ReactContext +import com.facebook.react.bridge.UiThreadUtil +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags +import com.facebook.react.jstasks.HeadlessJsTaskConfig +import com.facebook.react.jstasks.HeadlessJsTaskContext +import com.facebook.react.jstasks.HeadlessJsTaskEventListener + +class HeadlessTaskManager(private val context: Context) : HeadlessJsTaskEventListener { + + private var activeTaskId: Int? = null + + companion object { + private const val TAG = "[Callingx] HeadlessTaskManager" + } + + public fun startHeadlessTask(taskName: String, data: Bundle, timeout: Long) { + debugLog(TAG, "[headless] startHeadlessTask: Starting headless task: $taskName, $data, $timeout") + if (activeTaskId != null) { + Log.w(TAG, "[headless] startHeadlessTask: Task already starting or active, ignoring new task request") + return + } + + if (UiThreadUtil.isOnUiThread()) { + startTask(HeadlessJsTaskConfig(taskName, Arguments.fromBundle(data), timeout, true)) + } else { + UiThreadUtil.runOnUiThread( + Runnable { + startTask(HeadlessJsTaskConfig(taskName, Arguments.fromBundle(data), timeout, true)) + } + ) + } + } + + public fun stopHeadlessTask() { + debugLog(TAG, "[headless] stopHeadlessTask: Stopping headless task") + activeTaskId?.let { taskId -> + if (UiThreadUtil.isOnUiThread()) { + stopTask(taskId) + } else { + UiThreadUtil.runOnUiThread(Runnable { stopTask(taskId) }) + } + } + } + + protected fun startTask(taskConfig: HeadlessJsTaskConfig) { + UiThreadUtil.assertOnUiThread() + + val context = reactContext + if (context == null) { + createReactContextAndScheduleTask(taskConfig) + } else { + invokeStartTask(context, taskConfig) + } + } + + private fun invokeStartTask(reactContext: ReactContext, taskConfig: HeadlessJsTaskConfig) { + debugLog(TAG, "[headless] invokeStartTask: Invoking start task") + val headlessJsTaskContext = HeadlessJsTaskContext.getInstance(reactContext) + headlessJsTaskContext.addTaskEventListener(this) + + UiThreadUtil.runOnUiThread { + val taskId = headlessJsTaskContext.startTask(taskConfig) + activeTaskId = taskId + } + } + + private fun stopTask(taskId: Int) { + reactContext?.let { context -> + val headlessJsTaskContext = HeadlessJsTaskContext.getInstance(context) + if (headlessJsTaskContext.isTaskRunning(taskId)) { + headlessJsTaskContext.finishTask(taskId) + debugLog(TAG, "Stopped task: $taskId") + } + } + } + + fun release() { + stopHeadlessTask() + + reactContext?.let { context -> + val headlessJsTaskContext = HeadlessJsTaskContext.getInstance(context) + headlessJsTaskContext.removeTaskEventListener(this) + } + } + + override fun onHeadlessJsTaskStart(taskId: Int) { + debugLog(TAG, "[headless] onHeadlessJsTaskStart: Task started: $taskId") + } + + override fun onHeadlessJsTaskFinish(taskId: Int) { + debugLog(TAG, "[headless] onHeadlessJsTaskFinish: Task finished: $taskId") + activeTaskId = null + } + + /** + * Get the [ReactNativeHost] used by this app. By default, assumes [getApplication] is an instance + * of [ReactApplication] and calls [ReactApplication.reactNativeHost]. + * + * Override this method if your application class does not implement `ReactApplication` or you + * simply have a different mechanism for storing a `ReactNativeHost`, e.g. as a static field + * somewhere. + */ + @Suppress("DEPRECATION") + protected open val reactNativeHost: ReactNativeHost + get() = (context.applicationContext as ReactApplication).reactNativeHost + + /** + * Get the [ReactHost] used by this app. By default, assumes [getApplication] is an instance of + * [ReactApplication] and calls [ReactApplication.reactHost]. This method assumes it is called in + * new architecture and returns null if not. + */ + protected open val reactHost: ReactHost? + get() = (context.applicationContext as ReactApplication).reactHost + + protected val reactContext: ReactContext? + get() { + if (ReactNativeFeatureFlags.enableBridgelessArchitecture()) { + val reactHost = + checkNotNull(reactHost) { "ReactHost is not initialized in New Architecture" } + return reactHost.currentReactContext + } else { + val reactInstanceManager = reactNativeHost.reactInstanceManager + return reactInstanceManager.currentReactContext + } + } + + private fun createReactContextAndScheduleTask(taskConfig: HeadlessJsTaskConfig) { + if (ReactNativeFeatureFlags.enableBridgelessArchitecture()) { + val reactHost = checkNotNull(reactHost) + reactHost.addReactInstanceEventListener( + object : ReactInstanceEventListener { + override fun onReactContextInitialized(context: ReactContext) { + debugLog(TAG, "createReactContextAndScheduleTask: React context initialized") + invokeStartTask(context, taskConfig) + reactHost.removeReactInstanceEventListener(this) + } + } + ) + reactHost.start() + } else { + val reactInstanceManager = reactNativeHost.reactInstanceManager + reactInstanceManager.addReactInstanceEventListener( + object : ReactInstanceEventListener { + override fun onReactContextInitialized(context: ReactContext) { + debugLog(TAG, "createReactContextAndScheduleTask: React context initialized") + invokeStartTask(context, taskConfig) + reactInstanceManager.removeReactInstanceEventListener(this) + } + } + ) + reactInstanceManager.createReactContextInBackground() + } + } +} diff --git a/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/StreamMessagingService.kt b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/StreamMessagingService.kt new file mode 100644 index 0000000000..3f9dbd7b05 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/StreamMessagingService.kt @@ -0,0 +1,48 @@ +package io.getstream.rn.callingx + +import android.annotation.SuppressLint +import com.google.firebase.messaging.RemoteMessage +import io.invertase.firebase.messaging.ReactNativeFirebaseMessagingService + +/** + * Extends React Native Firebase's messaging service to start [CallService] when a + * data message contains "stream" (e.g. incoming call push), then delegates to the + * parent so setBackgroundMessageHandler() still runs in JS. + * + * Only compiled when the app has @react-native-firebase/app and @react-native-firebase/messaging + * as dependencies. The app must remove the default [io.invertase.firebase.messaging.ReactNativeFirebaseMessagingService] from + * the merged manifest so this service is the single FCM handler + */ +@SuppressLint("MissingFirebaseInstanceTokenRefresh") +class StreamMessagingService : ReactNativeFirebaseMessagingService() { + + companion object { + const val TAG = "[Callingx] StreamMessagingService" + } + + override fun onMessageReceived(remoteMessage: RemoteMessage) { + val data = remoteMessage.data + debugLog(TAG, "onMessageReceived data = $data") + + val isSupportedStreamVideoCallRing = + data["sender"] == "stream.video" && data["type"] == "call.ring" + + if (isSupportedStreamVideoCallRing) { + val callCid = data["call_cid"] + if (callCid.isNullOrEmpty()) { + debugLog( + TAG, + "missing call_cid for call.ring, skipping CallService start", + ) + } else { + CallService.startIncomingCallFromPush(applicationContext, data) + } + } else { + debugLog(TAG, "sender or type is not supported, skipping CallService start") + } + + // Let React Native Firebase continue its normal processing so + // setBackgroundMessageHandler() still runs in JS. + super.onMessageReceived(remoteMessage) + } +} diff --git a/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/model/Call.kt b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/model/Call.kt new file mode 100644 index 0000000000..97210f24bf --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/model/Call.kt @@ -0,0 +1,58 @@ +package io.getstream.rn.callingx.model + +import android.os.Bundle +import android.telecom.DisconnectCause +import androidx.core.telecom.CallAttributesCompat +import androidx.core.telecom.CallEndpointCompat +import kotlinx.coroutines.channels.Channel + +/** + * Custom representation of a call state. + */ +sealed class Call { + + /** + * There is no current or past calls in the stack + */ + object None : Call() + + /** + * Represents a registered call with the telecom stack with the values provided by the + * Telecom SDK + */ + data class Registered( + val id: String, + val callAttributes: CallAttributesCompat, + val displayOptions: Bundle?, + val isActive: Boolean, + val isOnHold: Boolean, + val isMuted: Boolean, + val isPending: Boolean, + val errorCode: Int?, + val currentCallEndpoint: CallEndpointCompat?, + val availableCallEndpoints: List, + internal val actionSource: Channel, + ) : Call() { + + /** + * @return true if it's an incoming registered call, false otherwise + */ + fun isIncoming() = callAttributes.direction == CallAttributesCompat.DIRECTION_INCOMING + + /** + * Sends an action to the call session. It will be processed if it's still registered. + * + * @return true if the action was sent, false otherwise + */ + fun processAction(action: CallAction) = actionSource.trySend(action).isSuccess + } + + /** + * Represent a previously registered call that was disconnected + */ + data class Unregistered( + val id: String, + val callAttributes: CallAttributesCompat, + val disconnectCause: DisconnectCause, + ) : Call() +} diff --git a/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/model/CallAction.kt b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/model/CallAction.kt new file mode 100644 index 0000000000..b84c6ac61a --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/model/CallAction.kt @@ -0,0 +1,36 @@ +package io.getstream.rn.callingx.model + +import android.os.ParcelUuid +import android.os.Parcelable +import android.telecom.DisconnectCause +import kotlinx.parcelize.Parcelize + +/** + * Simple interface to represent related call actions to communicate with the registered call scope + * in the [TelecomCallRepository.registerCall] + * + * Note: we are using [Parcelize] to make the actions parcelable so they can be directly used in the + * call notification. + */ +sealed interface CallAction : Parcelable { + @Parcelize + data class Answer(val isAudioCall: Boolean) : CallAction + + @Parcelize + data class Disconnect(val cause: DisconnectCause) : CallAction + + @Parcelize + object Hold : CallAction + + @Parcelize + object Activate : CallAction + + @Parcelize + data class ToggleMute(val isMute: Boolean) : CallAction + + @Parcelize + data class SwitchAudioEndpoint(val endpointId: ParcelUuid) : CallAction + + @Parcelize + data class TransferCall(val endpointId: ParcelUuid) : CallAction +} diff --git a/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/notifications/CallNotificationManager.kt b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/notifications/CallNotificationManager.kt new file mode 100644 index 0000000000..61aeaff276 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/notifications/CallNotificationManager.kt @@ -0,0 +1,379 @@ +package io.getstream.rn.callingx.notifications + +import android.app.Notification +import android.content.Context +import android.media.Ringtone +import android.media.RingtoneManager +import android.net.Uri +import android.os.Build +import android.telecom.DisconnectCause +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.Person +import androidx.core.graphics.drawable.IconCompat +import io.getstream.rn.callingx.CallService +import io.getstream.rn.callingx.CallingxModuleImpl +import io.getstream.rn.callingx.R +import io.getstream.rn.callingx.ResourceUtils +import io.getstream.rn.callingx.debugLog +import io.getstream.rn.callingx.getDisconnectCauseString +import io.getstream.rn.callingx.model.Call +import io.getstream.rn.callingx.repo.CallRepository +import io.getstream.rn.callingx.utils.SettingsStore +import androidx.core.graphics.toColorInt + +/** + * Handles call status changes and updates the notification accordingly. For more guidance around + * notifications check https://developer.android.com/develop/ui/views/notifications + * + * Supports multiple simultaneous call notifications, each keyed by callId. + * + * @see updateCallNotification + */ +class CallNotificationManager( + private val context: Context, + private val notificationManager: NotificationManagerCompat = + NotificationManagerCompat.from(context) +) { + + internal companion object { + private const val TAG = "[Callingx] CallNotificationManager" + private const val DISABLED_COLOR = "#757575" // NOTE: hint color might be ignored by OS + } + + enum class OptimisticState { NONE, ACCEPTING, REJECTING } + + private val lock = Any() + + private var notificationsConfig = NotificationsConfig.loadNotificationsConfig(context) + + private var ringtone: Ringtone? = null + + /** + * Per-call notification state. Consolidates all per-call tracking into a single struct. + */ + private data class CallNotificationState( + val optimisticState: OptimisticState = OptimisticState.NONE, + val lastSnapshot: NotificationSnapshot? = null, + val activeWhen: Long? = null, + val hasBecameActive: Boolean = false, + ) + + // Per-call state, all guarded by [lock] + private val notificationsState = mutableMapOf() + + /** The callId whose notification was used for startForeground(). */ + private var foregroundCallId: String? = null + + /** + * Deterministic per-call notification ID. + * + * `NotificationManager.notify()`/`cancel()` require a stable integer id for updates/cancels. + * We derive it from `callId` so we don't need to allocate/store ids. + */ + private fun getNotificationId(callId: String): Int { + // Keep the value non-negative (defensive for Android-side expectations). + return callId.hashCode() and 0x7fffffff + } + + /** + * Snapshot of call state used to detect notification changes. + */ + data class NotificationSnapshot( + val id: String, + val isActive: Boolean, + val isIncoming: Boolean, + val optimisticState: OptimisticState, + val displayTitle: String?, + val displayName: CharSequence, + val address: Uri + ) + + /** + * Creates a snapshot of the call state used to detect notification changes. + * @return NotificationSnapshot + */ + private fun Call.Registered.toSnapshot(callId: String) = NotificationSnapshot( + id = id, + isActive = isActive, + isIncoming = isIncoming(), + optimisticState = notificationsState[callId]?.optimisticState ?: OptimisticState.NONE, + displayTitle = displayOptions?.getString(CallService.EXTRA_DISPLAY_TITLE), + displayName = callAttributes.displayName, + address = callAttributes.address + ) + + fun getOrCreateNotificationId(callId: String): Int = synchronized(lock) { + if (!notificationsState.containsKey(callId)) { + notificationsState[callId] = CallNotificationState() + } + if (foregroundCallId == null) { + foregroundCallId = callId + } + return@synchronized getNotificationId(callId) + } + + /** + * Sets the optimistic state of the call notification. + * Optimistic state is used to update the notification text while the app is connecting or declining the call. + * @param state The optimistic state to set. + */ + fun setOptimisticState(callId: String, state: OptimisticState) = synchronized(lock) { + // Be resilient to races where we receive optimistic actions before a notification state entry exists. + val current = + notificationsState[callId] + ?: CallNotificationState().also { + if (foregroundCallId == null) { + foregroundCallId = callId + } + } + notificationsState[callId] = if (state != OptimisticState.NONE) { + current.copy(optimisticState = state, lastSnapshot = null) + } else { + current.copy(optimisticState = state) + } + } + + /** + * Creates a notification for the call. + * Notification is created based on the call state and optimistic state. + * @param call The call to create a notification for. + * @return The notification. + */ + fun createNotification(callId: String, call: Call.Registered): Notification = synchronized(lock) { + debugLog(TAG,"[notifications] createNotification: Creating notification for call ID: ${call.id}") + + val state = notificationsState[callId] + val optimisticState = state?.optimisticState ?: OptimisticState.NONE + + val contentIntent = + NotificationIntentFactory.getLaunchActivityIntent( + context, + CallingxModuleImpl.CALL_ANSWERED_ACTION, + call.id + ) + val callStyle = createCallStyle(call, optimisticState) + val channelId = getChannelId(call, optimisticState) + debugLog(TAG, "[notifications] createNotification: Channel ID: $channelId") + + val builder = + NotificationCompat.Builder(context, channelId) + .setContentIntent(contentIntent) + .setFullScreenIntent(contentIntent, true) + .setSmallIcon(R.drawable.ic_round_call_24) + .setCategory(NotificationCompat.CATEGORY_CALL) + .setPriority(NotificationCompat.PRIORITY_MAX) + .setOngoing(optimisticState != OptimisticState.REJECTING) + + builder.setStyle(callStyle) + + // When call becomes active we need to set the when to current time and show the chronometer + if (call.isActive && optimisticState == OptimisticState.NONE && state != null) { + // We need to set the activation time once when call becomes active + if (!state.hasBecameActive) { + debugLog(TAG, "[notifications] createNotification: Setting when to current time for $callId") + val now = System.currentTimeMillis() + notificationsState[callId] = state.copy(activeWhen = now, hasBecameActive = true) + } + builder.setWhen(notificationsState[callId]?.activeWhen ?: System.currentTimeMillis()) + builder.setUsesChronometer(true) + builder.setShowWhen(true) + } + + // If the call is not active and the optimistic state is not none, we need to set the notification text + // based on exact action that is being taken (accepting or rejecting) + if (optimisticState != OptimisticState.NONE && !call.isActive) { + val text = when (optimisticState) { + OptimisticState.ACCEPTING -> SettingsStore.getOptimisticAcceptingText(context) + OptimisticState.REJECTING -> SettingsStore.getOptimisticRejectingText(context) + else -> null + } + if (text != null) builder.setContentText(text) + } + + return builder.build() + } + + /** + * Updates the call notification. + * If the call is None or Unregistered, we need to dismiss the notification. + * If the call is active and the optimistic state is not none, we need to reset the optimistic state. + * If the call is active and the optimistic state is none, we need to create a new notification. + * @param call The call to update the notification for. + */ + fun updateCallNotification(callId: String, call: Call.Registered) = synchronized(lock) { + val state = notificationsState[callId] + val optimisticState = state?.optimisticState ?: OptimisticState.NONE + if (call.isActive && optimisticState != OptimisticState.NONE && state != null) { + notificationsState[callId] = state.copy(optimisticState = OptimisticState.NONE) + debugLog(TAG, "[notifications] updateCallNotification[$callId]: Resetting optimistic state") + } + + // If the new snapshot is the same as the last posted snapshot, we need to skip the update + val newSnapshot = call.toSnapshot(callId) + if (newSnapshot == notificationsState[callId]?.lastSnapshot) { + debugLog(TAG, "[notifications] updateCallNotification[$callId]: Skipping - no state change") + return@synchronized + } + + val notificationId = getOrCreateNotificationId(callId) + notificationsState[callId] = + notificationsState[callId]?.copy(lastSnapshot = newSnapshot) + ?: CallNotificationState(lastSnapshot = newSnapshot) + val notification = createNotification(callId, call) + notificationManager.notify(notificationId, notification) + debugLog(TAG, "[notifications] updateCallNotification[$callId]: Notification posted (id=$notificationId)") + } + + fun postNotification(callId: String, notification: Notification) = synchronized(lock) { + val notificationId = getOrCreateNotificationId(callId) + notificationManager.notify(notificationId, notification) + } + + /** + * Returns a new foreground notification ID if the caller needs to call startForeground() + * to re-promote the service, or null if no action is needed. + */ + fun cancelNotification(callId: String): Int? = synchronized(lock) { + debugLog(TAG, "[notifications] cancelNotification[$callId]") + val state = notificationsState.remove(callId) + val notificationId = getNotificationId(callId) + notificationManager.cancel(notificationId) + if (state != null) { + debugLog(TAG, "[notifications] cancelNotification[$callId]: Cancelled (id=$notificationId)") + } + + if (foregroundCallId == callId) { + foregroundCallId = notificationsState.keys.firstOrNull() + // Return the new foreground notification ID so the service can re-promote + if (foregroundCallId != null) { + return@synchronized getNotificationId(foregroundCallId!!) + } + } + return@synchronized null + } + + fun getForegroundCallId(): String? = synchronized(lock) { foregroundCallId } + + fun cancelAllNotifications() = synchronized(lock) { + for (callId in notificationsState.keys) { + notificationManager.cancel(getNotificationId(callId)) + } + notificationsState.clear() + foregroundCallId = null + } + + fun startRingtone() { + if (ringtone?.isPlaying == true) { + debugLog(TAG, "[notifications] startRingtone: Ringtone already playing") + return + } + + try { + val soundUri = + ResourceUtils.getSoundUri(context, notificationsConfig.incomingChannel.sound) + ?: RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE) + + ringtone = RingtoneManager.getRingtone(context, soundUri) + } catch (e: Exception) { + Log.e(TAG, "[notifications] startRingtone: Error starting ringtone: ${e.message}") + return + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + ringtone?.isLooping = true + } + + ringtone?.play() + debugLog(TAG, "[notifications] startRingtone: Ringtone started") + } + + fun stopRingtone() { + if (ringtone?.isPlaying == true) { + ringtone?.stop() + debugLog(TAG, "[notifications] stopRingtone: Ringtone stopped") + } + ringtone = null + } + + fun resetOptimisticState(callId: String) = synchronized(lock) { + debugLog(TAG, "[notifications] resetOptimisticState[$callId]: Resetting optimistic state") + val current = notificationsState[callId] ?: return@synchronized + notificationsState[callId] = current.copy(optimisticState = OptimisticState.NONE, lastSnapshot = null) + } + + private fun getChannelId(call: Call.Registered, optimisticState: OptimisticState): String { + return if (call.isIncoming() && !call.isActive && optimisticState == OptimisticState.NONE) { + notificationsConfig.incomingChannel.id + } else { + notificationsConfig.ongoingChannel.id + } + } + + private fun createCallStyle(call: Call.Registered, optimisticState: OptimisticState): NotificationCompat.CallStyle? { + val caller = createPerson(call) + + if (call.isIncoming() && !call.isActive && optimisticState == OptimisticState.NONE) { + return NotificationCompat.CallStyle.forIncomingCall( + caller, + NotificationIntentFactory.getPendingNotificationIntent( + context, + CallingxModuleImpl.CALL_END_ACTION, + call.id, + CallRepository.EventSource.SYS.name.lowercase(), + false + ), + NotificationIntentFactory.getPendingNotificationIntent( + context, + CallingxModuleImpl.CALL_ANSWERED_ACTION, + call.id, + CallRepository.EventSource.SYS.name.lowercase(), + true + ) + ) + } + + if (optimisticState == OptimisticState.REJECTING) { + return NotificationCompat.CallStyle.forOngoingCall( + caller, + NotificationIntentFactory.getPendingBroadcastIntent( + context, + "io.getstream.CALL_END_NOOP", + call.id + ) , + ).setDeclineButtonColorHint(DISABLED_COLOR.toColorInt()) + } + + return NotificationCompat.CallStyle.forOngoingCall( + caller, + NotificationIntentFactory.getPendingBroadcastIntent( + context, + CallingxModuleImpl.CALL_END_ACTION, + call.id + ) { + putExtra( + CallingxModuleImpl.EXTRA_DISCONNECT_CAUSE, + getDisconnectCauseString(DisconnectCause(DisconnectCause.LOCAL)) + ) + putExtra( + CallingxModuleImpl.EXTRA_SOURCE, + CallRepository.EventSource.SYS.name.lowercase() + ) + }, + ) + } + + private fun createPerson(call: Call.Registered): Person { + val displayCallerName = call.displayOptions?.getString(CallService.EXTRA_DISPLAY_TITLE) + val address = call.callAttributes.address.toString() + + return Person.Builder() + .setName(displayCallerName ?: call.callAttributes.displayName) + .setUri(address) + .setIcon(IconCompat.createWithResource(context, R.drawable.ic_user)) + .setImportant(true) + .build() + } + +} diff --git a/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationChannelsManager.kt b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationChannelsManager.kt new file mode 100644 index 0000000000..96e073f0a4 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationChannelsManager.kt @@ -0,0 +1,104 @@ +package io.getstream.rn.callingx.notifications + +import android.content.Context +import android.os.Build +import androidx.core.app.NotificationChannelCompat +import androidx.core.app.NotificationManagerCompat +import io.getstream.rn.callingx.debugLog + +class NotificationChannelsManager( + private val context: Context, + private val notificationManager: NotificationManagerCompat = + NotificationManagerCompat.from(context) +) { + + private var notificationsConfig: NotificationsConfig.Channels? = null + + companion object { + private const val TAG = "[Callingx] NotificationChannelsManager" + } + + data class NotificationStatus( + val canPost: Boolean, + val hasPermissions: Boolean, + val areNotificationsEnabled: Boolean, + val isIncomingChannelEnabled: Boolean, + val isOngoingChannelEnabled: Boolean, + ) + + fun setNotificationsConfig(notificationsConfig: NotificationsConfig.Channels) { + this.notificationsConfig = notificationsConfig + } + + fun createNotificationChannels() { + notificationsConfig?.let { + val incomingChannel = createNotificationChannel(it.incomingChannel) + val ongoingChannel = createNotificationChannel(it.ongoingChannel) + + notificationManager.createNotificationChannelsCompat( + listOf( + incomingChannel, + ongoingChannel, + ), + ) + debugLog(TAG, "createNotificationChannels: Notification channels registered") + } + } + + fun getNotificationStatus(): NotificationStatus { + val areNotificationsEnabled = areNotificationsEnabled() + val hasPermissions = hasNotificationPermissions() + val isIncomingChannelEnabled = isChannelEnabled(notificationsConfig?.incomingChannel?.id) + val isOngoingChannelEnabled = isChannelEnabled(notificationsConfig?.ongoingChannel?.id) + + // CallStyle is exempt from notification permission when self-managing calls (Android 13+). + // On older versions we require areNotificationsEnabled(). + val canPostCallStyle = + hasPermissions && + isIncomingChannelEnabled && + isOngoingChannelEnabled && + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU || areNotificationsEnabled()) + + return NotificationStatus( + canPostCallStyle, + hasPermissions, + areNotificationsEnabled, + isIncomingChannelEnabled, + isOngoingChannelEnabled + ) + } + + private fun createNotificationChannel( + config: NotificationsConfig.ChannelParams + ): NotificationChannelCompat { + return NotificationChannelCompat.Builder(config.id, config.importance) + .apply { + setName(config.name) + setVibrationEnabled(config.vibration) + setSound(null, null) + } + .build() + } + + private fun areNotificationsEnabled(): Boolean { + return notificationManager.areNotificationsEnabled() + } + + private fun hasNotificationPermissions(): Boolean { + // CallStyle is exempt from notification permission when self-managing calls. + return true + } + + private fun isChannelEnabled(channelId: String?): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return true + } + + if (channelId == null) { + return false + } + + val channel = notificationManager.getNotificationChannel(channelId) + return channel != null && channel.importance != NotificationManagerCompat.IMPORTANCE_NONE + } +} diff --git a/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationIntentFactory.kt b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationIntentFactory.kt new file mode 100644 index 0000000000..70ac76799b --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationIntentFactory.kt @@ -0,0 +1,123 @@ +package io.getstream.rn.callingx.notifications + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import io.getstream.rn.callingx.CallingxModuleImpl +import kotlin.math.absoluteValue + +object NotificationIntentFactory { + // Base request codes for PendingIntents — combined with callId hash for uniqueness + private const val REQUEST_CODE_LAUNCH_ACTIVITY = 1001 + private const val REQUEST_CODE_RECEIVER_ACTIVITY = 2001 + private const val REQUEST_CODE_SERVICE = 3001 + + /** Generates a unique request code per callId + base offset to avoid PendingIntent collisions. */ + private fun requestCodeFor(callId: String, base: Int): Int { + return (base + callId.hashCode()).absoluteValue + } + + fun getPendingNotificationIntent( + context: Context, + action: String, + callId: String, + source: String, + includeLaunchActivity: Boolean + ): PendingIntent { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getReceiverActivityIntent(context, action, callId, source, includeLaunchActivity) + } else { + getPendingServiceIntent(context, action, callId, source) + } + } + + fun getPendingServiceIntent(context: Context, action: String, callId: String, source: String): PendingIntent { + val intent = + Intent(context, NotificationReceiverService::class.java).apply { + this.action = action + putExtra(CallingxModuleImpl.EXTRA_CALL_ID, callId) + putExtra(CallingxModuleImpl.EXTRA_SOURCE, source) + } + + return PendingIntent.getService( + context, + requestCodeFor(callId, REQUEST_CODE_SERVICE), + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + ) + } + + fun getReceiverActivityIntent(context: Context, action: String, callId: String, source: String, includeLaunchActivity: Boolean): PendingIntent { + val receiverIntent = + Intent(context, NotificationReceiverActivity::class.java).apply { + this.action = action + putExtra(CallingxModuleImpl.EXTRA_CALL_ID, callId) + putExtra(CallingxModuleImpl.EXTRA_SOURCE, source) + } + + val launchActivity = context.packageManager.getLaunchIntentForPackage(context.packageName) + val launchActivityIntent = + launchActivity?.let { base -> + Intent(base).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP) + } + } + + // intents are started in order and build a synthetic back stack + // the last intent is the one on top, so the launch activity should come first + val intents = + if (includeLaunchActivity && launchActivityIntent != null) { + arrayOf(launchActivityIntent, receiverIntent) + } else { + arrayOf(receiverIntent) + } + + return PendingIntent.getActivities( + context, + requestCodeFor(callId, REQUEST_CODE_RECEIVER_ACTIVITY), + intents, + PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + } + + fun getLaunchActivityIntent(context: Context, action: String, callId: String, source: String? = null): PendingIntent { + val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) + val callIntent = + Intent(launchIntent).apply { + this.action = action + putExtra(CallingxModuleImpl.EXTRA_CALL_ID, callId) + source?.let { putExtra(CallingxModuleImpl.EXTRA_SOURCE, it) } + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP) + } + + return PendingIntent.getActivity( + context, + requestCodeFor(callId, REQUEST_CODE_LAUNCH_ACTIVITY), + callIntent, + PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, + ) + } + + fun getPendingBroadcastIntent( + context: Context, + action: String, + callId: String, + addExtras: Intent.() -> Unit = {} + ): PendingIntent { + val intent = + Intent(action).apply { + setPackage(context.packageName) + putExtra(CallingxModuleImpl.EXTRA_CALL_ID, callId) + addExtras(this) + } + + // Use action + callId hash for unique request code per action per call + return PendingIntent.getBroadcast( + context, + (action.hashCode() + callId.hashCode()).absoluteValue, + intent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + } +} diff --git a/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationReceiverActivity.kt b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationReceiverActivity.kt new file mode 100644 index 0000000000..d82a5d19cd --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationReceiverActivity.kt @@ -0,0 +1,76 @@ +package io.getstream.rn.callingx.notifications + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.telecom.DisconnectCause +import io.getstream.rn.callingx.CallingxModuleImpl +import io.getstream.rn.callingx.debugLog +import io.getstream.rn.callingx.getDisconnectCauseString + +// For Android 12+ +class NotificationReceiverActivity : Activity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + handleIntent(intent) + finish() + } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + handleIntent(intent) + finish() + } + + private fun handleIntent(intent: Intent?) { + val nonNullIntent = intent ?: return + val action = nonNullIntent.action ?: return + + when (action) { + CallingxModuleImpl.CALL_ANSWERED_ACTION -> onCallAnswered(nonNullIntent) + CallingxModuleImpl.CALL_END_ACTION -> onCallEnded(nonNullIntent) + } + } + + private fun onCallAnswered(intent: Intent) { + debugLog("[Callingx] NotificationReceiverActivity", "[receiver] answered call action") + val callId = intent.getStringExtra(CallingxModuleImpl.EXTRA_CALL_ID) + val source = intent.getStringExtra(CallingxModuleImpl.EXTRA_SOURCE) + + if (callId != null) { + Intent(CallingxModuleImpl.CALL_OPTIMISTIC_ACCEPT_ACTION) + .apply { + setPackage(packageName) + putExtra(CallingxModuleImpl.EXTRA_CALL_ID, callId) + } + .also { sendBroadcast(it) } + } + + Intent(CallingxModuleImpl.CALL_ANSWERED_ACTION) + .apply { + setPackage(packageName) + putExtra(CallingxModuleImpl.EXTRA_CALL_ID, callId) + putExtra(CallingxModuleImpl.EXTRA_SOURCE, source) + } + .also { sendBroadcast(it) } + } + + private fun onCallEnded(intent: Intent) { + debugLog("[Callingx] NotificationReceiverActivity", "[receiver] rejected call action") + val callId = intent.getStringExtra(CallingxModuleImpl.EXTRA_CALL_ID) + val source = intent.getStringExtra(CallingxModuleImpl.EXTRA_SOURCE) + + Intent(CallingxModuleImpl.CALL_END_ACTION) + .apply { + setPackage(packageName) + putExtra(CallingxModuleImpl.EXTRA_CALL_ID, callId) + putExtra(CallingxModuleImpl.EXTRA_SOURCE, source) + putExtra( + CallingxModuleImpl.EXTRA_DISCONNECT_CAUSE, + getDisconnectCauseString(DisconnectCause(DisconnectCause.REJECTED)) + ) + } + .also { sendBroadcast(it) } + } +} diff --git a/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationReceiverService.kt b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationReceiverService.kt new file mode 100644 index 0000000000..6e0df78ec3 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationReceiverService.kt @@ -0,0 +1,87 @@ +package io.getstream.rn.callingx.notifications + +import android.app.Service +import android.content.Intent +import android.os.IBinder +import android.telecom.DisconnectCause +import android.util.Log +import io.getstream.rn.callingx.CallingxModuleImpl +import io.getstream.rn.callingx.getDisconnectCauseString + +class NotificationReceiverService : Service() { + + companion object { + const val TAG = "[Callingx] NotificationReceiverService" + } + + override fun onBind(intent: Intent?): IBinder? = null + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + val action = intent?.action + if (action == null) { + stopSelf(startId) + return START_NOT_STICKY + } + + when (action) { + CallingxModuleImpl.CALL_ANSWERED_ACTION -> onCallAnswered(intent) + CallingxModuleImpl.CALL_END_ACTION -> onCallEnded(intent) + } + + stopSelf(startId) + return START_NOT_STICKY + } + + private fun onCallAnswered(intent: Intent) { + val callId = intent.getStringExtra(CallingxModuleImpl.EXTRA_CALL_ID) + val source = intent.getStringExtra(CallingxModuleImpl.EXTRA_SOURCE) + callId?.let { + try { + Intent(CallingxModuleImpl.CALL_OPTIMISTIC_ACCEPT_ACTION) + .apply { + setPackage(packageName) + putExtra(CallingxModuleImpl.EXTRA_CALL_ID, it) + } + .also { it -> sendBroadcast(it) } + + NotificationIntentFactory.getPendingBroadcastIntent( + applicationContext, + CallingxModuleImpl.CALL_ANSWERED_ACTION, + it + ) { putExtra(CallingxModuleImpl.EXTRA_SOURCE, source) } + .send() + + NotificationIntentFactory.getLaunchActivityIntent( + applicationContext, + CallingxModuleImpl.CALL_ANSWERED_ACTION, + it, + source + ) + .send() + } catch (e: Exception) { + Log.e(TAG, "Error sending call answered intent", e) + } + } + } + + /** Mirrors [NotificationReceiverActivity] on API levels below 33 where the service receives notification actions. */ + private fun onCallEnded(intent: Intent) { + val callId = intent.getStringExtra(CallingxModuleImpl.EXTRA_CALL_ID) + val source = intent.getStringExtra(CallingxModuleImpl.EXTRA_SOURCE) + try { + Intent(CallingxModuleImpl.CALL_END_ACTION) + .apply { + setPackage(packageName) + putExtra(CallingxModuleImpl.EXTRA_CALL_ID, callId) + putExtra(CallingxModuleImpl.EXTRA_SOURCE, source) + putExtra( + CallingxModuleImpl.EXTRA_DISCONNECT_CAUSE, + getDisconnectCauseString(DisconnectCause(DisconnectCause.REJECTED)) + ) + } + .also { sendBroadcast(it) } + } catch (e: Exception) { + Log.e(TAG, "Error sending call end intent", e) + } + } +} diff --git a/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationsConfig.kt b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationsConfig.kt new file mode 100644 index 0000000000..8486c4fa3a --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationsConfig.kt @@ -0,0 +1,116 @@ +package io.getstream.rn.callingx.notifications + +import android.content.Context +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.edit +import com.facebook.react.bridge.ReadableMap +import io.getstream.rn.callingx.debugLog + +object NotificationsConfig { + private const val TAG = "[Callingx] NotificationsConfig" + private const val PREFS_NAME = "CallingxPrefs" + private const val PREFIX_IN = "incoming_" + private const val PREFIX_OUT = "ongoing_" + private const val KEY_ID = "id" + private const val KEY_NAME = "name" + private const val KEY_SOUND = "sound" + private const val KEY_VIBRATION = "vibration" + + data class ChannelParams( + val id: String, + val name: String, + val sound: String?, + val vibration: Boolean, + val importance: Int, + ) + + data class Channels( + val incomingChannel: ChannelParams, + val ongoingChannel: ChannelParams, + ) + + fun saveNotificationsConfig(context: Context, rawConfig: ReadableMap): Channels { + debugLog(TAG, "saveNotificationsConfig: Saving notifications config: $rawConfig") + val config = extractNotificationsConfig(rawConfig) + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + prefs.edit { + // Incoming channel + putString(PREFIX_IN + KEY_ID, config.incomingChannel.id) + putString(PREFIX_IN + KEY_NAME, config.incomingChannel.name) + putString(PREFIX_IN + KEY_SOUND, config.incomingChannel.sound) + putBoolean(PREFIX_IN + KEY_VIBRATION, config.incomingChannel.vibration) + + // Outgoing channel + putString(PREFIX_OUT + KEY_ID, config.ongoingChannel.id) + putString(PREFIX_OUT + KEY_NAME, config.ongoingChannel.name) + } + + return config + } + + fun loadNotificationsConfig(context: Context): Channels { + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + debugLog( + TAG, + "loadNotificationsConfig: Loading notifications config ${ + prefs.getString( + PREFIX_IN + KEY_ID, + "" + ) + }" + ) + return Channels( + incomingChannel = + ChannelParams( + id = prefs.getString(PREFIX_IN + KEY_ID, "") ?: "", + name = prefs.getString(PREFIX_IN + KEY_NAME, "") ?: "", + sound = prefs.getString(PREFIX_IN + KEY_SOUND, "") ?: "", + vibration = prefs.getBoolean(PREFIX_IN + KEY_VIBRATION, false), + importance = NotificationManagerCompat.IMPORTANCE_MAX, + ), + ongoingChannel = + ChannelParams( + id = prefs.getString(PREFIX_OUT + KEY_ID, "") ?: "", + name = prefs.getString(PREFIX_OUT + KEY_NAME, "") ?: "", + importance = NotificationManagerCompat.IMPORTANCE_DEFAULT, + vibration = false, + sound = null, + ) + ) + } + + fun extractNotificationsConfig(config: ReadableMap): Channels { + return Channels( + incomingChannel = + extractChannelConfig( + config.getMap("incomingChannel"), + NotificationManagerCompat.IMPORTANCE_MAX + ), + ongoingChannel = + extractChannelConfig( + config.getMap("ongoingChannel"), + NotificationManagerCompat.IMPORTANCE_DEFAULT + ), + ) + } + + fun extractChannelConfig(channel: ReadableMap?, importance: Int): ChannelParams { + if (channel == null) { + return ChannelParams( + id = "", + name = "", + sound = "", + vibration = false, + importance = importance, + ) + } + + return ChannelParams( + id = channel.getString("id") ?: "", + name = channel.getString("name") ?: "", + sound = channel.getString("sound"), + vibration = channel.hasKey("vibration") && channel.getBoolean("vibration") ?: false, + importance = importance, + ) + } +} diff --git a/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/repo/CallRepository.kt b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/repo/CallRepository.kt new file mode 100644 index 0000000000..695313a2f9 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/repo/CallRepository.kt @@ -0,0 +1,182 @@ +package io.getstream.rn.callingx.repo + +import android.content.Context +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.telecom.DisconnectCause +import android.util.Log +import androidx.core.telecom.CallAttributesCompat +import io.getstream.rn.callingx.model.Call +import io.getstream.rn.callingx.model.CallAction +import io.getstream.rn.callingx.CallService +import io.getstream.rn.callingx.debugLog +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.channels.Channel + +abstract class CallRepository(protected val context: Context) { + + enum class EventSource { + APP, SYS + } + + interface Listener { + fun onCallStateChanged(callId: String, call: Call) + fun onIsCallAnswered(callId: String, source: EventSource) + fun onIsCallDisconnected(callId: String?, cause: DisconnectCause, source: EventSource) + fun onIsCallInactive(callId: String) + fun onIsCallActive(callId: String) + fun onCallRegistered(callId: String, incoming: Boolean) + fun onMuteCallChanged(callId: String, isMuted: Boolean) + fun onCallEndpointChanged(callId: String, endpoint: String) + } + + protected val _calls: MutableStateFlow> = MutableStateFlow(emptyMap()) + val calls: StateFlow> = _calls.asStateFlow() + + protected var _listener: Listener? = null + protected val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + protected val registrationMutex: Mutex = Mutex() + + abstract fun setListener(listener: Listener?) + abstract fun release() + + abstract suspend fun registerCall( + callId: String, + displayName: String, + address: Uri, + isIncoming: Boolean, + isVideo: Boolean, + displayOptions: Bundle?, + ) + + open fun updateCall( + callId: String, + displayName: String, + address: Uri, + isVideo: Boolean, + displayOptions: Bundle?, + ) { + updateCallById(callId) { copy(displayOptions = displayOptions) } + } + + fun getCall(callId: String): Call.Registered? = _calls.value[callId] + + fun hasAnyCalls(): Boolean = _calls.value.isNotEmpty() + + fun hasRingingCall(excludeCallId: String? = null): Boolean = + _calls.value.any { (id, c) -> id != excludeCallId && !c.isPending && c.isIncoming() && !c.isActive } + + fun hasActiveCall(excludeCallId: String? = null): Boolean = + _calls.value.any { (id, c) -> id != excludeCallId && !c.isPending && c.isActive } + + //this call instance is used to display call notification before the call is registered, this is needed to invoke startForeground method on the service + public fun getTempCall(callInfo: CallService.CallInfo, incoming: Boolean): Call.Registered { + val attributes = createCallAttributes( + displayName = callInfo.name, + address = callInfo.uri, + isIncoming = incoming, + isVideo = callInfo.isVideo + ) + + return Call.Registered( + id = callInfo.callId, + isPending = true, + isActive = false, + isOnHold = false, + callAttributes = attributes, + displayOptions = callInfo.displayOptions, + isMuted = false, + errorCode = null, + currentCallEndpoint = null, + availableCallEndpoints = emptyList(), + actionSource = Channel() // Temporary channel, will be replaced by actual registration + ) + } + + /** + * Update the state of a specific call applying the transform lambda only if the call is + * found in the map. Otherwise keep the current state. + */ + protected fun updateCallById(callId: String, transform: Call.Registered.() -> Call) { + _calls.update { currentMap -> + val call = currentMap[callId] + if (call != null) { + val updated = call.transform() + debugLog( + getTag(), + "[repository] updateCallById: Call $callId state updated to: ${updated::class.simpleName}" + ) + if (updated is Call.Registered) { + currentMap + (callId to updated) + } else { + // Call transitioned to non-Registered state (e.g. Unregistered) — remove from map + currentMap - callId + } + } else { + Log.w( + getTag(), + "[repository] updateCallById: Call $callId not found in map, skipping update" + ) + currentMap + } + } + } + + protected fun addCall(callId: String, call: Call.Registered) { + _calls.update { it + (callId to call) } + } + + protected fun removeCall(callId: String) { + _calls.update { it - callId } + } + + protected fun createCallAttributes( + displayName: String, + address: Uri, + isIncoming: Boolean, + isVideo: Boolean + ): CallAttributesCompat { + return CallAttributesCompat( + displayName = displayName, + address = address, + direction = + if (isIncoming) { + CallAttributesCompat.DIRECTION_INCOMING + } else { + CallAttributesCompat.DIRECTION_OUTGOING + }, + callType = + if (isVideo) { + CallAttributesCompat.CALL_TYPE_VIDEO_CALL + } else { + CallAttributesCompat.CALL_TYPE_AUDIO_CALL + }, + callCapabilities = + CallAttributesCompat.SUPPORTS_SET_INACTIVE or + CallAttributesCompat.SUPPORTS_STREAM or + CallAttributesCompat.SUPPORTS_TRANSFER, + ) + } + + protected abstract fun getTag(): String +} + +object CallRepositoryFactory { + + fun create(context: Context): CallRepository { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + TelecomCallRepository(context) // Your current CallRepository renamed + } else { + LegacyCallRepository(context) // Fallback implementation + } + } +} diff --git a/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/repo/LegacyCallRepository.kt b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/repo/LegacyCallRepository.kt new file mode 100644 index 0000000000..393ec974a0 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/repo/LegacyCallRepository.kt @@ -0,0 +1,149 @@ +package io.getstream.rn.callingx.repo + +import android.content.Context +import android.net.Uri +import android.os.Bundle +import android.telecom.DisconnectCause +import android.util.Log +import io.getstream.rn.callingx.model.Call +import io.getstream.rn.callingx.model.CallAction +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.withLock + +class LegacyCallRepository(context: Context) : CallRepository(context) { + + companion object { + private const val TAG = "[Callingx] LegacyCallRepository" + } + + private var observeCallsJob: Job? = null + + override fun getTag(): String = TAG + + override fun setListener(listener: Listener?) { + this._listener = listener + observeCallsJob?.cancel() + observeCallsJob = scope.launch { + var previousCalls: Map = emptyMap() + try { + calls.collect { currentCalls -> + // Notify about changes per call + for ((callId, call) in currentCalls) { + _listener?.onCallStateChanged(callId, call) + } + for ((callId, _) in previousCalls) { + if (!currentCalls.containsKey(callId)) { + _listener?.onCallStateChanged(callId, Call.None) + } + } + previousCalls = currentCalls + } + } catch (e: Exception) { + Log.e(TAG, "[repository] setListener: Error collecting call state", e) + } + } + } + + override fun release() { + _calls.value = emptyMap() + + observeCallsJob?.cancel() + observeCallsJob = null + _listener = null + + scope.cancel() + } + + override suspend fun registerCall( + callId: String, + displayName: String, + address: Uri, + isIncoming: Boolean, + isVideo: Boolean, + displayOptions: Bundle?, + ) = registrationMutex.withLock { + // Check if this specific call is already registered + if (_calls.value.containsKey(callId)) { + Log.w( + TAG, + "[repository] registerCall: Call $callId already registered, ignoring duplicate" + ) + return@withLock + } + + val attributes = createCallAttributes(displayName, address, isIncoming, isVideo) + val actionSource = Channel() + + val registeredCall = Call.Registered( + id = callId, + isPending = false, + isActive = false, + isOnHold = false, + callAttributes = attributes, + displayOptions = displayOptions, + isMuted = false, + errorCode = null, + currentCallEndpoint = null, + availableCallEndpoints = emptyList(), + actionSource = actionSource, + ) + + addCall(callId, registeredCall) + _listener?.onCallRegistered(callId, isIncoming) + + // Process actions without telecom SDK + scope.launch { + try { + actionSource.consumeAsFlow().collect { action -> processActionLegacy(callId, action) } + } catch (e: Exception) { + Log.e(TAG, "[repository] registerCall: Error consuming actions for $callId", e) + } + } + } + + override fun updateCall( + callId: String, + displayName: String, + address: Uri, + isVideo: Boolean, + displayOptions: Bundle?, + ) { + super.updateCall(callId, displayName, address, isVideo, displayOptions) + } + + private fun processActionLegacy(callId: String, action: CallAction) { + when (action) { + is CallAction.Answer -> { + updateCallById(callId) { copy(isActive = true, isOnHold = false) } + val call = _calls.value[callId] + if (call != null) { + _listener?.onIsCallAnswered(callId, EventSource.APP) + } + } + is CallAction.Disconnect -> { + val call = _calls.value[callId] + if (call != null) { + removeCall(callId) + _listener?.onIsCallDisconnected( + callId, + action.cause, + EventSource.APP + ) + } + } + is CallAction.ToggleMute -> { + updateCallById(callId) { copy(isMuted = action.isMute) } + } + // Handle other actions... + else -> { + /* No-op for unsupported actions */ + } + } + } + +} diff --git a/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/repo/TelecomCallRepository.kt b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/repo/TelecomCallRepository.kt new file mode 100644 index 0000000000..b3a0eed088 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/repo/TelecomCallRepository.kt @@ -0,0 +1,481 @@ +package io.getstream.rn.callingx.repo + +import android.content.Context +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.telecom.DisconnectCause +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.telecom.CallAttributesCompat +import androidx.core.telecom.CallControlResult +import androidx.core.telecom.CallControlScope +import androidx.core.telecom.CallsManager +import io.getstream.rn.callingx.debugLog +import io.getstream.rn.callingx.model.Call +import io.getstream.rn.callingx.model.CallAction +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.withLock +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicBoolean + +/** + * Per-call flags tracking whether an action was initiated by the app (self) or by the system. + */ +private data class CallActionFlags( + val isSelfAnswered: AtomicBoolean = AtomicBoolean(false), + val isSelfDisconnected: AtomicBoolean = AtomicBoolean(false), +) + +/** + * The central repository that keeps track of calls and allows to register new ones. + * + * This class contains the main logic to integrate with Telecom SDK. + * Multiple calls can be registered simultaneously — each gets its own [CallControlScope]. + * + * @see registerCall + */ +@RequiresApi(Build.VERSION_CODES.O) +class TelecomCallRepository(context: Context) : CallRepository(context) { + + companion object { + private const val TAG = "[Callingx] TelecomCallRepository" + } + + @Volatile + private var isReleased: Boolean = false + + private var observeCallsJob: Job? = null + + private val callsManager: CallsManager + + /** Per-call action-source flags, keyed by callId. */ + private val actionFlags = ConcurrentHashMap() + + init { + val capabilities = + CallsManager.CAPABILITY_SUPPORTS_CALL_STREAMING or + CallsManager.CAPABILITY_SUPPORTS_VIDEO_CALLING + callsManager = + CallsManager(context.applicationContext).apply { + registerAppWithTelecom(capabilities) + } + debugLog(TAG, "[repository] init: CallsManager created and registered") + } + + override fun getTag(): String = TAG + + override fun setListener(listener: Listener?) { + this._listener = listener + + observeCallsJob?.cancel() + observeCallsJob = observeCalls() + } + + override fun release() { + if (isReleased) { + debugLog(TAG, "[repository] release: Already released, ignoring") + return + } + isReleased = true + + // Disconnect all active calls + val currentCalls = _calls.value + for ((callId, call) in currentCalls) { + call.processAction(CallAction.Disconnect(DisconnectCause(DisconnectCause.LOCAL))) + } + _calls.value = emptyMap() + actionFlags.clear() + + observeCallsJob?.cancel() + observeCallsJob = null + _listener = null + + scope.cancel() + } + + /** + * Register a new call with the provided attributes. Use the [calls] StateFlow to receive + * status updates and process call related actions. + */ + override suspend fun registerCall( + callId: String, + displayName: String, + address: Uri, + isIncoming: Boolean, + isVideo: Boolean, + displayOptions: Bundle?, + ) { + if (isReleased) { + Log.w( + TAG, + "[repository] registerCall: Repository already released, ignoring registration for $callId" + ) + return + } + + // Hold the mutex only for the dedup check — release before entering the long-lived call scope + val attributes: CallAttributesCompat + val actionSource: Channel + val flags: CallActionFlags + + registrationMutex.withLock { + debugLog( + TAG, + "[repository] registerCall: Starting registration - CallId: $callId, Name: $displayName, Address: $address, Incoming: $isIncoming" + ) + + // Check if this specific call is already registered or registration is in progress + if (_calls.value.containsKey(callId)) { + Log.w( + TAG, + "[repository] registerCall: Call $callId already registered, ignoring duplicate" + ) + return + } + + debugLog( + TAG, + "[repository] registerCall: Call $callId not found in map, proceeding with registration" + ) + + attributes = createCallAttributes(displayName, address, isIncoming, isVideo) + actionSource = Channel() + flags = CallActionFlags() + actionFlags[callId] = flags + + // Add call to the map early so that duplicate registrations are rejected + // and listeners are notified immediately. Actions sent via trySend() may arrive + // before the call scope starts collecting; in that case, they are dropped + // rather than buffered, which is acceptable because we explicitly handle + // pending actions in CallService/CallRegistrationStore. + val registeredCall = Call.Registered( + id = callId, + isPending = true, + isActive = false, + isOnHold = false, + callAttributes = attributes, + displayOptions = displayOptions, + isMuted = false, + errorCode = null, + currentCallEndpoint = null, + availableCallEndpoints = emptyList(), + actionSource = actionSource, + ) + addCall(callId, registeredCall) + debugLog(TAG, "[repository] registerCall: Call $callId added to map in pending state") + } + + // Register the call with Telecom and handle actions in the scope (mutex released) + try { + callsManager.addCall( + attributes, + onIsCallAnswered(callId, flags), + onIsCallDisconnected(callId, flags), + onIsCallActive(callId), + onIsCallInactive(callId) + ) { + debugLog( + TAG, + "[repository] registerCall: Inside call scope for $callId, setting up call handlers" + ) + + // Call is now registered in Telecom — mark as no longer pending + updateCallById(callId) { copy(isPending = false) } + + // Consume the actions to interact with the call inside the scope + launch { processCallActions(callId, flags, actionSource.consumeAsFlow()) } + + launch { + currentCallEndpoint.collect { + updateCallById(callId) { copy(currentCallEndpoint = it) } + } + } + launch { + availableEndpoints.collect { + updateCallById(callId) { copy(availableCallEndpoints = it) } + } + } + launch { + isMuted.collect { + updateCallById(callId) { copy(isMuted = it) } + } + } + } + debugLog( + TAG, + "[repository] registerCall: Call $callId scope ended normally" + ) + } catch (e: Exception) { + Log.e(TAG, "[repository] registerCall: Error registering call $callId", e) + throw e + } finally { + // Call lifecycle cleanup: + // - processCallActions(...) is launched in the CallControlScope, so its collector is + // cancelled automatically when the Telecom call scope finishes. + // - We then remove the call from the repository map and clear per-call flags. + // - The Call.actionSource channel is no longer referenced and can be garbage-collected; + // we do not explicitly close it because callers use trySend(), which never suspends. + debugLog(TAG, "[repository] registerCall: Cleaning up call $callId") + removeCall(callId) + actionFlags.remove(callId) + } + } + + override fun updateCall( + callId: String, + displayName: String, + address: Uri, + isVideo: Boolean, + displayOptions: Bundle?, + ) { + debugLog( + TAG, + "[repository] updateCall: Starting update - CallId: $callId, Name: $displayName, Address: $address, IsVideo: $isVideo" + ) + super.updateCall(callId, displayName, address, isVideo, displayOptions) + } + + private fun observeCalls(): Job { + // Track previous state per call for diffing (only non-pending calls) + var previousCalls: Map = emptyMap() + + return calls + .onEach { allCalls -> + // Filter out pending calls — they are not yet registered in Telecom + val currentCalls = allCalls.filter { (_, call) -> !call.isPending } + + // Detect new calls + for ((callId, call) in currentCalls) { + val previous = previousCalls[callId] + if (previous == null) { + // New call added + _listener?.onCallRegistered(callId, call.isIncoming()) + } else { + // Existing call changed + if (previous.isMuted != call.isMuted) { + debugLog(TAG, "[repository] observeCalls: Mute changed for $callId: ${call.isMuted}") + _listener?.onMuteCallChanged(callId, call.isMuted) + } + if (previous.currentCallEndpoint != call.currentCallEndpoint) { + call.currentCallEndpoint?.let { + _listener?.onCallEndpointChanged(callId, it.name.toString()) + } + } + } + _listener?.onCallStateChanged(callId, call) + } + + // Detect removed calls + for ((callId, _) in previousCalls) { + if (!currentCalls.containsKey(callId)) { + _listener?.onCallStateChanged(callId, Call.None) + } + } + + previousCalls = currentCalls + } + .launchIn(scope) + } + + /** Collect the action source to handle client actions inside the call scope */ + private suspend fun CallControlScope.processCallActions( + callId: String, + flags: CallActionFlags, + actionSource: Flow + ) { + actionSource.collect { action -> + debugLog(TAG, "[repository] processCallActions[$callId]: action: ${action::class.simpleName}") + when (action) { + is CallAction.Answer -> { + doAnswer(callId, flags, action.isAudioCall) + } + is CallAction.Disconnect -> { + doDisconnect(callId, flags, action) + } + is CallAction.SwitchAudioEndpoint -> { + doSwitchEndpoint(callId, action) + } + is CallAction.TransferCall -> { + debugLog( + TAG, + "[repository] processCallActions[$callId]: Transfer to endpoint: ${action.endpointId}" + ) + val call = _calls.value[callId] + val endpoints = + call?.availableCallEndpoints?.firstOrNull { + it.identifier == action.endpointId + } + if (endpoints != null) { + requestEndpointChange( + endpoint = endpoints, + ) + } else { + Log.w( + TAG, + "[repository] processCallActions[$callId]: Endpoint not found for transfer, ignoring" + ) + } + } + CallAction.Hold -> { + when (val result = setInactive()) { + is CallControlResult.Success -> { + onIsCallInactive(callId)() + } + is CallControlResult.Error -> { + Log.e( + TAG, + "[repository] processCallActions[$callId]: Hold action failed with error code: ${result.errorCode}" + ) + updateCallById(callId) { copy(errorCode = result.errorCode) } + } + } + } + CallAction.Activate -> { + when (val result = setActive()) { + is CallControlResult.Success -> { + onIsCallActive(callId)() + } + is CallControlResult.Error -> { + Log.e( + TAG, + "[repository] processCallActions[$callId]: Activate action failed with error code: ${result.errorCode}" + ) + updateCallById(callId) { copy(errorCode = result.errorCode) } + } + } + } + is CallAction.ToggleMute -> { + debugLog(TAG, "[repository] processCallActions[$callId]: Toggling mute: ${action.isMute}") + updateCallById(callId) { + copy(isMuted = action.isMute) + } + } + } + } + debugLog(TAG, "[repository] processCallActions[$callId]: Action collection ended") + } + + + private suspend fun CallControlScope.doSwitchEndpoint(callId: String, action: CallAction.SwitchAudioEndpoint) { + debugLog(TAG, "[repository] doSwitchEndpoint[$callId]: Switching to endpoint: ${action.endpointId}") + val call = _calls.value[callId] + if (call == null) { + Log.w(TAG, "[repository] doSwitchEndpoint[$callId]: Call not found, ignoring") + return + } + val endpoints = call.availableCallEndpoints + val newEndpoint = endpoints.firstOrNull { it.identifier == action.endpointId } + + if (newEndpoint != null) { + debugLog( + TAG, + "[repository] doSwitchEndpoint[$callId]: Found endpoint: ${newEndpoint.name}, requesting change" + ) + requestEndpointChange(newEndpoint).also { + debugLog(TAG, "[repository] doSwitchEndpoint[$callId]: Endpoint change result: $it") + } + } else { + Log.w(TAG, "[repository] doSwitchEndpoint[$callId]: Endpoint not found in available endpoints") + } + } + + private suspend fun CallControlScope.doDisconnect(callId: String, flags: CallActionFlags, action: CallAction.Disconnect) { + flags.isSelfDisconnected.set(true) + debugLog(TAG, "[repository] doDisconnect[$callId]: Disconnecting call with cause: ${action.cause}") + disconnect(action.cause) + debugLog(TAG, "[repository] doDisconnect[$callId]: Disconnect called, triggering onIsCallDisconnected") + onIsCallDisconnected(callId, flags)(action.cause) + } + + private suspend fun CallControlScope.doAnswer(callId: String, flags: CallActionFlags, isAudioCall: Boolean) { + flags.isSelfAnswered.set(true) + val callType = + if (isAudioCall) CallAttributesCompat.CALL_TYPE_AUDIO_CALL + else CallAttributesCompat.CALL_TYPE_VIDEO_CALL + + when (val result = answer(callType)) { + is CallControlResult.Success -> { + onIsCallAnswered(callId, flags)(callType) + } + is CallControlResult.Error -> { + Log.e( + TAG, + "[repository] doAnswer[$callId]: Answer failed with error code: ${result.errorCode}" + ) + flags.isSelfAnswered.set(false) + val call = _calls.value[callId] + if (call != null) { + removeCall(callId) + _listener?.onIsCallDisconnected( + callId, + DisconnectCause(DisconnectCause.BUSY), + EventSource.APP + ) + } + } + } + } + + private fun onIsCallAnswered(callId: String, flags: CallActionFlags): suspend (type: Int) -> Unit = { + debugLog( + TAG, + "[repository] onIsCallAnswered[$callId]: Call answered, type: $it, isSelfAnswered: ${flags.isSelfAnswered.get()}" + ) + updateCallById(callId) { copy(isActive = true, isOnHold = false) } + + val source = if (flags.isSelfAnswered.get()) EventSource.APP else EventSource.SYS + if (_calls.value.containsKey(callId)) { + _listener?.onIsCallAnswered(callId, source) + } + flags.isSelfAnswered.set(false) + debugLog(TAG, "[repository] onIsCallAnswered[$callId]: Call state updated to active") + } + + private fun onIsCallDisconnected(callId: String, flags: CallActionFlags): suspend (cause: DisconnectCause) -> Unit = { cause -> + debugLog( + TAG, + "[repository] onIsCallDisconnected[$callId]: Call disconnected, cause: ${cause.reason}, description: ${cause.description}" + ) + val source = if (flags.isSelfDisconnected.get()) EventSource.APP else EventSource.SYS + + removeCall(callId) + _listener?.onIsCallDisconnected(callId, cause, source) + flags.isSelfDisconnected.set(false) + debugLog(TAG, "[repository] onIsCallDisconnected[$callId]: Call removed from map") + } + + private fun onIsCallActive(callId: String): suspend () -> Unit = { + debugLog(TAG, "[repository] onIsCallActive[$callId]: Call became active") + updateCallById(callId) { + copy( + errorCode = null, + isActive = true, + isOnHold = false, + ) + } + + if (_calls.value.containsKey(callId)) { + _listener?.onIsCallActive(callId) + } + debugLog(TAG, "[repository] onIsCallActive[$callId]: Call state updated") + } + + private fun onIsCallInactive(callId: String): suspend () -> Unit = { + debugLog(TAG, "[repository] onIsCallInactive[$callId]: Call became inactive (on hold)") + updateCallById(callId) { copy(errorCode = null, isOnHold = true) } + + if (_calls.value.containsKey(callId)) { + _listener?.onIsCallInactive(callId) + } + debugLog(TAG, "[repository] onIsCallInactive[$callId]: Call state updated to on hold") + } + +} diff --git a/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/utils/CallEventBus.kt b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/utils/CallEventBus.kt new file mode 100644 index 0000000000..e98177bf7b --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/utils/CallEventBus.kt @@ -0,0 +1,61 @@ +package io.getstream.rn.callingx + +import android.os.Bundle + +data class CallEvent(val action: String, val extras: Bundle) + +object CallEventBus { + + interface Listener { + fun onCallEvent(event: CallEvent) + } + + private val pendingEvents = mutableListOf() + private var listener: Listener? = null + private var isJsReady: Boolean = false + + @JvmStatic + @Synchronized + fun publish(event: CallEvent) { + val currentListener = listener + if (currentListener != null && isJsReady) { + currentListener.onCallEvent(event) + } else { + pendingEvents.add(event) + } + } + + @JvmStatic + @Synchronized + fun subscribe(listener: Listener) { + this.listener = listener + } + + @JvmStatic + @Synchronized + fun unsubscribe(listener: Listener) { + if (this.listener === listener) { + this.listener = null + isJsReady = false + pendingEvents.clear() + } + } + + @JvmStatic + @Synchronized + fun drainPendingEvents(): List { + if (pendingEvents.isEmpty()) { + return emptyList() + } + val copy = pendingEvents.toList() + pendingEvents.clear() + return copy + } + + @JvmStatic + @Synchronized + fun markJsReady() { + isJsReady = true + } +} + diff --git a/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/utils/ResourceUtils.kt b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/utils/ResourceUtils.kt new file mode 100644 index 0000000000..289dc85d80 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/utils/ResourceUtils.kt @@ -0,0 +1,60 @@ +package io.getstream.rn.callingx + +import android.content.Context +import android.media.RingtoneManager +import android.net.Uri +import androidx.core.net.toUri + + +object ResourceUtils { + private const val TAG = "[Callingx] ResourceUtils" + + private val resourceIdCache = mutableMapOf() + + private fun getResourceIdByName(context: Context, name: String?, type: String): Int { + if (name.isNullOrEmpty()) { + return 0 + } + + val normalizedName = name.lowercase().replace("-", "_") + val key = "${normalizedName}_$type" + + synchronized(resourceIdCache) { + resourceIdCache[key]?.let { + return it + } + + val packageName = context.packageName + + val id = context.resources.getIdentifier(normalizedName, type, packageName) + resourceIdCache[key] = id + return id + } + } + + fun getSoundUri(context: Context, sound: String?): Uri? { + debugLog(TAG, "getSoundUri: Getting sound URI for: $sound") + return when { + sound == null -> null + sound.contains("://") -> sound.toUri() + sound.equals("default", ignoreCase = true) -> { + RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) + } + + else -> { + // The API user is attempting to set a sound by file name, verify it exists + var soundResourceId = getResourceIdByName(context, sound, "raw") + if (soundResourceId == 0 && sound.contains(".")) { + soundResourceId = getResourceIdByName(context, sound.substringBeforeLast('.'), "raw") + } + if (soundResourceId == 0) { + null + } else { + // Use the actual sound name vs the resource ID, to obtain a stable URI, Issue #341 + val soundName = context.resources.getResourceEntryName(soundResourceId) + "android.resource://${context.packageName}/raw/$soundName".toUri() + } + } + } + } +} diff --git a/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/utils/SettingsStore.kt b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/utils/SettingsStore.kt new file mode 100644 index 0000000000..1511a7682b --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/utils/SettingsStore.kt @@ -0,0 +1,51 @@ +package io.getstream.rn.callingx.utils + +import android.content.Context +import androidx.core.content.edit + +object SettingsStore { + + private const val PREF_NAME = "io.getstream.rn.callingx.settings" + private const val KEY_REJECT_CALL_WHEN_BUSY = "reject_call_when_busy" + private const val KEY_OPTIMISTIC_ACCEPTING_TEXT = "optimistic_accepting_text" + private const val KEY_OPTIMISTIC_REJECTING_TEXT = "optimistic_rejecting_text" + + private const val DEFAULT_OPTIMISTIC_ACCEPTING_TEXT = "Connecting..." + private const val DEFAULT_OPTIMISTIC_REJECTING_TEXT = "Declining..." + + fun setShouldRejectCallWhenBusy(context: Context, shouldReject: Boolean) { + val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + prefs.edit { putBoolean(KEY_REJECT_CALL_WHEN_BUSY, shouldReject) } + } + + fun shouldRejectCallWhenBusy(context: Context): Boolean { + val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + return prefs.getBoolean(KEY_REJECT_CALL_WHEN_BUSY, false) + } + + fun setOptimisticTexts( + context: Context, + acceptingText: String?, + rejectingText: String?, + ) { + val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + prefs.edit { + if (acceptingText != null) { + putString(KEY_OPTIMISTIC_ACCEPTING_TEXT, acceptingText) + } + if (rejectingText != null) { + putString(KEY_OPTIMISTIC_REJECTING_TEXT, rejectingText) + } + } + } + + fun getOptimisticAcceptingText(context: Context): String { + val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + return prefs.getString(KEY_OPTIMISTIC_ACCEPTING_TEXT, DEFAULT_OPTIMISTIC_ACCEPTING_TEXT) ?: DEFAULT_OPTIMISTIC_ACCEPTING_TEXT + } + + fun getOptimisticRejectingText(context: Context): String { + val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + return prefs.getString(KEY_OPTIMISTIC_REJECTING_TEXT, DEFAULT_OPTIMISTIC_REJECTING_TEXT) ?: DEFAULT_OPTIMISTIC_REJECTING_TEXT + } +} diff --git a/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/utils/Utils.kt b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/utils/Utils.kt new file mode 100644 index 0000000000..b1055cf220 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/utils/Utils.kt @@ -0,0 +1,23 @@ +package io.getstream.rn.callingx + +import android.telecom.DisconnectCause +import android.util.Log + +fun getDisconnectCauseString(cause: DisconnectCause): String { + return when (cause.code) { + DisconnectCause.LOCAL -> "local" + DisconnectCause.REMOTE -> "remote" + DisconnectCause.REJECTED -> "rejected" + DisconnectCause.BUSY -> "busy" + DisconnectCause.ANSWERED_ELSEWHERE -> "answeredElsewhere" + DisconnectCause.MISSED -> "missed" + DisconnectCause.ERROR -> "error" + else -> cause.toString() + } +} + +fun debugLog(tag: String, message: String) { + if (BuildConfig.DEBUG) { + Log.d(tag, message) + } +} \ No newline at end of file diff --git a/packages/react-native-callingx/android/src/main/res/drawable/ic_phone_paused_24.xml b/packages/react-native-callingx/android/src/main/res/drawable/ic_phone_paused_24.xml new file mode 100644 index 0000000000..7773fecd3e --- /dev/null +++ b/packages/react-native-callingx/android/src/main/res/drawable/ic_phone_paused_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/packages/react-native-callingx/android/src/main/res/drawable/ic_round_call_24.xml b/packages/react-native-callingx/android/src/main/res/drawable/ic_round_call_24.xml new file mode 100644 index 0000000000..3daf420f62 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/res/drawable/ic_round_call_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/packages/react-native-callingx/android/src/main/res/drawable/ic_user.xml b/packages/react-native-callingx/android/src/main/res/drawable/ic_user.xml new file mode 100644 index 0000000000..eaaa2941f8 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/res/drawable/ic_user.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/packages/react-native-callingx/android/src/newarch/java/io/getstream/rn/callingx/CallingxModule.kt b/packages/react-native-callingx/android/src/newarch/java/io/getstream/rn/callingx/CallingxModule.kt new file mode 100644 index 0000000000..bd2813e0d5 --- /dev/null +++ b/packages/react-native-callingx/android/src/newarch/java/io/getstream/rn/callingx/CallingxModule.kt @@ -0,0 +1,157 @@ +package io.getstream.rn.callingx + +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.WritableArray +import com.facebook.react.bridge.WritableMap +import com.facebook.react.module.annotations.ReactModule + +@ReactModule(name = CallingxModule.NAME) +class CallingxModule(reactContext: ReactApplicationContext) : + NativeCallingxSpec(reactContext), CallingxEventEmitterAdapter { + + companion object { + const val NAME = CallingxModuleImpl.NAME + } + + private val impl = CallingxModuleImpl(reactContext, this) + + override fun emitNewEvent(value: WritableMap) { + emitOnNewEvent(value) + } + + override fun getName(): String = NAME + + override fun initialize() { + super.initialize() + impl.initialize() + } + + override fun invalidate() { + impl.invalidate() + super.invalidate() + } + + override fun setupiOS(options: ReadableMap) { + // leave empty + } + + override fun setupAndroid(options: ReadableMap) { + impl.setupAndroid(options) + } + + override fun canPostNotifications(): Boolean { + return impl.canPostNotifications() + } + + override fun setShouldRejectCallWhenBusy(shouldReject: Boolean) { + impl.setShouldRejectCallWhenBusy(shouldReject) + } + + override fun getInitialVoipEvents(): WritableArray { + // leave empty + return com.facebook.react.bridge.Arguments.createArray() + } + + override fun registerVoipToken() { + // leave empty + } + + override fun getInitialEvents(): WritableArray { + return impl.getInitialEvents() + } + + override fun setCurrentCallActive(callId: String, promise: Promise) { + impl.setCurrentCallActive(callId, promise) + } + + override fun displayIncomingCall( + callId: String, + phoneNumber: String, + callerName: String, + hasVideo: Boolean, + displayOptions: ReadableMap?, + promise: Promise + ) { + impl.displayIncomingCall(callId, phoneNumber, callerName, hasVideo, displayOptions, promise) + } + + override fun answerIncomingCall(callId: String, promise: Promise) { + impl.answerIncomingCall(callId, promise) + } + + override fun startCall( + callId: String, + phoneNumber: String, + callerName: String, + hasVideo: Boolean, + displayOptions: ReadableMap?, + promise: Promise + ) { + impl.startCall(callId, phoneNumber, callerName, hasVideo, displayOptions, promise) + } + + override fun updateDisplay( + callId: String, + phoneNumber: String, + callerName: String, + displayOptions: ReadableMap?, + promise: Promise + ) { + impl.updateDisplay(callId, phoneNumber, callerName, displayOptions, promise) + } + + override fun endCallWithReason(callId: String, reason: Double, promise: Promise) { + impl.endCallWithReason(callId, reason, promise) + } + + override fun endCall(callId: String, promise: Promise) { + impl.endCall(callId, promise) + } + + override fun isCallTracked(callId: String): Boolean { + return impl.isCallTracked(callId) + } + + override fun hasRegisteredCall(): Boolean { + return impl.hasRegisteredCall() + } + + override fun setMutedCall(callId: String, isMuted: Boolean, promise: Promise) { + impl.setMutedCall(callId, isMuted, promise) + } + + override fun setOnHoldCall(callId: String, isOnHold: Boolean, promise: Promise) { + impl.setOnHoldCall(callId, isOnHold, promise) + } + + override fun startBackgroundTask(taskName: String, timeout: Double, promise: Promise) { + impl.startBackgroundTask(taskName, timeout, promise) + } + + override fun stopBackgroundTask(taskName: String, promise: Promise) { + impl.stopBackgroundTask(taskName, promise) + } + + override fun registerBackgroundTaskAvailable() { + impl.registerBackgroundTaskAvailable() + } + + + override fun fulfillAnswerCallAction(callId: String, didFail: Boolean) { + impl.fulfillAnswerCallAction(callId, didFail) + } + + override fun fulfillEndCallAction(callId: String, didFail: Boolean) { + impl.fulfillEndCallAction(callId, didFail) + } + + override fun log(message: String, level: String) { + impl.log(message, level) + } + + override fun stopService(promise: Promise) { + impl.stopService(promise) + } +} diff --git a/packages/react-native-callingx/android/src/newarch/java/io/getstream/rn/callingx/CallingxPackage.kt b/packages/react-native-callingx/android/src/newarch/java/io/getstream/rn/callingx/CallingxPackage.kt new file mode 100644 index 0000000000..7ec9bf29f1 --- /dev/null +++ b/packages/react-native-callingx/android/src/newarch/java/io/getstream/rn/callingx/CallingxPackage.kt @@ -0,0 +1,16 @@ +package io.getstream.rn.callingx + +import com.facebook.react.BaseReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.module.model.ReactModuleInfo +import com.facebook.react.module.model.ReactModuleInfoProvider + +class CallingPackage : BaseReactPackage() { + override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? = + if (name == CallingxModule.NAME) CallingxModule(reactContext) else null + + override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider { + mapOf(CallingxModule.NAME to ReactModuleInfo(CallingxModule.NAME, CallingxModule.NAME, false, false, false, true)) + } +} diff --git a/packages/react-native-callingx/android/src/oldarch/java/io/getstream/rn/callingx/CallingxModule.kt b/packages/react-native-callingx/android/src/oldarch/java/io/getstream/rn/callingx/CallingxModule.kt new file mode 100644 index 0000000000..83d79f213e --- /dev/null +++ b/packages/react-native-callingx/android/src/oldarch/java/io/getstream/rn/callingx/CallingxModule.kt @@ -0,0 +1,187 @@ +package io.getstream.rn.callingx + +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.WritableArray +import com.facebook.react.bridge.WritableMap +import com.facebook.react.module.annotations.ReactModule +import com.facebook.react.modules.core.DeviceEventManagerModule + +@ReactModule(name = CallingxModule.NAME) +class CallingxModule(private val reactContext: ReactApplicationContext) : + ReactContextBaseJavaModule(reactContext), CallingxEventEmitterAdapter { + + companion object { + const val NAME = CallingxModuleImpl.NAME + } + + private val impl = CallingxModuleImpl(reactContext, this) + + override fun emitNewEvent(value: WritableMap) { + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit("onNewEvent", value) + } + + override fun getName(): String = NAME + + override fun initialize() { + super.initialize() + impl.initialize() + } + + override fun invalidate() { + impl.invalidate() + super.invalidate() + } + + @ReactMethod + fun setupiOS(options: ReadableMap) { + // leave empty + } + + @ReactMethod + fun setupAndroid(options: ReadableMap) { + impl.setupAndroid(options) + } + + @ReactMethod(isBlockingSynchronousMethod = true) + fun canPostNotifications(): Boolean { + return impl.canPostNotifications() + } + + @ReactMethod + fun setShouldRejectCallWhenBusy(shouldReject: Boolean) { + impl.setShouldRejectCallWhenBusy(shouldReject) + } + + @ReactMethod(isBlockingSynchronousMethod = true) + fun getInitialVoipEvents(): WritableArray { + // leave empty + return Arguments.createArray() + } + + @ReactMethod + fun registerVoipToken() { + // leave empty + } + + @ReactMethod(isBlockingSynchronousMethod = true) + fun getInitialEvents(): WritableArray { + return impl.getInitialEvents() + } + + @ReactMethod + fun setCurrentCallActive(callId: String, promise: Promise) { + impl.setCurrentCallActive(callId, promise) + } + + @ReactMethod + fun displayIncomingCall( + callId: String, + phoneNumber: String, + callerName: String, + hasVideo: Boolean, + displayOptions: ReadableMap?, + promise: Promise + ) { + impl.displayIncomingCall(callId, phoneNumber, callerName, hasVideo, displayOptions, promise) + } + + @ReactMethod + fun answerIncomingCall(callId: String, promise: Promise) { + impl.answerIncomingCall(callId, promise) + } + + @ReactMethod + fun startCall( + callId: String, + phoneNumber: String, + callerName: String, + hasVideo: Boolean, + displayOptions: ReadableMap?, + promise: Promise + ) { + impl.startCall(callId, phoneNumber, callerName, hasVideo, displayOptions, promise) + } + + @ReactMethod + fun updateDisplay( + callId: String, + phoneNumber: String, + callerName: String, + displayOptions: ReadableMap?, + promise: Promise + ) { + impl.updateDisplay(callId, phoneNumber, callerName, displayOptions, promise) + } + + @ReactMethod + fun endCallWithReason(callId: String, reason: Double, promise: Promise) { + impl.endCallWithReason(callId, reason, promise) + } + + @ReactMethod + fun endCall(callId: String, promise: Promise) { + impl.endCall(callId, promise) + } + + @ReactMethod(isBlockingSynchronousMethod = true) + fun isCallTracked(callId: String): Boolean { + return impl.isCallTracked(callId) + } + + @ReactMethod(isBlockingSynchronousMethod = true) + fun hasRegisteredCall(): Boolean { + return impl.hasRegisteredCall() + } + + @ReactMethod + fun setMutedCall(callId: String, isMuted: Boolean, promise: Promise) { + impl.setMutedCall(callId, isMuted, promise) + } + + @ReactMethod + fun setOnHoldCall(callId: String, isOnHold: Boolean, promise: Promise) { + impl.setOnHoldCall(callId, isOnHold, promise) + } + + @ReactMethod + fun startBackgroundTask(taskName: String, timeout: Double, promise: Promise) { + impl.startBackgroundTask(taskName, timeout, promise) + } + + @ReactMethod + fun stopBackgroundTask(taskName: String, promise: Promise) { + impl.stopBackgroundTask(taskName, promise) + } + + @ReactMethod + fun registerBackgroundTaskAvailable() { + impl.registerBackgroundTaskAvailable() + } + + @ReactMethod + fun fulfillAnswerCallAction(callId: String, didFail: Boolean) { + impl.fulfillAnswerCallAction(callId, didFail) + } + + @ReactMethod + fun fulfillEndCallAction(callId: String, didFail: Boolean) { + impl.fulfillEndCallAction(callId, didFail) + } + + @ReactMethod + fun log(message: String, level: String) { + impl.log(message, level) + } + + @ReactMethod + fun stopService(promise: Promise) { + impl.stopService(promise) + } +} diff --git a/packages/react-native-callingx/android/src/oldarch/java/io/getstream/rn/callingx/CallingxPackage.kt b/packages/react-native-callingx/android/src/oldarch/java/io/getstream/rn/callingx/CallingxPackage.kt new file mode 100644 index 0000000000..c8de117fb3 --- /dev/null +++ b/packages/react-native-callingx/android/src/oldarch/java/io/getstream/rn/callingx/CallingxPackage.kt @@ -0,0 +1,16 @@ +package io.getstream.rn.callingx + +import com.facebook.react.ReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.uimanager.ViewManager + +class CallingPackage : ReactPackage { + override fun createNativeModules(reactContext: ReactApplicationContext): List { + return listOf(CallingxModule(reactContext)) + } + + override fun createViewManagers(reactContext: ReactApplicationContext): List> { + return emptyList() + } +} diff --git a/packages/react-native-callingx/babel.config.js b/packages/react-native-callingx/babel.config.js new file mode 100644 index 0000000000..0c05fd6963 --- /dev/null +++ b/packages/react-native-callingx/babel.config.js @@ -0,0 +1,12 @@ +module.exports = { + overrides: [ + { + exclude: /\/node_modules\//, + presets: ['module:react-native-builder-bob/babel-preset'], + }, + { + include: /\/node_modules\//, + presets: ['module:@react-native/babel-preset'], + }, + ], +}; diff --git a/packages/react-native-callingx/ios/AudioSessionManager.swift b/packages/react-native-callingx/ios/AudioSessionManager.swift new file mode 100644 index 0000000000..f9d456d8e6 --- /dev/null +++ b/packages/react-native-callingx/ios/AudioSessionManager.swift @@ -0,0 +1,41 @@ +import Foundation +import AVFoundation +import stream_react_native_webrtc + +@objcMembers public class AudioSessionManager: NSObject { + + public static func createAudioSessionIfNeeded() { + #if DEBUG + NSLog("%@","[Callingx][createAudioSessionIfNeeded] Creating audio session") + #endif + + let categoryOptions: AVAudioSession.CategoryOptions + #if compiler(>=6.2) // For Xcode 26.0+ + categoryOptions = [.allowBluetoothHFP, .defaultToSpeaker] + #else + categoryOptions = [.allowBluetooth, .defaultToSpeaker] + #endif + let mode: AVAudioSession.Mode = .voiceChat + + // Configure RTCAudioSessionConfiguration to match our intended settings + // This ensures WebRTC's internal state stays consistent during interruptions/route changes + let rtcConfig = RTCAudioSessionConfiguration.webRTC() + rtcConfig.category = AVAudioSession.Category.playAndRecord.rawValue + rtcConfig.mode = mode.rawValue + rtcConfig.categoryOptions = categoryOptions + RTCAudioSessionConfiguration.setWebRTC(rtcConfig) + + // Apply settings via RTCAudioSession (with lock) to keep WebRTC internal state consistent + let rtcSession = RTCAudioSession.sharedInstance() + rtcSession.lockForConfiguration() + defer { rtcSession.unlockForConfiguration() } + + do { + try rtcSession.setConfiguration(rtcConfig) + } catch { + #if DEBUG + NSLog("%@","[Callingx][createAudioSessionIfNeeded] Error configuring audio session: \(error)") + #endif + } + } +} diff --git a/packages/react-native-callingx/ios/Callingx.mm b/packages/react-native-callingx/ios/Callingx.mm new file mode 100644 index 0000000000..54f9c5d780 --- /dev/null +++ b/packages/react-native-callingx/ios/Callingx.mm @@ -0,0 +1,608 @@ +#ifdef RCT_NEW_ARCH_ENABLED +#import +#endif + +#import +#import +#import + +#import +#import +#import "WebRTCModule.h" + +// Import Swift generated header +#if __has_include("Callingx-Swift.h") +#import "Callingx-Swift.h" +#else +#import +#endif + +// MARK: - Callingx Interface + +#ifdef RCT_NEW_ARCH_ENABLED +@interface Callingx : NativeCallingxSpecBase +#else +@interface Callingx : RCTEventEmitter +#endif + +@property (nonatomic, strong) CXCallController *callKeepCallController; +@property (nonatomic, strong) CXProvider *callKeepProvider; + +@end + +@implementation Callingx { + CallingxImpl *_moduleImpl; +} + +#pragma mark - Singleton + ++ (id)allocWithZone:(NSZone *)zone { + static Callingx *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [super allocWithZone:zone]; + }); + return sharedInstance; +} + +#pragma mark - Module Registration + ++ (BOOL)requiresMainQueueSetup { + return YES; +} + +#ifdef RCT_NEW_ARCH_ENABLED ++ (NSString *)moduleName { + return @"Callingx"; +} +#else +RCT_EXPORT_MODULE(Callingx) +#endif + +#pragma mark - Class Methods (Public API) + ++ (void)reportNewIncomingCall:(NSString *)callId + handle:(NSString *)handle + handleType:(NSString *)handleType + hasVideo:(BOOL)hasVideo + localizedCallerName:(NSString *_Nullable)localizedCallerName + supportsHolding:(BOOL)supportsHolding + supportsDTMF:(BOOL)supportsDTMF + supportsGrouping:(BOOL)supportsGrouping + supportsUngrouping:(BOOL)supportsUngrouping + payload:(NSDictionary *_Nullable)payload + withCompletionHandler:(void (^_Nullable)(void))completion { + + [CallingxImpl reportNewIncomingCallWithCallId:callId + handle:handle + handleType:handleType + hasVideo:hasVideo + localizedCallerName:localizedCallerName + supportsHolding:supportsHolding + supportsDTMF:supportsDTMF + supportsGrouping:supportsGrouping + supportsUngrouping:supportsUngrouping + payload:payload + completion:completion + resolve:nil + reject:nil + ]; +} + ++ (BOOL)canRegisterCall { + return [CallingxImpl canRegisterCall]; +} + ++ (void)endCall:(NSString *)callId reason:(int)reason { + [CallingxImpl endCall:callId reason:reason]; +} + +#pragma mark - Instance Lifecycle + +- (instancetype)init { + if (self = [super init]) { + _moduleImpl = [CallingxImpl getSharedInstance]; + _moduleImpl.eventEmitter = self; + + [VoipNotificationsManager shared].eventEmitter = self; + } + return self; +} + +- (void)dealloc { + _moduleImpl = nil; +} + +#pragma mark - Old Arch Event Support + +#ifndef RCT_NEW_ARCH_ENABLED +- (NSArray *)supportedEvents { + return @[@"onNewEvent", @"onNewVoipEvent"]; +} +#endif + +#pragma mark - Turbo Module (New Arch Only) + +#ifdef RCT_NEW_ARCH_ENABLED +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params { + return std::make_shared(params); +} +#endif + +#pragma mark - Event Emission + +- (void)emitEvent:(NSDictionary *)dictionary { +#ifdef RCT_NEW_ARCH_ENABLED + [self emitOnNewEvent:dictionary]; +#else + [self sendEventWithName:@"onNewEvent" body:dictionary]; +#endif +} + +- (void)emitVoipEvent:(NSDictionary *)dictionary { +#ifdef RCT_NEW_ARCH_ENABLED + [self emitOnNewVoipEvent:dictionary]; +#else + [self sendEventWithName:@"onNewVoipEvent" body:dictionary]; +#endif +} + +#pragma mark - Internal Helpers + +- (void)_setupiOSWithOptions:(NSDictionary *)optionsDict { + [_moduleImpl setupWithOptions:optionsDict]; + + // Inject WebRTCModule so CallingxImpl can access AudioDeviceModule. + // self.bridge is NOT available on TurboModules — use currentBridge instead, + // which returns the real RCTBridge or RCTBridgeProxy (bridgeless interop). + WebRTCModule *webrtcModule = [[RCTBridge currentBridge] moduleForName:@"WebRTCModule"]; + _moduleImpl.webRTCModule = webrtcModule; + + self.callKeepCallController = _moduleImpl.callKeepCallController; + self.callKeepProvider = _moduleImpl.callKeepProvider; +} + +#pragma mark - setupiOS + +#ifdef RCT_NEW_ARCH_ENABLED +- (void)setupiOS:(JS::NativeCallingx::SpecSetupiOSOptions &)options { + NSDictionary *optionsDict = @{ + @"supportsVideo" : @(options.supportsVideo()), + @"maximumCallsPerCallGroup" : @(options.maximumCallsPerCallGroup()), + @"maximumCallGroups" : @(options.maximumCallGroups()), + @"handleType" : options.handleType(), + @"ringtoneSound" : options.sound(), + @"imageName" : options.imageName(), + @"includesCallsInRecents" : @(options.callsHistory()), + @"displayCallTimeout" : @(options.displayCallTimeout()) + }; + + [self _setupiOSWithOptions:optionsDict]; +} +#else +RCT_EXPORT_METHOD(setupiOS:(NSDictionary *)options) { + NSDictionary *optionsDict = @{ + @"supportsVideo" : options[@"supportsVideo"] ?: @(NO), + @"maximumCallsPerCallGroup" : options[@"maximumCallsPerCallGroup"] ?: @(1), + @"maximumCallGroups" : options[@"maximumCallGroups"] ?: @(1), + @"handleType" : options[@"handleType"] ?: @"generic", + @"ringtoneSound" : options[@"sound"] ?: @"", + @"imageName" : options[@"imageName"] ?: @"", + @"includesCallsInRecents" : options[@"callsHistory"] ?: @(NO), + @"displayCallTimeout" : options[@"displayCallTimeout"] ?: @(0) + }; + + [self _setupiOSWithOptions:optionsDict]; +} +#endif + +#pragma mark - setupAndroid + +#ifdef RCT_NEW_ARCH_ENABLED +- (void)setupAndroid:(JS::NativeCallingx::SpecSetupAndroidOptions &)options { + // iOS only - leave empty +} +#else +RCT_EXPORT_METHOD(setupAndroid:(NSDictionary *)options) { + // iOS only - leave empty +} +#endif + +#pragma mark - stopService + +#ifdef RCT_NEW_ARCH_ENABLED +- (void)stopService:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + // Not implemented on iOS + resolve(@YES); +} +#else +RCT_EXPORT_METHOD(stopService:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + // Not implemented on iOS + resolve(@YES); +} + +#endif +#pragma mark - setShouldRejectCallWhenBusy + +#ifdef RCT_NEW_ARCH_ENABLED +- (void)setShouldRejectCallWhenBusy:(BOOL)shouldReject { + [Settings setShouldRejectCallWhenBusy:shouldReject]; +} +#else +RCT_EXPORT_METHOD(setShouldRejectCallWhenBusy:(BOOL)shouldReject) { + [Settings setShouldRejectCallWhenBusy:shouldReject]; +} +#endif + +#pragma mark - getInitialEvents + +#ifdef RCT_NEW_ARCH_ENABLED +- (NSArray *)getInitialEvents { + return [_moduleImpl getInitialEvents]; +} +#else +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getInitialEvents) { + return [_moduleImpl getInitialEvents]; +} +#endif + +#pragma mark - getInitialVoipEvents + +#ifdef RCT_NEW_ARCH_ENABLED +- (NSArray *)getInitialVoipEvents { + return [[VoipNotificationsManager shared] getInitialEvents]; +} +#else +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getInitialVoipEvents) { + return [[VoipNotificationsManager shared] getInitialEvents]; +} +#endif + +#pragma mark - registerVoipToken + +#ifdef RCT_NEW_ARCH_ENABLED +- (void)registerVoipToken { + [[VoipNotificationsManager shared] registerVoipToken]; +} +#else +RCT_EXPORT_METHOD(registerVoipToken) { + [[VoipNotificationsManager shared] registerVoipToken]; +} +#endif + +#pragma mark - answerIncomingCall + +#ifdef RCT_NEW_ARCH_ENABLED +- (void)answerIncomingCall:(nonnull NSString *)callId + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + BOOL result = [_moduleImpl answerIncomingCall:callId]; + resolve(@(result)); +} +#else +RCT_EXPORT_METHOD(answerIncomingCall:(NSString *)callId + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + BOOL result = [_moduleImpl answerIncomingCall:callId]; + resolve(@(result)); +} +#endif + +#pragma mark - displayIncomingCall + +#ifdef RCT_NEW_ARCH_ENABLED +- (void)displayIncomingCall:(nonnull NSString *)callId + phoneNumber:(nonnull NSString *)phoneNumber + callerName:(nonnull NSString *)callerName + hasVideo:(BOOL)hasVideo + displayOptions:(JS::NativeCallingx::SpecDisplayIncomingCallDisplayOptions &)displayOptions + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + [_moduleImpl displayIncomingCallWithCallId:callId + phoneNumber:phoneNumber + callerName:callerName + hasVideo:hasVideo + resolve:resolve + reject:reject + ]; +} +#else +RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)callId + phoneNumber:(NSString *)phoneNumber + callerName:(NSString *)callerName + hasVideo:(BOOL)hasVideo + displayOptions:(NSDictionary *)displayOptions + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + [_moduleImpl displayIncomingCallWithCallId:callId + phoneNumber:phoneNumber + callerName:callerName + hasVideo:hasVideo + resolve:resolve + reject:reject + ]; +} +#endif + +#pragma mark - endCallWithReason + +#ifdef RCT_NEW_ARCH_ENABLED +- (void)endCallWithReason:(nonnull NSString *)callId + reason:(double)reason + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + [CallingxImpl endCall:callId reason:(int)reason]; + resolve(@YES); +} +#else +RCT_EXPORT_METHOD(endCallWithReason:(NSString *)callId + reason:(double)reason + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + [CallingxImpl endCall:callId reason:(int)reason]; + resolve(@YES); +} +#endif + +#pragma mark - endCall + +#ifdef RCT_NEW_ARCH_ENABLED +- (void)endCall:(nonnull NSString *)callId + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + BOOL result = [_moduleImpl endCall:callId]; + resolve(@(result)); +} +#else +RCT_EXPORT_METHOD(endCall:(NSString *)callId + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + BOOL result = [_moduleImpl endCall:callId]; + resolve(@(result)); +} +#endif + +#pragma mark - isCallRegistered + +#ifdef RCT_NEW_ARCH_ENABLED +- (NSNumber *)isCallTracked:(nonnull NSString *)callId { + return @([_moduleImpl isCallTracked:callId]); +} +#else +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(isCallTracked:(NSString *)callId) { + return @([_moduleImpl isCallTracked:callId]); +} +#endif + +#pragma mark - hasRegisteredCall + +#ifdef RCT_NEW_ARCH_ENABLED +- (NSNumber *)hasRegisteredCall { + return @([CallingxImpl hasRegisteredCall]); +} +#else +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(hasRegisteredCall) { + return @([CallingxImpl hasRegisteredCall]); +} +#endif + +#pragma mark - setCurrentCallActive + +#ifdef RCT_NEW_ARCH_ENABLED +- (void)setCurrentCallActive:(nonnull NSString *)callId + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + BOOL result = [_moduleImpl setCurrentCallActive:callId]; + resolve(@(result)); +} +#else +RCT_EXPORT_METHOD(setCurrentCallActive:(NSString *)callId + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + BOOL result = [_moduleImpl setCurrentCallActive:callId]; + resolve(@(result)); +} +#endif + +#pragma mark - setMutedCall + +#ifdef RCT_NEW_ARCH_ENABLED +- (void)setMutedCall:(nonnull NSString *)callId + isMuted:(BOOL)isMuted + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + BOOL result = [_moduleImpl setMutedCall:callId isMuted:isMuted]; + resolve(@(result)); +} +#else +RCT_EXPORT_METHOD(setMutedCall:(NSString *)callId + isMuted:(BOOL)isMuted + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + BOOL result = [_moduleImpl setMutedCall:callId isMuted:isMuted]; + resolve(@(result)); +} +#endif + +#pragma mark - setOnHoldCall + +#ifdef RCT_NEW_ARCH_ENABLED +- (void)setOnHoldCall:(nonnull NSString *)callId + isOnHold:(BOOL)isOnHold + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + BOOL result = [_moduleImpl setOnHoldCall:callId isOnHold:isOnHold]; + resolve(@(result)); +} +#else +RCT_EXPORT_METHOD(setOnHoldCall:(NSString *)callId + isOnHold:(BOOL)isOnHold + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + BOOL result = [_moduleImpl setOnHoldCall:callId isOnHold:isOnHold]; + resolve(@(result)); +} +#endif + +#pragma mark - startCall + +#ifdef RCT_NEW_ARCH_ENABLED +- (void)startCall:(nonnull NSString *)callId + phoneNumber:(nonnull NSString *)phoneNumber + callerName:(nonnull NSString *)callerName + hasVideo:(BOOL)hasVideo + displayOptions:(JS::NativeCallingx::SpecStartCallDisplayOptions &)displayOptions + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + [_moduleImpl startCallWithCallId:callId + phoneNumber:phoneNumber + callerName:callerName + hasVideo:hasVideo]; + resolve(@YES); +} +#else +RCT_EXPORT_METHOD(startCall:(NSString *)callId + phoneNumber:(NSString *)phoneNumber + callerName:(NSString *)callerName + hasVideo:(BOOL)hasVideo + displayOptions:(NSDictionary *)displayOptions + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + [_moduleImpl startCallWithCallId:callId + phoneNumber:phoneNumber + callerName:callerName + hasVideo:hasVideo]; + resolve(@YES); +} +#endif + +#pragma mark - updateDisplay + +#ifdef RCT_NEW_ARCH_ENABLED +- (void)updateDisplay:(nonnull NSString *)callId + phoneNumber:(nonnull NSString *)phoneNumber + callerName:(nonnull NSString *)callerName + displayOptions:(JS::NativeCallingx::SpecUpdateDisplayDisplayOptions &)displayOptions + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + BOOL result = [_moduleImpl updateDisplayWithCallId:callId + phoneNumber:phoneNumber + callerName:callerName]; + resolve(@(result)); +} +#else +RCT_EXPORT_METHOD(updateDisplay:(NSString *)callId + phoneNumber:(NSString *)phoneNumber + callerName:(NSString *)callerName + displayOptions:(NSDictionary *)displayOptions + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + BOOL result = [_moduleImpl updateDisplayWithCallId:callId + phoneNumber:phoneNumber + callerName:callerName]; + resolve(@(result)); +} +#endif + +#pragma mark - fulfillAnswerCallAction + +#ifdef RCT_NEW_ARCH_ENABLED +- (void)fulfillAnswerCallAction:(NSString *)callId didFail:(BOOL)didFail { + [_moduleImpl fulfillAnswerCallAction:callId didFail:didFail]; +} +#else +RCT_EXPORT_METHOD(fulfillAnswerCallAction:(NSString *)callId didFail:(BOOL)didFail) { + [_moduleImpl fulfillAnswerCallAction:callId didFail:didFail]; +} +#endif + +#pragma mark - fulfillEndCallAction + +#ifdef RCT_NEW_ARCH_ENABLED +- (void)fulfillEndCallAction:(NSString *)callId didFail:(BOOL)didFail { + [_moduleImpl fulfillEndCallAction:callId didFail:didFail]; +} +#else +RCT_EXPORT_METHOD(fulfillEndCallAction:(NSString *)callId didFail:(BOOL)didFail) { + [_moduleImpl fulfillEndCallAction:callId didFail:didFail]; +} +#endif + +#pragma mark - log + +#ifdef RCT_NEW_ARCH_ENABLED +- (void)log:(NSString *)message level:(NSString *)level { + NSLog(@"[Callingx][log] %@, %@", message, level); +} +#else +RCT_EXPORT_METHOD(log:(NSString *)message level:(NSString *)level) { + NSLog(@"[Callingx][log] %@, %@", message, level); +} +#endif + +#pragma mark - startBackgroundTask + +#ifdef RCT_NEW_ARCH_ENABLED +- (void)startBackgroundTask:(NSString *)taskName + timeout:(double)timeout + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + // Not implemented on iOS + resolve(@YES); +} +#else +RCT_EXPORT_METHOD(startBackgroundTask:(NSString *)taskName + timeout:(double)timeout + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + // Not implemented on iOS + resolve(@YES); +} +#endif + +#pragma mark - stopBackgroundTask + +#ifdef RCT_NEW_ARCH_ENABLED +- (void)stopBackgroundTask:(NSString *)taskName + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + // Not implemented on iOS + resolve(@YES); +} +#else +RCT_EXPORT_METHOD(stopBackgroundTask:(NSString *)taskName + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + // Not implemented on iOS + resolve(@YES); +} +#endif + +#pragma mark - registerBackgroundTaskAvailable + +#ifdef RCT_NEW_ARCH_ENABLED +- (void)registerBackgroundTaskAvailable { + // Not implemented on iOS - background tasks work differently on iOS +} +#else +RCT_EXPORT_METHOD(registerBackgroundTaskAvailable) { + // Not implemented on iOS - background tasks work differently on iOS +} +#endif + +#pragma mark - canPostNotifications + +#ifdef RCT_NEW_ARCH_ENABLED +- (nonnull NSNumber *)canPostNotifications { + return @YES; +} +#else +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(canPostNotifications) { + return @YES; +} +#endif + +@end diff --git a/packages/react-native-callingx/ios/CallingxCall.swift b/packages/react-native-callingx/ios/CallingxCall.swift new file mode 100644 index 0000000000..da94667d6f --- /dev/null +++ b/packages/react-native-callingx/ios/CallingxCall.swift @@ -0,0 +1,105 @@ +import Foundation + +/// Represents a call tracked by the CallingxImpl module. +/// Holds per-call lifecycle state to guard against duplicate actions +/// and to track timestamps for CallKit reporting. +@objcMembers public class CallingxCall: NSObject { + public let uuid: UUID + public let cid: String + public let isOutgoing: Bool + + // MARK: - Action-source flags + // These flags track whether an action was initiated from the app (vs system UI). + // They follow a "set-then-read-then-reset" pattern in the CXProviderDelegate methods + // to determine the event source ("app" vs "sys") before being reset. + + /// Whether answerIncomingCall was initiated from the app (vs system UI) + public private(set) var isSelfAnswered: Bool = false + /// Whether endCall was initiated from the app (vs system UI) + public private(set) var isSelfEnded: Bool = false + /// Whether setMutedCall was initiated from the app (vs system UI) + public private(set) var isSelfMuted: Bool = false + + // MARK: - Lifecycle timestamps + + /// When the call started connecting (outgoing: maps to reportOutgoingCall(startedConnectingAt:); + /// incoming: set when answerIncomingCall is called, internal-only) + public private(set) var startedConnectingAt: Date? + /// When the call became connected (outgoing: maps to reportOutgoingCall(connectedAt:); + /// incoming: set when CXAnswerCallAction delegate fires, internal-only) + public private(set) var connectedAt: Date? + /// When the call ended + public private(set) var endedAt: Date? + + // MARK: - Derived states + + public var hasStartedConnecting: Bool { startedConnectingAt != nil } + public var isConnected: Bool { connectedAt != nil } + public var hasEnded: Bool { endedAt != nil } + /// Whether the call has been answered (incoming) or started connecting (outgoing). + /// Checks both startedConnectingAt (set by answerIncomingCall from app) and connectedAt + /// (set by CXAnswerCallAction delegate, which fires even when answered from system UI). + /// Used as the primary guard against duplicate answerIncomingCall invocations. + public var isAnswered: Bool { startedConnectingAt != nil || connectedAt != nil } + + // MARK: - Initialization + + public init(uuid: UUID, cid: String, isOutgoing: Bool = false) { + self.uuid = uuid + self.cid = cid + self.isOutgoing = isOutgoing + } + + // MARK: - Action-source flag methods + + public func markSelfAnswered() { isSelfAnswered = true } + public func markSelfEnded() { isSelfEnded = true } + public func markSelfMuted() { isSelfMuted = true } + + public func resetSelfAnswered() { isSelfAnswered = false } + public func resetSelfEnded() { isSelfEnded = false } + public func resetSelfMuted() { isSelfMuted = false } + + /// Resets all action-source flags. Called when a CXTransaction fails. + public func resetAllSelfFlags() { + isSelfAnswered = false + isSelfEnded = false + isSelfMuted = false + } + + // MARK: - Lifecycle transition methods + + public func markStartedConnecting() { + if startedConnectingAt == nil { + startedConnectingAt = Date() + } + } + + public func markConnected() { + if connectedAt == nil { + connectedAt = Date() + } + } + + public func markEnded() { + if endedAt == nil { + endedAt = Date() + } + } + + // MARK: - Debug description + + public override var description: String { + let state: String + if hasEnded { + state = "ended" + } else if isConnected { + state = "connected" + } else if hasStartedConnecting { + state = "connecting" + } else { + state = "ringing" + } + return "CallingxCall(cid: \(cid), uuid: \(uuid.uuidString.lowercased()), outgoing: \(isOutgoing), state: \(state))" + } +} diff --git a/packages/react-native-callingx/ios/CallingxImpl.swift b/packages/react-native-callingx/ios/CallingxImpl.swift new file mode 100644 index 0000000000..f0b81280a0 --- /dev/null +++ b/packages/react-native-callingx/ios/CallingxImpl.swift @@ -0,0 +1,953 @@ +import Foundation +import CallKit +import AVFoundation +import UIKit +import stream_react_native_webrtc + +// MARK: - Event Names +@objcMembers public class CallingxEvents: NSObject { + public static let didReceiveStartCallAction = "didReceiveStartCallAction" + public static let didToggleHoldAction = "didToggleHoldCallAction" + public static let didPerformSetMutedCallAction = "didPerformSetMutedCallAction" + public static let didChangeAudioRoute = "didChangeAudioRoute" + public static let didDisplayIncomingCall = "didDisplayIncomingCall" + public static let didActivateAudioSession = "didActivateAudioSession" + public static let didDeactivateAudioSession = "didDeactivateAudioSession" + public static let performAnswerCallAction = "answerCall" + public static let performEndCallAction = "endCall" + public static let performPlayDTMFCallAction = "didPerformDTMFAction" + public static let providerReset = "providerReset" +} + +// MARK: - Event Emitter Protocol +@objc public protocol CallingxEventEmitter { + func emitEvent(_ dictionary: [String: Any]) +} + +// MARK: - Callingx Implementation +@objc public class CallingxImpl: NSObject, CXProviderDelegate { + + // MARK: - Shared State + @objc public static var sharedProvider: CXProvider? + @objc public static var uuidStorage: UUIDStorage? + @objc public static var sharedInstance: CallingxImpl? + /// Events stored before the module instance exists (e.g. VoIP from killed state). Drained in getInitialEvents(). + private static var delayedEvents: [[String: Any]] = [] + + // MARK: - Instance Properties + @objc public var callKeepCallController: CXCallController? + @objc public var callKeepProvider: CXProvider? + @objc public weak var eventEmitter: CallingxEventEmitter? + @objc public weak var webRTCModule: WebRTCModule? + + private var canSendEvents: Bool = false + private var isSetup: Bool = false + + // Pending CXActions awaiting JS fulfillment + private var pendingAnswerActions: [String: (action: CXAnswerCallAction, enqueuedAt: DispatchTime)] = [:] + private var pendingEndActions: [String: (action: CXEndCallAction, enqueuedAt: DispatchTime)] = [:] + private let pendingActionsQueue = DispatchQueue(label: "io.getstream.callingx.pendingActions") + // a large timeout to accomodate for cold start + metro server load time + private let pendingActionTimeoutSeconds = 30 + + @objc public static func getSharedInstance() -> CallingxImpl { + if sharedInstance == nil { + sharedInstance = CallingxImpl() + } + + return sharedInstance! + } + + // MARK: - Initialization + @objc public override init() { + super.init() + + isSetup = false + canSendEvents = false + + NotificationCenter.default.addObserver( + self, + selector: #selector(onAudioRouteChange(_:)), + name: AVAudioSession.routeChangeNotification, + object: nil + ) + + CallingxImpl.sharedInstance = self + + if CallingxImpl.uuidStorage == nil { + CallingxImpl.uuidStorage = UUIDStorage() + } + + if CallingxImpl.sharedProvider == nil { + CallingxImpl.sharedProvider = CXProvider(configuration: Settings.getProviderConfiguration()) + } + + callKeepProvider = CallingxImpl.sharedProvider + callKeepProvider?.setDelegate(nil, queue: nil) + callKeepProvider?.setDelegate(self, queue: nil) + } + + deinit { + NotificationCenter.default.removeObserver(self) + + callKeepProvider?.setDelegate(nil, queue: nil) + callKeepProvider?.invalidate() + CallingxImpl.sharedProvider = nil + canSendEvents = false + isSetup = false + } + + // MARK: - Class Methods + @objc public static func initializeIfNeeded() { + _ = getSharedInstance() // ensures the shared instance is created and CXProvider delegate is set + } + + @objc public static func reportNewIncomingCall( + callId: String, + handle: String, + handleType: String, + hasVideo: Bool, + localizedCallerName: String?, + supportsHolding: Bool, + supportsDTMF: Bool, + supportsGrouping: Bool, + supportsUngrouping: Bool, + payload: [String: Any]?, + completion: (() -> Void)?, + resolve: RCTPromiseResolveBlock?, + reject: RCTPromiseRejectBlock? + ) { + initializeIfNeeded() + + guard let storage = uuidStorage else { return } + + if storage.containsCid(callId) { + #if DEBUG + NSLog("%@","[Callingx][reportNewIncomingCall] callId already exists") + #endif + completion?() + resolve?(true) + return + } + + let cxHandleType = Settings.getHandleType(handleType) + let uuid = storage.getOrCreateUUID(forCid: callId) + let callUpdate = CXCallUpdate() + callUpdate.remoteHandle = CXHandle(type: cxHandleType, value: handle) + callUpdate.supportsHolding = supportsHolding + callUpdate.supportsDTMF = supportsDTMF + callUpdate.supportsGrouping = supportsGrouping + callUpdate.supportsUngrouping = supportsUngrouping + callUpdate.hasVideo = hasVideo + callUpdate.localizedCallerName = localizedCallerName + + sharedProvider?.reportNewIncomingCall(with: uuid, update: callUpdate) { error in + #if DEBUG + NSLog("%@","[Callingx][reportNewIncomingCall] callId = \(callId), error = \(String(describing: error))") + #endif + + let errorCode = error != nil ? CallingxImpl.getIncomingCallErrorCode(error!) : "" + + let body = [ + "error": error?.localizedDescription ?? "", + "errorCode": errorCode, + "callId": callId, + "handle": handle, + "localizedCallerName": localizedCallerName ?? "", + "hasVideo": hasVideo ? "1" : "0", + "supportsHolding": supportsHolding ? "1" : "0", + "supportsDTMF": supportsDTMF ? "1" : "0", + "supportsGrouping": supportsGrouping ? "1" : "0", + "supportsUngrouping": supportsUngrouping ? "1" : "0", + "payload": payload ?? "" + ] + + if let instance = CallingxImpl.sharedInstance { + instance.sendEvent(CallingxEvents.didDisplayIncomingCall, body: body) + } + + if error == nil { + #if DEBUG + NSLog("%@","[Callingx][reportNewIncomingCall] success callId = \(callId)") + #endif + resolve?(true) + } else { + reject?("DISPLAY_INCOMING_CALL_ERROR", error?.localizedDescription, error) + } + + completion?() + } + } + + @objc public static func canRegisterCall() -> Bool { + let hasCall = hasRegisteredCall() + let shouldReject = Settings.getShouldRejectCallWhenBusy() + return !shouldReject || (shouldReject && !hasCall) + } + + @objc public static func hasRegisteredCall() -> Bool { + guard let storage = uuidStorage else { return false } + + let appUUIDs = storage.allUUIDs() + if appUUIDs.isEmpty { return false } + + let observer = CXCallObserver() + for call in observer.calls { + for uuid in appUUIDs { + if call.uuid == uuid { + return true + } + } + } + return false + } + + @objc public static func getAudioOutput() -> String? { + let outputs = AVAudioSession.sharedInstance().currentRoute.outputs + if !outputs.isEmpty { + return outputs[0].portType.rawValue + } + return nil + } + + @objc public static func endCall(_ callId: String, reason: Int) { + #if DEBUG + NSLog("%@","[Callingx][endCall] callId = \(callId) reason = \(reason)") + #endif + + guard let call = uuidStorage?.getCall(forCid: callId) else { + #if DEBUG + NSLog("%@","[Callingx][endCall] callId not found") + #endif + return + } + + call.markEnded() + + // CXCallEndedReason raw values: failed=1, remoteEnded=2, unanswered=3, answeredElsewhere=4, declinedElsewhere=5 + let endedReason = CXCallEndedReason(rawValue: reason) ?? .failed + + sharedProvider?.reportCall(with: call.uuid, endedAt: call.endedAt ?? Date(), reason: endedReason) + uuidStorage?.removeCid(callId) + } + + @objc public static func getIncomingCallErrorCode(_ error: Error) -> String { + let nsError = error as NSError + switch nsError.code { + case CXErrorCodeIncomingCallError.unentitled.rawValue: + return "Unentitled" + case CXErrorCodeIncomingCallError.callUUIDAlreadyExists.rawValue: + return "CallUUIDAlreadyExists" + case CXErrorCodeIncomingCallError.filteredByDoNotDisturb.rawValue: + return "FilteredByDoNotDisturb" + case CXErrorCodeIncomingCallError.filteredByBlockList.rawValue: + return "FilteredByBlockList" + default: + return "Unknown" + } + } + + // MARK: - Instance Methods + @objc public func requestTransaction(_ transaction: CXTransaction) { + #if DEBUG + NSLog("%@","[Callingx][requestTransaction] transaction = \(transaction)") + #endif + + if callKeepCallController == nil { + callKeepCallController = CXCallController() + } + + callKeepCallController?.request(transaction) { [weak self] error in + if let error = error { + #if DEBUG + NSLog("%@","[Callingx][requestTransaction] Error requesting transaction (\(transaction.actions)): (\(error))") + #endif + + // Reset per-call action-source flags for all actions in the failed transaction + for action in transaction.actions { + if let callAction = action as? CXCallAction, + let call = CallingxImpl.uuidStorage?.getCallByUUID(callAction.callUUID) { + call.resetAllSelfFlags() + } + } + } else { + #if DEBUG + NSLog("%@","[Callingx][requestTransaction] Requested transaction successfully") + #endif + + if let startCallAction = transaction.actions.first as? CXStartCallAction { + let callUpdate = CXCallUpdate() + callUpdate.remoteHandle = startCallAction.handle + callUpdate.hasVideo = startCallAction.isVideo + callUpdate.localizedCallerName = startCallAction.contactIdentifier + callUpdate.supportsDTMF = false + callUpdate.supportsHolding = false + callUpdate.supportsGrouping = false + callUpdate.supportsUngrouping = false + + self?.callKeepProvider?.reportCall(with: startCallAction.callUUID, updated: callUpdate) + } + } + } + } + + @objc public func sendEvent(_ name: String, body: [String: Any]?) { + #if DEBUG + NSLog("%@","[Callingx] sendEventWithNameWrapper: \(name)") + #endif + + let sendEventAction = { + var dictionary: [String: Any] = ["eventName": name] + if let body = body { + dictionary["params"] = body + } + + if self.canSendEvents { + self.eventEmitter?.emitEvent(dictionary) + } else { + CallingxImpl.delayedEvents.append(dictionary) + #if DEBUG + NSLog("%@","[Callingx] delayedEvents: \(CallingxImpl.delayedEvents)") + #endif + } + } + + if (Thread.isMainThread) { + sendEventAction() + } else { + DispatchQueue.main.async { + sendEventAction() + } + } + } + + @objc private func onAudioRouteChange(_ notification: Notification) { + guard let info = notification.userInfo, + let reasonValue = info[AVAudioSessionRouteChangeReasonKey] as? UInt, + let output = CallingxImpl.getAudioOutput() else { + return + } + + let params: [String: Any] = [ + "output": output, + "reason": reasonValue + ] + + sendEvent(CallingxEvents.didChangeAudioRoute, body: params) + } + + // MARK: - Setup Methods + @objc public func setup(options: [String: Any]) { + callKeepCallController = CXCallController() + + Settings.setSettings(options) + + // This is mostly needed for very first setup, as we need to override the default + // provider configuration which is set in the constructor. + // IMPORTANT: We override CXProvider instance only if there is no registered call, otherwise we may lose corrsponding call state/events from CallKit + if !CallingxImpl.hasRegisteredCall() { + let oldProvider = CallingxImpl.sharedProvider + let newProvider = CXProvider(configuration: Settings.getProviderConfiguration()) + newProvider.setDelegate(self, queue: nil) + + CallingxImpl.sharedProvider = newProvider + callKeepProvider = newProvider + + oldProvider?.setDelegate(nil, queue: nil) + oldProvider?.invalidate() + } + + isSetup = true + } + + @objc public func getInitialEvents() -> [[String: Any]] { + var events: [[String: Any]] = [] + let action = { + #if DEBUG + NSLog("%@","[Callingx][getInitialEvents] delayedEvents = \(CallingxImpl.delayedEvents)") + #endif + + events = CallingxImpl.delayedEvents + CallingxImpl.delayedEvents = [] + self.canSendEvents = true + } + + if (Thread.isMainThread) { + action() + } else { + DispatchQueue.main.sync { + action() + } + } + + return events + } + + // MARK: - Call Management + @objc public func answerIncomingCall(_ callId: String) -> Bool { + #if DEBUG + NSLog("%@","[Callingx][answerIncomingCall] callId = \(callId)") + #endif + + guard let call = CallingxImpl.uuidStorage?.getCall(forCid: callId) else { + #if DEBUG + NSLog("%@","[Callingx][answerIncomingCall] callId not found") + #endif + return false + } + + // Guard: already answered or ended — prevent duplicate CXAnswerCallAction transactions + if call.isAnswered || call.hasEnded { + #if DEBUG + NSLog("%@","[Callingx][answerIncomingCall] callId already answered/ended, skipping") + #endif + return true + } + + call.markSelfAnswered() + call.markStartedConnecting() // internal state: incoming call is now connecting + + let answerCallAction = CXAnswerCallAction(call: call.uuid) + let transaction = CXTransaction() + transaction.addAction(answerCallAction) + + requestTransaction(transaction) + return true + } + + @objc public func displayIncomingCall( + callId: String, + phoneNumber: String, + callerName: String, + hasVideo: Bool, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let uuid = CallingxImpl.uuidStorage?.getUUID(forCid: callId) + CallingxImpl.reportNewIncomingCall( + callId: callId, + handle: phoneNumber, + handleType: "generic", + hasVideo: hasVideo, + localizedCallerName: callerName, + supportsHolding: false, + supportsDTMF: false, + supportsGrouping: false, + supportsUngrouping: false, + payload: nil, + completion: nil, + resolve: resolve, + reject: reject + ) + + let wasAlreadyAnswered = uuid != nil + if !wasAlreadyAnswered { + let settings = Settings.getSettings() + if let timeout = settings["displayCallTimeout"] as? Int { + let popTime = DispatchTime.now() + .milliseconds(timeout) + DispatchQueue.main.asyncAfter(deadline: popTime) { [weak self] in + guard let self = self, !self.isSetup else { return } + #if DEBUG + NSLog("%@","[Callingx] Displayed a call without a reachable app, ending the call: \(callId)") + #endif + CallingxImpl.endCall(callId, reason: CXCallEndedReason.failed.rawValue) + } + } + } + } + + @objc public func endCall(_ callId: String) -> Bool { + #if DEBUG + NSLog("%@","[Callingx][endCall] callId = \(callId)") + #endif + + guard let call = CallingxImpl.uuidStorage?.getCall(forCid: callId) else { + #if DEBUG + NSLog("%@","[Callingx][endCall] callId not found") + #endif + return false + } + + // Guard: already ended — prevent duplicate CXEndCallAction transactions + if call.hasEnded { + #if DEBUG + NSLog("%@","[Callingx][endCall] callId already ended, skipping") + #endif + return true + } + + call.markSelfEnded() + call.markEnded() + + let endCallAction = CXEndCallAction(call: call.uuid) + let transaction = CXTransaction(action: endCallAction) + + requestTransaction(transaction) + return true + } + + @objc public func isCallTracked(_ callId: String) -> Bool { + guard let uuid = CallingxImpl.uuidStorage?.getUUID(forCid: callId) else { + #if DEBUG + NSLog("%@","[Callingx][isCallTracked] callId not found") + #endif + return false + } + + let observer = CXCallObserver() + for call in observer.calls { + if call.uuid == uuid { + return true + } + } + return false + } + + @objc public func setCurrentCallActive(_ callId: String) -> Bool { + #if DEBUG + NSLog("%@","[Callingx][setCurrentCallActive] callId = \(callId)") + #endif + + guard let call = CallingxImpl.uuidStorage?.getCall(forCid: callId) else { + #if DEBUG + NSLog("%@","[Callingx][setCurrentCallActive] callId not found") + #endif + return false + } + + call.markConnected() + + // Report connected timestamp to CallKit. + // startedConnectingAt is reported separately in the CXStartCallAction delegate. + callKeepProvider?.reportOutgoingCall(with: call.uuid, connectedAt: call.connectedAt ?? Date()) + return true + } + + @objc public func setMutedCall(_ callId: String, isMuted: Bool) -> Bool { + #if DEBUG + NSLog("%@","[Callingx][setMutedCall] muted = \(isMuted)") + #endif + + guard let call = CallingxImpl.uuidStorage?.getCall(forCid: callId) else { + #if DEBUG + NSLog("%@","[Callingx][setMutedCall] callId not found") + #endif + return false + } + + call.markSelfMuted() + let setMutedAction = CXSetMutedCallAction(call: call.uuid, muted: isMuted) + let transaction = CXTransaction() + transaction.addAction(setMutedAction) + + requestTransaction(transaction) + return true + } + + @objc public func setOnHoldCall(_ callId: String, isOnHold: Bool) -> Bool { + #if DEBUG + NSLog("%@","[Callingx][setOnHold] uuidString = \(callId), shouldHold = \(isOnHold)") + #endif + + guard let uuid = CallingxImpl.uuidStorage?.getUUID(forCid: callId) else { + #if DEBUG + NSLog("%@","[Callingx][setOnHoldCall] callId not found") + #endif + return false + } + + let setHeldCallAction = CXSetHeldCallAction(call: uuid, onHold: isOnHold) + let transaction = CXTransaction() + transaction.addAction(setHeldCallAction) + + requestTransaction(transaction) + return true + } + + @objc public func startCall( + callId: String, + phoneNumber: String, + callerName: String, + hasVideo: Bool + ) { + #if DEBUG + NSLog("%@","[Callingx][startCall] uuidString = \(callId), phoneNumber = \(phoneNumber)") + #endif + + guard let storage = CallingxImpl.uuidStorage else { return } + + if (storage.containsCid(callId)) { + #if DEBUG + NSLog("%@","[Callingx][startCall] Call \(callId) is already registered") + #endif + return + } + + let call = storage.getOrCreateCall(forCid: callId, isOutgoing: true) + call.markStartedConnecting() // outgoing: will be reported via reportOutgoingCall(startedConnectingAt:) + + let handleType = Settings.getHandleType("generic") + let callHandle = CXHandle(type: handleType, value: phoneNumber) + let startCallAction = CXStartCallAction(call: call.uuid, handle: callHandle) + startCallAction.isVideo = hasVideo + startCallAction.contactIdentifier = callerName + + let transaction = CXTransaction(action: startCallAction) + requestTransaction(transaction) + } + + @objc public func updateDisplay( + callId: String, + phoneNumber: String, + callerName: String + ) -> Bool { + #if DEBUG + NSLog("%@","[Callingx][updateDisplay] uuidString = \(callId) displayName = \(callerName) uri = \(phoneNumber)") + #endif + + guard let uuid = CallingxImpl.uuidStorage?.getUUID(forCid: callId) else { + #if DEBUG + NSLog("%@","[Callingx][updateDisplay] callId not found") + #endif + return false + } + + let handleTypeString = Settings.getSettings()["handleType"] as? String + let handleType = Settings.getHandleType(handleTypeString ?? "generic") + let callHandle = CXHandle(type: handleType, value: phoneNumber) + let callUpdate = CXCallUpdate() + callUpdate.localizedCallerName = callerName + callUpdate.remoteHandle = callHandle + + callKeepProvider?.reportCall(with: uuid, updated: callUpdate) + return true + } + + // MARK: - CXProviderDelegate + public func provider(_ provider: CXProvider, perform action: CXStartCallAction) { + #if DEBUG + NSLog("%@","[Callingx][CXProviderDelegate][provider:performStartCallAction]") + #endif + + guard let call = CallingxImpl.uuidStorage?.getCallByUUID(action.callUUID) else { + #if DEBUG + NSLog("%@","[Callingx][CXProviderDelegate][provider:performStartCallAction] callId not found") + #endif + action.fail() + return + } + + getAudioDeviceModule()?.reset() + AudioSessionManager.createAudioSessionIfNeeded() + + sendEvent(CallingxEvents.didReceiveStartCallAction, body: [ + "callId": call.cid, + "handle": action.handle.value + ]) + + action.fulfill() + + // Report startedConnectingAt to CallKit now that the action is fulfilled. + // The timestamp was set in startCall when the call was created. + callKeepProvider?.reportOutgoingCall(with: call.uuid, startedConnectingAt: call.startedConnectingAt ?? Date()) + } + + public func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) { + guard let call = CallingxImpl.uuidStorage?.getCallByUUID(action.callUUID) else { + #if DEBUG + NSLog("%@","[Callingx][CXProviderDelegate][provider:performAnswerCallAction] callId not found") + #endif + action.fail() + return + } + + #if DEBUG + NSLog("%@","[Callingx][CXProviderDelegate][provider:performAnswerCallAction] isSelfAnswered: \(call.isSelfAnswered)") + #endif + + getAudioDeviceModule()?.reset() + AudioSessionManager.createAudioSessionIfNeeded() + + let source = call.isSelfAnswered ? "app" : "sys" + sendEvent(CallingxEvents.performAnswerCallAction, body: [ + "callId": call.cid, + "source": source + ]) + + call.resetSelfAnswered() + call.markConnected() // incoming: call is now connected + + if source == "app" { + // App initiated this answer — no need to wait for JS, fulfill immediately + action.fulfill() + } else { + // System initiated — defer fulfillment until JS reports back via fulfillAnswerCallAction + let cid = call.cid + pendingActionsQueue.sync { + self.pendingAnswerActions[cid] = (action: action, enqueuedAt: DispatchTime.now()) + } + // Safety timer: auto-fail if JS never responds. + // Answer timeout = call never connected + let timeout = DispatchTime.now() + DispatchTimeInterval.seconds(pendingActionTimeoutSeconds) + pendingActionsQueue.asyncAfter(deadline: timeout) { [weak self] in + if let pending = self?.pendingAnswerActions.removeValue(forKey: cid) { + #if DEBUG + NSLog("%@","[Callingx][CXProviderDelegate][provider:performAnswerCallAction] answer timeout for callId: \(cid)") + #endif + pending.action.fail() + } + } + } + } + + public func provider(_ provider: CXProvider, perform action: CXEndCallAction) { + guard let call = CallingxImpl.uuidStorage?.getCallByUUID(action.callUUID) else { + #if DEBUG + NSLog("%@","[Callingx][CXProviderDelegate][provider:performEndCallAction] callId not found") + #endif + // End actions represent explicit user intent to close call UI. + // Fulfill stale/duplicate end actions to avoid "Call Failed" UX. + action.fulfill() + return + } + + #if DEBUG + NSLog("%@","[Callingx][CXProviderDelegate][provider:performEndCallAction] isSelfEnded: \(call.isSelfEnded)") + #endif + + let source = call.isSelfEnded ? "app" : "sys" + sendEvent(CallingxEvents.performEndCallAction, body: [ + "callId": call.cid, + "source": source + ]) + + call.resetSelfEnded() + call.markEnded() + CallingxImpl.uuidStorage?.removeCid(call.cid) + + if source == "app" { + // App initiated this end — no need to wait for JS, fulfill immediately + action.fulfill() + } else { + // System initiated — defer fulfillment until JS reports back via fulfillEndCallAction + let cid = call.cid + pendingActionsQueue.sync { + self.pendingEndActions[cid] = (action: action, enqueuedAt: DispatchTime.now()) + } + // Safety timer: auto-fulfill if JS never responds. + let timeout = DispatchTime.now() + DispatchTimeInterval.seconds(pendingActionTimeoutSeconds) + pendingActionsQueue.asyncAfter(deadline: timeout) { [weak self] in + if let pending = self?.pendingEndActions.removeValue(forKey: cid) { + #if DEBUG + NSLog("%@","[Callingx][CXProviderDelegate][provider:performEndCallAction] end timeout for callId: \(cid)") + #endif + pending.action.fulfill() + } + } + } + } + + public func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) { + #if DEBUG + NSLog("%@","[Callingx][CXProviderDelegate][provider:performSetHeldCallAction]") + #endif + + guard let callId = CallingxImpl.uuidStorage?.getCid(forUUID: action.callUUID) else { + #if DEBUG + NSLog("%@","[Callingx][CXProviderDelegate][provider:performSetHeldCallAction] callId not found") + #endif + action.fail() + return + } + + sendEvent(CallingxEvents.didToggleHoldAction, body: [ + "hold": action.isOnHold, + "callId": callId + ]) + + action.fulfill() + } + + public func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) { + guard let call = CallingxImpl.uuidStorage?.getCallByUUID(action.callUUID) else { + #if DEBUG + NSLog("%@","[Callingx][CXProviderDelegate][provider:performSetMutedCallAction] callId not found") + #endif + action.fail() + return + } + + let isAppInitiated = call.isSelfMuted + call.resetSelfMuted() + + #if DEBUG + NSLog("%@","[Callingx][CXProviderDelegate][provider:performSetMutedCallAction] \(action.isMuted) isAppInitiated: \(isAppInitiated)") + #endif + + // Only send the event to JS when the mute was initiated by the system + // (e.g. user tapped mute on the native CallKit UI). + // Skip app-initiated actions to prevent the feedback loop: + // app mutes mic → setMutedCall → CallKit delegate → event to JS → mic toggle → loop + if !isAppInitiated { + sendEvent(CallingxEvents.didPerformSetMutedCallAction, body: [ + "muted": action.isMuted, + "callId": call.cid + ]) + } + + action.fulfill() + } + + public func provider(_ provider: CXProvider, perform action: CXPlayDTMFCallAction) { + #if DEBUG + NSLog("%@","[Callingx][CXProviderDelegate][provider:performPlayDTMFCallAction]") + #endif + + guard let callId = CallingxImpl.uuidStorage?.getCid(forUUID: action.callUUID) else { + #if DEBUG + NSLog("%@","[Callingx][CXProviderDelegate][provider:performPlayDTMFCallAction] callId not found") + #endif + action.fail() + return + } + + sendEvent(CallingxEvents.performPlayDTMFCallAction, body: [ + "digits": action.digits, + "callId": callId + ]) + + action.fulfill() + } + + public func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) { + #if DEBUG + NSLog("%@","[Callingx][CXProviderDelegate][provider:didActivateAudioSession] category=\(audioSession.category) mode=\(audioSession.mode)") + #endif + + // When CallKit activates the AVAudioSession, inform WebRTC as well. + RTCAudioSession.sharedInstance().audioSessionDidActivate(audioSession) + + // Enable wake lock to keep the device awake during the call + DispatchQueue.main.async { + UIApplication.shared.isIdleTimerDisabled = true + } + + sendEvent(CallingxEvents.didActivateAudioSession, body: nil) + } + + public func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) { + #if DEBUG + NSLog("%@","[Callingx][CXProviderDelegate][provider:didDeactivateAudioSession] category=\(audioSession.category) mode=\(audioSession.mode)") + #endif + + // When CallKit deactivates the AVAudioSession, inform WebRTC as well. + RTCAudioSession.sharedInstance().audioSessionDidDeactivate(audioSession) + getAudioDeviceModule()?.reset() + + // Disable wake lock when the call ends + DispatchQueue.main.async { + UIApplication.shared.isIdleTimerDisabled = false + } + + sendEvent(CallingxEvents.didDeactivateAudioSession, body: nil) + } + + public func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) { + // note: in practice we should never be getting this callback as we already have a pending timeout set. + // in our tests callkit timesout and exectutes this method in approximately 60 seconds. + #if DEBUG + NSLog("%@","[Callingx][CXProviderDelegate][provider:timedOutPerformingAction]") + #endif + + guard let callAction = action as? CXCallAction else { + return + } + + pendingActionsQueue.sync { + // cid mapping as soon as end is initiated, so cleanup by matching callUUID. + if let answerEntry = pendingAnswerActions.first(where: { $0.value.action.callUUID == callAction.callUUID }) { + pendingAnswerActions.removeValue(forKey: answerEntry.key) + let elapsedMs = elapsedMilliseconds(since: answerEntry.value.enqueuedAt) + #if DEBUG + NSLog("%@","[Callingx][CXProviderDelegate][provider:timedOutPerformingAction] removed pending answer action for callId: \(answerEntry.key), elapsedMs=\(elapsedMs)") + #endif + } + + if let endEntry = pendingEndActions.first(where: { $0.value.action.callUUID == callAction.callUUID }) { + pendingEndActions.removeValue(forKey: endEntry.key) + let elapsedMs = elapsedMilliseconds(since: endEntry.value.enqueuedAt) + #if DEBUG + NSLog("%@","[Callingx][CXProviderDelegate][provider:timedOutPerformingAction] removed pending end action for callId: \(endEntry.key), elapsedMs=\(elapsedMs)") + #endif + } + } + } + + public func providerDidReset(_ provider: CXProvider) { + #if DEBUG + NSLog("%@","[Callingx][providerDidReset]") + #endif + + // Clear any pending actions to prevent memory leaks. + // After a provider reset, all pending CXActions are invalid. + pendingActionsQueue.sync { + pendingAnswerActions.removeAll() + pendingEndActions.removeAll() + } + + sendEvent(CallingxEvents.providerReset, body: nil) + } + + // MARK: - Pending Action Fulfillment + + @objc public func fulfillAnswerCallAction(_ callId: String, didFail: Bool) { + pendingActionsQueue.sync { [weak self] in + guard let pending = self?.pendingAnswerActions.removeValue(forKey: callId) else { + #if DEBUG + NSLog("%@","[Callingx][fulfillAnswerCallAction] action not found for callId: \(callId)") + #endif + return + } + let elapsedMs = elapsedMilliseconds(since: pending.enqueuedAt) + #if DEBUG + NSLog("%@","[Callingx][fulfillAnswerCallAction] callId: \(callId), didFail: \(didFail), elapsedMs=\(elapsedMs)") + #endif + if didFail { pending.action.fail() } else { pending.action.fulfill() } + } + } + + @objc public func fulfillEndCallAction(_ callId: String, didFail: Bool) { + pendingActionsQueue.sync { [weak self] in + guard let pending = self?.pendingEndActions.removeValue(forKey: callId) else { + #if DEBUG + NSLog("%@","[Callingx][fulfillEndCallAction] action not found for callId: \(callId)") + #endif + return + } + let elapsedMs = elapsedMilliseconds(since: pending.enqueuedAt) + #if DEBUG + NSLog("%@","[Callingx][fulfillEndCallAction] callId: \(callId), didFail: \(didFail), elapsedMs=\(elapsedMs)") + #endif + if didFail { pending.action.fail() } else { pending.action.fulfill() } + } + } + + // MARK: - Helper Methods + private func elapsedMilliseconds(since start: DispatchTime) -> Int { + let nowNs = DispatchTime.now().uptimeNanoseconds + let startNs = start.uptimeNanoseconds + guard nowNs >= startNs else { return 0 } + return Int((nowNs - startNs) / 1_000_000) + } + + private func getAudioDeviceModule() -> AudioDeviceModule? { + guard let adm = webRTCModule?.audioDeviceModule else { + #if DEBUG + NSLog("%@","[Callingx] WebRTCModule is not available. Ensure it was injected from the TurboModule host.") + #endif + return nil + } + return adm + } +} + diff --git a/packages/react-native-callingx/ios/CallingxPublic.h b/packages/react-native-callingx/ios/CallingxPublic.h new file mode 100644 index 0000000000..320b4993ce --- /dev/null +++ b/packages/react-native-callingx/ios/CallingxPublic.h @@ -0,0 +1,76 @@ +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Callingx - React Native Turbo Module for CallKit integration + * + * This header exposes the public API for use in AppDelegate or other native code. + * + * Usage in AppDelegate.m: + * ``` + * #import + * + * [Callingx reportNewIncomingCall:callId + * handle:handle + * handleType:@"number" + * hasVideo:YES + * localizedCallerName:callerName + * supportsHolding:NO + * supportsDTMF:NO + * supportsGrouping:NO + * supportsUngrouping:NO + * payload:payload + * withCompletionHandler:^(void){ }]; + * ``` + */ +@interface Callingx : NSObject + +/** + * Report a new incoming call to CallKit. + * Call this from your PushKit delegate when receiving a VoIP push notification. + * + * @param callId Unique identifier for the call (e.g., call ID from your backend) + * @param handle The phone number or identifier to display + * @param handleType Type of handle: "number", "email", or "generic" + * @param hasVideo Whether this is a video call + * @param localizedCallerName The caller's display name + * @param supportsHolding Whether the call supports being put on hold + * @param supportsDTMF Whether the call supports DTMF tones + * @param supportsGrouping Whether the call can be grouped with other calls + * @param supportsUngrouping Whether the call can be ungrouped + * @param payload Optional payload data from the push notification + * @param completion Completion handler called after the call is reported, with an error if the call could not be displayed + */ ++ (void)reportNewIncomingCall:(NSString *)callId + handle:(NSString *)handle + handleType:(NSString *)handleType + hasVideo:(BOOL)hasVideo + localizedCallerName:(NSString * _Nullable)localizedCallerName + supportsHolding:(BOOL)supportsHolding + supportsDTMF:(BOOL)supportsDTMF + supportsGrouping:(BOOL)supportsGrouping + supportsUngrouping:(BOOL)supportsUngrouping + payload:(NSDictionary * _Nullable)payload + withCompletionHandler:(void (^_Nullable)(void))completion; + +/** + * End a call with a specific reason. + * + * @param callId The call ID to end + * @param reason The reason for ending: 1=failed, 2=remoteEnded, 3=unanswered, 4=answeredElsewhere, 5=declinedElsewhere + */ ++ (void)endCall:(NSString *)callId + reason:(int)reason; + +/** + * Check if a new call can be registered (based on shouldRejectCallWhenBusy setting) + * + * @return YES if a new call can be registered, NO if should be rejected + */ ++ (BOOL)canRegisterCall; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native-callingx/ios/Settings.swift b/packages/react-native-callingx/ios/Settings.swift new file mode 100644 index 0000000000..4bca4f89e5 --- /dev/null +++ b/packages/react-native-callingx/ios/Settings.swift @@ -0,0 +1,108 @@ +import Foundation +import CallKit +import UIKit + +@objcMembers public class Settings: NSObject { + private static let settingsKey = "CallingxSettings" + + public static func getSettings() -> [String: Any] { + return UserDefaults.standard.dictionary(forKey: settingsKey) ?? [:] + } + + public static func setSettings(_ options: [String: Any]?) { + #if DEBUG + NSLog("%@","[Settings][setSettings] options = \(String(describing: options))") + #endif + + var settings: [String: Any] = getSettings() + + if let options = options { + for (key, value) in options { + settings[key] = value + } + } + + UserDefaults.standard.set(settings, forKey: settingsKey) + UserDefaults.standard.synchronize() + } + + public static func getShouldRejectCallWhenBusy() -> Bool { + guard let shouldReject = getSettings()["shouldRejectCallWhenBusy"] as? Bool else { + return false + } + return shouldReject + } + + public static func setShouldRejectCallWhenBusy(_ shouldReject: Bool) { + setSettings(["shouldRejectCallWhenBusy": shouldReject]) + } + + public static func getProviderConfiguration() -> CXProviderConfiguration { + #if DEBUG + NSLog("%@","[Settings][getProviderConfiguration]") + #endif + + let settings = getSettings() + let providerConfiguration = CXProviderConfiguration() + providerConfiguration.supportsVideo = true + providerConfiguration.maximumCallGroups = 1 + providerConfiguration.maximumCallsPerCallGroup = 1 + providerConfiguration.supportedHandleTypes = getSupportedHandleTypes(settings["handleType"]) + + if let supportsVideo = settings["supportsVideo"] as? Bool { + providerConfiguration.supportsVideo = supportsVideo + } + if let maximumCallGroups = settings["maximumCallGroups"] as? Int { + providerConfiguration.maximumCallGroups = maximumCallGroups + } + if let maximumCallsPerCallGroup = settings["maximumCallsPerCallGroup"] as? Int { + providerConfiguration.maximumCallsPerCallGroup = maximumCallsPerCallGroup + } + + if let imageName = settings["imageName"] as? String, !imageName.isEmpty { + if let image = UIImage(named: imageName) { + providerConfiguration.iconTemplateImageData = image.pngData() + } + } + + if let ringtoneSound = settings["ringtoneSound"] as? String, !ringtoneSound.isEmpty { + providerConfiguration.ringtoneSound = ringtoneSound + } + + if let includesCallsInRecents = settings["includesCallsInRecents"] as? Bool { + providerConfiguration.includesCallsInRecents = includesCallsInRecents + } + + return providerConfiguration + } + + public static func getSupportedHandleTypes(_ handleType: Any?) -> Set { + if let handleTypeArray = handleType as? [String] { + var types = Set() + for type in handleTypeArray { + types.insert(getHandleType(type)) + } + return types + } else if let handleTypeString = handleType as? String { + let type = getHandleType(handleTypeString) + return Set([type]) + } else { + return Set([CXHandle.HandleType.phoneNumber]) + } + } + + public static func getHandleType(_ handleType: String?) -> CXHandle.HandleType { + guard let handleType = handleType else { return .generic } + + switch handleType { + case "generic": + return .generic + case "number", "phone": + return .phoneNumber + case "email": + return .emailAddress + default: + return .generic + } + } +} diff --git a/packages/react-native-callingx/ios/UUIDStorage.swift b/packages/react-native-callingx/ios/UUIDStorage.swift new file mode 100644 index 0000000000..3ecb0e7e96 --- /dev/null +++ b/packages/react-native-callingx/ios/UUIDStorage.swift @@ -0,0 +1,169 @@ +import Foundation + +@objcMembers public class UUIDStorage: NSObject { + /// Primary storage: cid -> CallingxCall + private var callsByCid: [String: CallingxCall] = [:] + /// Reverse lookup: lowercased UUID string -> CallingxCall + private var callsByUUID: [String: CallingxCall] = [:] + private let queue = DispatchQueue(label: "com.stream.uuidstorage", attributes: []) + + public override init() { + super.init() + } + + // MARK: - CallingxCall-based API (new) + + /// Returns the existing call for the given cid, or creates a new one. + public func getOrCreateCall(forCid cid: String, isOutgoing: Bool = false) -> CallingxCall { + return queue.sync { + if let existing = callsByCid[cid] { + #if DEBUG + NSLog("%@","[UUIDStorage] getOrCreateCall: found existing \(existing)") + #endif + return existing + } + + let uuid = UUID() + let call = CallingxCall(uuid: uuid, cid: cid, isOutgoing: isOutgoing) + let uuidString = uuid.uuidString.lowercased() + callsByCid[cid] = call + callsByUUID[uuidString] = call + #if DEBUG + NSLog("%@","[UUIDStorage] getOrCreateCall: created \(call)") + #endif + return call + } + } + + /// Returns the call for the given cid, or nil if not found. + public func getCall(forCid cid: String) -> CallingxCall? { + return queue.sync { + return callsByCid[cid] + } + } + + /// Returns the call for the given UUID, or nil if not found. + public func getCallByUUID(_ uuid: UUID) -> CallingxCall? { + return queue.sync { + let uuidString = uuid.uuidString.lowercased() + return callsByUUID[uuidString] + } + } + + // MARK: - Legacy API (preserved for backward compatibility) + + public func allUUIDs() -> [UUID] { + return queue.sync { + return callsByCid.values.map { $0.uuid } + } + } + + /// Returns the existing UUID for the given cid, or creates a new CallingxCall and returns its UUID. + public func getOrCreateUUID(forCid cid: String) -> UUID { + return queue.sync { + if let existing = callsByCid[cid] { + #if DEBUG + NSLog("%@","[UUIDStorage] getUUIDForCid: found existing UUID \(existing.uuid.uuidString.lowercased()) for cid \(cid)") + #endif + return existing.uuid + } + + let uuid = UUID() + let call = CallingxCall(uuid: uuid, cid: cid, isOutgoing: false) + let uuidString = uuid.uuidString.lowercased() + callsByCid[cid] = call + callsByUUID[uuidString] = call + #if DEBUG + NSLog("%@","[UUIDStorage] getUUIDForCid: created new UUID \(uuidString) for cid \(cid)") + #endif + return uuid + } + } + + public func getUUID(forCid cid: String) -> UUID? { + return queue.sync { + return callsByCid[cid]?.uuid + } + } + + public func getCid(forUUID uuid: UUID) -> String? { + return queue.sync { + let uuidString = uuid.uuidString.lowercased() + let cid = callsByUUID[uuidString]?.cid + #if DEBUG + NSLog("%@","[UUIDStorage] getCidForUUID: UUID \(uuidString) -> cid \(cid ?? "(not found)")") + #endif + return cid + } + } + + public func removeCid(forUUID uuid: UUID) { + queue.sync { + let uuidString = uuid.uuidString.lowercased() + if let call = callsByUUID[uuidString] { + callsByCid.removeValue(forKey: call.cid) + callsByUUID.removeValue(forKey: uuidString) + #if DEBUG + NSLog("%@","[UUIDStorage] removeCidForUUID: removed cid \(call.cid) for UUID \(uuidString)") + #endif + } else { + #if DEBUG + NSLog("%@","[UUIDStorage] removeCidForUUID: no cid found for UUID \(uuidString)") + #endif + } + } + } + + public func removeCid(_ cid: String) { + queue.sync { + if let call = callsByCid[cid] { + let uuidString = call.uuid.uuidString.lowercased() + callsByUUID.removeValue(forKey: uuidString) + callsByCid.removeValue(forKey: cid) + #if DEBUG + NSLog("%@","[UUIDStorage] removeCid: removed cid \(cid) with UUID \(uuidString)") + #endif + } else { + #if DEBUG + NSLog("%@","[UUIDStorage] removeCid: no UUID found for cid \(cid)") + #endif + } + } + } + + public func removeAllObjects() { + queue.sync { + let count = callsByCid.count + callsByCid.removeAll() + callsByUUID.removeAll() + #if DEBUG + NSLog("%@","[UUIDStorage] removeAllObjects: cleared \(count) entries") + #endif + } + } + + public func count() -> Int { + return queue.sync { + return callsByCid.count + } + } + + public func containsCid(_ cid: String) -> Bool { + return queue.sync { + return callsByCid[cid] != nil + } + } + + public func containsUUID(_ uuid: UUID) -> Bool { + return queue.sync { + return callsByUUID[uuid.uuidString.lowercased()] != nil + } + } + + public override var description: String { + return queue.sync { + let entries = callsByCid.map { "\($0.key): \($0.value)" }.joined(separator: ", ") + return "UUIDStorage: [\(entries)]" + } + } +} diff --git a/packages/react-native-callingx/ios/VoipNotificationsManager.swift b/packages/react-native-callingx/ios/VoipNotificationsManager.swift new file mode 100644 index 0000000000..248523be25 --- /dev/null +++ b/packages/react-native-callingx/ios/VoipNotificationsManager.swift @@ -0,0 +1,180 @@ +import Foundation +import PushKit +import React + +@objcMembers public class VoipNotificationsEvents: NSObject { + public static let registered = "voipNotificationsRegistered" + public static let newNotification = "voipNotificationReceived" +} + +typealias RNVoipPushNotificationCompletion = () -> Void + +@objc public protocol VoipNotificationsEventEmitter { + func emitVoipEvent(_ dictionary: [String: Any]) +} + +@objc public class VoipNotificationsManager: NSObject { + + @objc public weak var eventEmitter: VoipNotificationsEventEmitter? + + private static var isVoipRegistered = false + private static var lastVoipToken = "" + private static var voipRegistry: PKPushRegistry? + + private var canSendEvents: Bool = false + private var delayedEvents: [[String:Any]] = [] + + private static var sharedInstance: VoipNotificationsManager? + + @objc public static func shared() -> VoipNotificationsManager { + if sharedInstance == nil { + sharedInstance = VoipNotificationsManager() + } + return sharedInstance! + } + + @objc public override init() { + super.init() + + canSendEvents = false + delayedEvents = [] + + if VoipNotificationsManager.sharedInstance == nil { + VoipNotificationsManager.sharedInstance = self + } + } + + deinit { + NotificationCenter.default.removeObserver(self) + + canSendEvents = false + delayedEvents = [] + } + + // MARK: - Class Methods + + @objc public static func voipRegistration() { + if isVoipRegistered { + #if DEBUG + NSLog("%@","[VoipNotificationsManager] voipRegistration is already registered. return _lastVoipToken = \(lastVoipToken)") + #endif + let voipPushManager = VoipNotificationsManager.shared() + voipPushManager.sendEventWithNameWrapper(name: VoipNotificationsEvents.registered, body: ["token": lastVoipToken]) + } else { + #if DEBUG + NSLog("%@","[VoipNotificationsManager] voipRegistration enter") + #endif + DispatchQueue.main.async { + let voipRegistry = PKPushRegistry(queue: DispatchQueue.main) + // Set the registry's delegate to AppDelegate + // Note: The original code casts the delegate, but this should be handled by AppDelegate + if let appDelegate = RCTSharedApplication()?.delegate as? PKPushRegistryDelegate { + voipRegistry.delegate = appDelegate + // Set the push type to VoIP + // Store the registry to prevent deallocation + voipRegistry.desiredPushTypes = [.voIP] + VoipNotificationsManager.voipRegistry = voipRegistry + + isVoipRegistered = true + } else { + #if DEBUG + NSLog("%@","[VoipNotificationsManager] voipRegistration appDelegate not found. return") + #endif + } + } + } + } + + @objc public static func didUpdatePushCredentials(_ credentials: PKPushCredentials, forType type: String) { + #if DEBUG + NSLog("%@","[VoipNotificationsManager] didUpdatePushCredentials credentials.token = \(credentials.token), type = \(type)") + #endif + + let voipTokenLength = credentials.token.count + if voipTokenLength == 0 { + return + } + + lastVoipToken = credentials.token.map { String(format: "%02x", $0) }.joined() + + let voipPushManager = VoipNotificationsManager.shared() + voipPushManager.sendEventWithNameWrapper(name: VoipNotificationsEvents.registered, body: ["token": lastVoipToken]) + } + + @objc public static func didReceiveIncomingPushWithPayload(_ payload: PKPushPayload, forType type: String) { + #if DEBUG + NSLog("%@","[VoipNotificationsManager] didReceiveIncomingPushWithPayload payload.dictionaryPayload = \(payload.dictionaryPayload), type = \(type)") + #endif + + let dictionaryPayload: [String: Any] = Dictionary(uniqueKeysWithValues: payload.dictionaryPayload.map { (key, value) in + (String(describing: key), value) + }) + + let voipPushManager = VoipNotificationsManager.shared() + voipPushManager.sendEventWithNameWrapper(name: VoipNotificationsEvents.newNotification, body: dictionaryPayload) + } + + // MARK: - React Native Methods + @objc public func getInitialEvents() -> [[String: Any]] { + var events: [[String: Any]] = [] + let action = { + #if DEBUG + NSLog("%@","[VoipNotificationsManager][getInitialEvents] delayedEvents = \(self.delayedEvents)") + #endif + + events = self.delayedEvents + self.delayedEvents = [] + self.canSendEvents = true + } + + if (Thread.isMainThread) { + action() + } else { + DispatchQueue.main.sync { + action() + } + } + return events + } + + @objc public func registerVoipToken() { + if RCTRunningInAppExtension() { + return + } + DispatchQueue.main.async { + VoipNotificationsManager.voipRegistration() + } + } + + private func sendEventWithNameWrapper(name: String, body: [String: Any]?) { + #if DEBUG + NSLog("%@","[VoipNotificationsManager] sendEventWithNameWrapper: \(name)") + #endif + + let sendEventAction = { + var dictionary: [String: Any] = ["eventName": name] + if let body = body { + dictionary["params"] = body + } + + if self.canSendEvents { + self.eventEmitter?.emitVoipEvent(dictionary) + } else { + self.delayedEvents.append(dictionary) + #if DEBUG + NSLog("%@","[VoipNotificationsManager] delayedEvents: \(self.delayedEvents)") + #endif + } + } + + if (Thread.isMainThread) { + sendEventAction() + } else { + DispatchQueue.main.async { + sendEventAction() + } + } + + } +} + diff --git a/packages/react-native-callingx/package.json b/packages/react-native-callingx/package.json new file mode 100644 index 0000000000..3ad1f7364e --- /dev/null +++ b/packages/react-native-callingx/package.json @@ -0,0 +1,114 @@ +{ + "name": "@stream-io/react-native-callingx", + "version": "0.1.0", + "description": "CallKit and Telecom API capabilities for React Native", + "main": "./dist/module/index.js", + "module": "./dist/module/index.js", + "types": "./dist/typescript/src/index.d.ts", + "react-native": "src/index", + "source": "src/index", + "exports": { + ".": { + "source": "./src/index.ts", + "types": "./dist/typescript/src/index.d.ts", + "default": "./dist/module/index.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "src", + "dist", + "android", + "ios", + "cpp", + "*.podspec", + "react-native.config.js", + "!ios/build", + "!android/build", + "!android/gradle", + "!android/gradlew", + "!android/gradlew.bat", + "!android/local.properties", + "!**/__tests__", + "!**/__fixtures__", + "!**/__mocks__", + "!**/.*" + ], + "scripts": { + "clean": "del-cli android/build dist", + "prepare": "bob build", + "build": "bob build", + "typecheck": "tsc" + }, + "keywords": [ + "react-native", + "ios", + "android" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/GetStream/stream-video-js.git" + }, + "author": "https://getstream.io (https://github.com/GetStream)", + "license": "MIT", + "bugs": { + "url": "https://github.com/GetStream/stream-video-js/issues" + }, + "homepage": "https://github.com/GetStream/stream-video-js#readme", + "publishConfig": { + "registry": "https://registry.npmjs.org/" + }, + "devDependencies": { + "@react-native-community/cli": "20.0.1", + "@react-native/babel-preset": "^0.81.5", + "@stream-io/react-native-webrtc": "137.1.3", + "@types/react": "^19.1.0", + "del-cli": "^6.0.0", + "react": "19.1.0", + "react-native": "^0.81.5", + "react-native-builder-bob": "^0.40.15", + "typescript": "^5.9.2" + }, + "peerDependencies": { + "@react-native-firebase/app": ">=23.0.0", + "@react-native-firebase/messaging": ">=23.0.0", + "@stream-io/react-native-webrtc": ">=137.1.2", + "react": "*", + "react-native": "*" + }, + "react-native-builder-bob": { + "source": "src", + "output": "dist", + "targets": [ + [ + "module", + { + "esm": true + } + ], + [ + "typescript", + { + "project": "tsconfig.build.json" + } + ] + ] + }, + "codegenConfig": { + "name": "CallingxSpec", + "type": "modules", + "jsSrcsDir": "src/spec", + "android": { + "javaPackageName": "io.getstream.rn.callingx" + }, + "ios": { + "moduleName": "Callingx" + } + }, + "create-react-native-library": { + "languages": "kotlin-objc", + "type": "turbo-module", + "tools": [], + "version": "0.55.0" + } +} diff --git a/packages/react-native-callingx/project.json b/packages/react-native-callingx/project.json new file mode 100644 index 0000000000..59831da016 --- /dev/null +++ b/packages/react-native-callingx/project.json @@ -0,0 +1,63 @@ +{ + "name": "@stream-io/react-native-callingx", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "targets": { + "version": { + "executor": "@jscutlery/semver:version", + "options": { + "dryRun": false, + "preset": { + "name": "conventionalcommits", + "preMajor": true, + "types": [ + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Bug Fixes" }, + { "type": "chore", "hidden": true }, + { "type": "docs", "hidden": true }, + { "type": "style", "hidden": true }, + { "type": "refactor", "hidden": true }, + { "type": "perf", "section": "Features" }, + { "type": "test", "hidden": true } + ] + }, + "trackDeps": true, + "push": true, + "skipCommitTypes": ["chore", "ci", "docs"], + "postTargets": [ + "@stream-io/react-native-callingx:build", + "@stream-io/react-native-callingx:github", + "@stream-io/react-native-callingx:publish" + ] + } + }, + "build": { + "executor": "nx:run-commands", + "options": { + "commands": [ + { + "command": "yarn build:react-native-callingx", + "forwardAllArgs": false + } + ] + } + }, + "github": { + "executor": "@jscutlery/semver:github", + "options": { + "tag": "${tag}", + "notes": "${notes}" + } + }, + "publish": { + "executor": "nx:run-commands", + "options": { + "commands": [ + { + "command": "yarn release:react-native-callingx", + "forwardAllArgs": false + } + ] + } + } + } +} diff --git a/packages/react-native-callingx/react-native.config.js b/packages/react-native-callingx/react-native.config.js new file mode 100644 index 0000000000..21b3b68dd7 --- /dev/null +++ b/packages/react-native-callingx/react-native.config.js @@ -0,0 +1,8 @@ +module.exports = { + dependency: { + platforms: { + android: {}, + ios: {}, + }, + }, +}; diff --git a/packages/react-native-callingx/src/CallingxModule.ts b/packages/react-native-callingx/src/CallingxModule.ts new file mode 100644 index 0000000000..e2060ebe76 --- /dev/null +++ b/packages/react-native-callingx/src/CallingxModule.ts @@ -0,0 +1,292 @@ +import { Platform } from 'react-native'; +import NativeCallingModule from './spec/NativeCallingx'; +import { + HEADLESS_TASK_NAME, + registerHeadlessTask, + setHeadlessTask, +} from './utils/headlessTask'; +import type { ManagableTask } from './utils/headlessTask'; +import { EventManager } from './EventManager'; +import type { EventListener } from './EventManager'; +import { + type ICallingxModule, + type InfoDisplayOptions, + type EndCallReason, + type EventData, + type EventName, + type EventParams, + type CallingExpOptions, + type VoipEventName, + type VoipEventParams, + type VoipEventData, +} from './types'; +import { + androidEndCallReasonMap, + defaultAndroidOptions, + defaultiOSOptions, + iosEndCallReasonMap, +} from './utils/constants'; +import { isVoipEvent } from './utils/utils'; + +class CallingxModule implements ICallingxModule { + private _isSetup = false; + private _isOngoingCallsEnabled = false; + private _isHeadlessTaskRegistered = false; + + private titleTransformer: (memberName: string, incoming: boolean) => string = + (memberName: string) => memberName; + + private eventManager: EventManager = + new EventManager(); + private voipEventManager: EventManager = + new EventManager(); + + get canPostNotifications(): boolean { + if (Platform.OS !== 'android') { + return true; + } + + return NativeCallingModule.canPostNotifications(); + } + + get isOngoingCallsEnabled(): boolean { + return this._isOngoingCallsEnabled; + } + + get isSetup(): boolean { + return this._isSetup; + } + + setup(options: CallingExpOptions): void { + if (this._isSetup) { + return; + } + + this._isOngoingCallsEnabled = options.enableOngoingCalls ?? false; + this.setShouldRejectCallWhenBusy(options.shouldRejectCallWhenBusy ?? false); + + if (Platform.OS === 'ios') { + NativeCallingModule.setupiOS({ ...defaultiOSOptions, ...options.ios }); + } + + if (Platform.OS === 'android') { + const { + titleTransformer, + incomingChannel, + ongoingChannel, + notificationTexts, + } = options.android ?? {}; + + this.titleTransformer = + titleTransformer ?? ((memberName: string) => memberName); + + const notificationsConfig = { + incomingChannel: { + ...defaultAndroidOptions.incomingChannel, + ...(incomingChannel ?? {}), + }, + ongoingChannel: { + ...defaultAndroidOptions.ongoingChannel, + ...(ongoingChannel ?? {}), + }, + notificationTexts, + }; + + if ( + notificationsConfig.incomingChannel.id === + notificationsConfig.ongoingChannel.id + ) { + throw new Error('Incoming and outgoing channel IDs cannot be the same'); + } + + NativeCallingModule.setupAndroid(notificationsConfig); + + registerHeadlessTask(); + } + + this._isSetup = true; + } + + setShouldRejectCallWhenBusy(shouldReject: boolean): void { + NativeCallingModule.setShouldRejectCallWhenBusy(shouldReject); + } + + getInitialEvents(): EventData[] { + return NativeCallingModule.getInitialEvents() as EventData[]; + } + + getInitialVoipEvents(): VoipEventData[] { + return NativeCallingModule.getInitialVoipEvents() as VoipEventData[]; + } + + //activates call that was registered with the telecom stack + setCurrentCallActive(callId: string): Promise { + return NativeCallingModule.setCurrentCallActive(callId); + } + + displayIncomingCall( + callId: string, + phoneNumber: string, + callerName: string, + hasVideo: boolean, + ): Promise { + const displayOptions: InfoDisplayOptions = { + displayTitle: this.titleTransformer(callerName, true), + }; + return NativeCallingModule.displayIncomingCall( + callId, + phoneNumber, + callerName, + hasVideo, + displayOptions, + ); + } + + answerIncomingCall(callId: string): Promise { + return NativeCallingModule.answerIncomingCall(callId); + } + + //registers call with the telecom stack + startCall( + callId: string, + phoneNumber: string, + callerName: string, + hasVideo: boolean, + ): Promise { + const displayOptions: InfoDisplayOptions = { + displayTitle: this.titleTransformer(callerName, false), + }; + return NativeCallingModule.startCall( + callId, + phoneNumber, + callerName, + hasVideo, + displayOptions, + ); + } + + updateDisplay( + callId: string, + phoneNumber: string, + callerName: string, + incoming: boolean, + ): Promise { + const displayOptions: InfoDisplayOptions = { + displayTitle: this.titleTransformer(callerName, incoming), + }; + return NativeCallingModule.updateDisplay( + callId, + phoneNumber, + callerName, + displayOptions, + ); + } + + endCallWithReason(callId: string, reason: EndCallReason): Promise { + const reasons = + Platform.OS === 'ios' ? iosEndCallReasonMap : androidEndCallReasonMap; + + if (Platform.OS === 'ios' && reason === 'local') { + return NativeCallingModule.endCall(callId); + } + + return NativeCallingModule.endCallWithReason(callId, reasons[reason]); + } + + isCallTracked(callId: string): boolean { + return NativeCallingModule.isCallTracked(callId); + } + + hasRegisteredCall(): boolean { + return NativeCallingModule.hasRegisteredCall(); + } + + setMutedCall(callId: string, isMuted: boolean): Promise { + return NativeCallingModule.setMutedCall(callId, isMuted); + } + + setOnHoldCall(callId: string, isOnHold: boolean): Promise { + return NativeCallingModule.setOnHoldCall(callId, isOnHold); + } + + registerBackgroundTask(taskProvider: ManagableTask): void { + const stopTask = () => { + this._isHeadlessTaskRegistered = false; + NativeCallingModule.stopBackgroundTask(HEADLESS_TASK_NAME); + }; + + setHeadlessTask((taskData: any) => taskProvider(taskData, stopTask)); + + this._isHeadlessTaskRegistered = true; + NativeCallingModule.registerBackgroundTaskAvailable(); + } + + async startBackgroundTask(taskProvider?: ManagableTask): Promise { + // If taskProvider is provided, register it first + if (taskProvider) { + this.registerBackgroundTask(taskProvider); + } + + // Check if task is registered + if (!this._isHeadlessTaskRegistered) { + throw new Error( + 'Background task not registered. Call registerBackgroundTask first.', + ); + } + + return NativeCallingModule.startBackgroundTask(HEADLESS_TASK_NAME, 0); + } + + stopBackgroundTask(): Promise { + this._isHeadlessTaskRegistered = false; + return NativeCallingModule.stopBackgroundTask(HEADLESS_TASK_NAME); + } + + fulfillAnswerCallAction(callId: string, didFail: boolean): void { + NativeCallingModule.fulfillAnswerCallAction(callId, didFail); + } + + fulfillEndCallAction(callId: string, didFail: boolean): void { + NativeCallingModule.fulfillEndCallAction(callId, didFail); + } + + registerVoipToken(): void { + NativeCallingModule.registerVoipToken(); + } + + stopService(): Promise { + return NativeCallingModule.stopService(); + } + + addEventListener( + eventName: T, + callback: EventListener< + T extends EventName + ? EventParams[T] + : T extends VoipEventName + ? VoipEventParams[T] + : never + >, + ): { remove: () => void } { + type ManagerType = EventManager; + + const manager: ManagerType = ( + isVoipEvent(eventName) ? this.voipEventManager : this.eventManager + ) as ManagerType; + + manager.addListener(eventName, callback as any); + + return { + remove: () => { + manager.removeListener(eventName, callback as any); + }, + }; + } + + log(message: string, level: 'debug' | 'info' | 'warn' | 'error'): void { + NativeCallingModule.log(message, level); + } +} + +const module = new CallingxModule(); +export default module; diff --git a/packages/react-native-callingx/src/EventManager.ts b/packages/react-native-callingx/src/EventManager.ts new file mode 100644 index 0000000000..3d63a7c9da --- /dev/null +++ b/packages/react-native-callingx/src/EventManager.ts @@ -0,0 +1,74 @@ +import { NativeEventEmitter, type EventSubscription } from 'react-native'; +import NativeCallingModule from './spec/NativeCallingx'; +import type { + EventData, + EventName, + VoipEventData, + VoipEventName, +} from './types'; +import { isVoipEvent, isTurboModuleEnabled } from './utils/utils'; + +type EventListener = (params: T) => void; + +class EventManager { + private listenersCount: number = 0; + private eventListeners: Map[]> = new Map(); + private subscription: EventSubscription | null = null; + + addListener( + eventName: T, + callback: EventListener, + ): void { + const listeners = this.eventListeners.get(eventName) || []; + listeners.push(callback as EventListener); + this.eventListeners.set(eventName, listeners); + this.listenersCount++; + + if (this.subscription === null) { + const eventHandler = (event: EventData | VoipEventData) => { + const eventListeners = + this.eventListeners.get(event.eventName as Name) || []; + eventListeners.forEach((listener) => listener(event.params as Params)); + }; + + if (isTurboModuleEnabled) { + if (isVoipEvent(eventName)) { + this.subscription = NativeCallingModule.onNewVoipEvent(eventHandler); + } else { + this.subscription = NativeCallingModule.onNewEvent(eventHandler); + } + } else { + const nativeEmitter = new NativeEventEmitter( + NativeCallingModule as any, + ); + const nativeEventName = isVoipEvent(eventName) + ? 'onNewVoipEvent' + : 'onNewEvent'; + this.subscription = nativeEmitter.addListener( + nativeEventName, + eventHandler as any, + ); + } + } + } + + removeListener( + eventName: T, + callback: EventListener, + ): void { + const listeners = this.eventListeners.get(eventName) || []; + const updatedListeners = listeners.filter((c) => c !== callback); + this.eventListeners.set(eventName, updatedListeners); + + if (updatedListeners.length !== listeners.length) { + this.listenersCount--; + } + + if (this.listenersCount === 0) { + this.subscription?.remove(); + this.subscription = null; + } + } +} + +export { EventManager, type EventListener }; diff --git a/packages/react-native-callingx/src/index.ts b/packages/react-native-callingx/src/index.ts new file mode 100644 index 0000000000..0381bf9540 --- /dev/null +++ b/packages/react-native-callingx/src/index.ts @@ -0,0 +1,2 @@ +export { default as CallingxModule } from './CallingxModule'; +export * from './types'; diff --git a/packages/react-native-callingx/src/spec/NativeCallingx.ts b/packages/react-native-callingx/src/spec/NativeCallingx.ts new file mode 100644 index 0000000000..32c48851d6 --- /dev/null +++ b/packages/react-native-callingx/src/spec/NativeCallingx.ts @@ -0,0 +1,189 @@ +import { + TurboModuleRegistry, + NativeModules, + type TurboModule, +} from 'react-native'; + +// @ts-expect-error - CodegenTypes is not properly typed +import type { EventEmitter } from 'react-native/Libraries/Types/CodegenTypes'; +import { isTurboModuleEnabled } from '../utils/utils'; + +export interface Spec extends TurboModule { + setupiOS(options: { + supportsVideo: boolean; + maximumCallsPerCallGroup: number; + maximumCallGroups: number; + handleType: string; + sound: string | null; + imageName: string | null; + callsHistory: boolean; + displayCallTimeout: number; + }): void; + + setupAndroid(options: { + incomingChannel: { + id: string; + name: string; + sound: string; + vibration: boolean; + }; + ongoingChannel: { + id: string; + name: string; + }; + notificationTexts?: { + accepting?: string; + rejecting?: string; + }; + }): void; + + setShouldRejectCallWhenBusy(shouldReject: boolean): void; + + canPostNotifications(): boolean; + + getInitialEvents(): Array<{ + eventName: string; + params: { + callId: string; + cause?: string; + muted?: boolean; + hold?: boolean; + source?: string; + }; + }>; + + getInitialVoipEvents(): Array<{ + eventName: string; + params: { + token?: string; + aps?: { + 'thread-id': string; + 'mutable-content': number; + alert: { + title: string; + }; + category: string; + sound: string; + }; + stream?: { + sender: string; + created_by_id: string; + body: string; + title: string; + call_display_name: string; + created_by_display_name: string; + version: string; + type: string; + receiver_id: string; + call_cid: string; + video: string; + }; + }; + }>; + + setCurrentCallActive(callId: string): Promise; + + displayIncomingCall( + callId: string, + phoneNumber: string, + callerName: string, + hasVideo: boolean, + displayOptions?: { + displayTitle?: string; + }, + ): Promise; + + //use when need to answer an incoming call withing app UI + answerIncomingCall(callId: string): Promise; + + startCall( + callId: string, + phoneNumber: string, + callerName: string, + hasVideo: boolean, + displayOptions?: { + displayTitle?: string; + }, + ): Promise; + + updateDisplay( + callId: string, + phoneNumber: string, + callerName: string, + displayOptions?: { + displayTitle?: string; + }, + ): Promise; + + isCallTracked(callId: string): boolean; + + hasRegisteredCall(): boolean; + + endCallWithReason(callId: string, reason: number): Promise; + + endCall(callId: string): Promise; + + setMutedCall(callId: string, isMuted: boolean): Promise; + + setOnHoldCall(callId: string, isOnHold: boolean): Promise; + + registerBackgroundTaskAvailable(): void; + startBackgroundTask(taskName: string, timeout: number): Promise; + + stopBackgroundTask(taskName: string): Promise; + + fulfillAnswerCallAction(callId: string, didFail: boolean): void; + + fulfillEndCallAction(callId: string, didFail: boolean): void; + + registerVoipToken(): void; + + stopService(): Promise; + + readonly onNewEvent: EventEmitter<{ + eventName: string; + params: { + callId: string; + cause?: string; + muted?: boolean; + hold?: boolean; + }; + }>; + + readonly onNewVoipEvent: EventEmitter<{ + eventName: string; + params: { + token: string; + aps: { + 'thread-id': string; + 'mutable-content': number; + alert: { + title: string; + }; + category: string; + sound: string; + }; + stream: { + sender: string; + created_by_id: string; + body: string; + title: string; + call_display_name: string; + created_by_display_name: string; + version: string; + type: string; + receiver_id: string; + call_cid: string; + video: string; + }; + }; + }>; + + log(message: string, level: 'debug' | 'info' | 'warn' | 'error'): void; +} + +const CallingxModule: Spec = isTurboModuleEnabled + ? TurboModuleRegistry.getEnforcing('Callingx') + : (NativeModules.Callingx as Spec); + +export default CallingxModule; diff --git a/packages/react-native-callingx/src/types.ts b/packages/react-native-callingx/src/types.ts new file mode 100644 index 0000000000..7abe1f8ad7 --- /dev/null +++ b/packages/react-native-callingx/src/types.ts @@ -0,0 +1,375 @@ +import type { EventListener } from './EventManager'; +import type { ManagableTask } from './utils/headlessTask'; + +export interface ICallingxModule { + /** + * Whether the module can post call notifications. Android only. iOS always returns true. + * Returns true when: + * - The incoming notification channel is enabled, + * - The ongoing notification channel is enabled, + * - And on Android 12 and below: app notifications are enabled in system settings. + * CallStyle is exempt from the POST_NOTIFICATIONS permission on Android 13+ when self-managing calls. + * @returns The boolean value. + */ + get canPostNotifications(): boolean; + get isOngoingCallsEnabled(): boolean; + get isSetup(): boolean; + + /** + * Setup the module. This method must be called before any other method. + * For iOS, the module will setup CallKit parameters. + * See: {@link InternalIOSOptions} + * For Android, the module will create notification channels. + * See: {@link InternalAndroidOptions} + * @param options - The options to setup the callingx module. See {@link CallingExpOptions} + */ + setup(options: CallingExpOptions): void; + /** + * Set whether to reject calls when the user is busy. + * The value is used in iOS native module to prevent calls registration in CallKit when the user is busy. + * @param shouldReject - Whether to reject calls when the user is busy. + */ + setShouldRejectCallWhenBusy(shouldReject: boolean): void; + /** + * Get the initial events. This method is used to get the initial events from the app launch. + * The events are queued and can be retrieved after the module is setup. + * IMPORTANT: After the events are retrieved, new events will be sent to the event listeners. + * @returns The initial events. + */ + getInitialEvents(): EventData[]; + + /** + * Get the initial voip events. This method is used to get the initial voip events from the app launch. + * The events are queued and can be retrieved after the module is setup. + * IMPORTANT: After the events are retrieved, new events will be sent to the event listeners. + * @returns The initial voip events. + */ + getInitialVoipEvents(): VoipEventData[]; + + displayIncomingCall( + callId: string, + phoneNumber: string, + callerName: string, + hasVideo: boolean, + ): Promise; + answerIncomingCall(callId: string): Promise; + + startCall( + callId: string, + phoneNumber: string, + callerName: string, + hasVideo: boolean, + ): Promise; + + /** + * Set the current call active. This method is used to set the current call active. + * This method is used to activate the call that was registered with {@link startCall}. + * @param callId - The call id. + * @returns The promise. + */ + setCurrentCallActive(callId: string): Promise; + + updateDisplay( + callId: string, + phoneNumber: string, + callerName: string, + incoming: boolean, + ): Promise; + + /** + * Check if the call is tracked in the native calling module. + * @param callId - The call id. + * @returns The boolean value. + */ + isCallTracked(callId: string): boolean; + + /** + * Check if there is a registered call. + * @returns The boolean value. + */ + hasRegisteredCall(): boolean; + + /** + * End the call with a reason. This method is used to end the call with a reason. + * Note: In general invoking this method will trigger the call end event. + * But, in case of iOS, when the call is ended with the reason 'local', the call end event will not be triggered. + * @param callId - The call id. + * @param reason - The reason. + * @returns The promise. + */ + endCallWithReason(callId: string, reason: EndCallReason): Promise; + + setMutedCall(callId: string, isMuted: boolean): Promise; + + setOnHoldCall(callId: string, isOnHold: boolean): Promise; + + /** + * Register a background task provider. This method only registers the task and does not start it. + * The task will be automatically started when the service starts (via displayIncomingCall or startCall) + * if it has been registered. + * @param taskProvider - The task provider function that will be executed when the background task starts. + */ + registerBackgroundTask(taskProvider: ManagableTask): void; + + /** + * Start the background task. This method will only start the task if: + * 1. A background task has been registered via registerBackgroundTask + * 2. The service is currently started + * @param taskProvider - The task provider function. If not provided, uses the previously registered task. + * @returns Promise that resolves when the task is started, or rejects if conditions are not met. + */ + startBackgroundTask(taskProvider?: ManagableTask): Promise; + + stopBackgroundTask(taskName: string): Promise; + + /** + * Fulfill or fail a pending CXAnswerCallAction on iOS. + * Must be called after starting the JS-side joining process (e.g: without awaiting for call.join() to complete) + * @param callId - The call id. + * @param didFail - If true, calls action.fail(); otherwise calls action.fulfill(). + */ + fulfillAnswerCallAction(callId: string, didFail: boolean): void; + + /** + * Fulfill or fail a pending CXEndCallAction on iOS. + * Must be called after completetion of the JS-side processing (e.g: after call.leave() is done). + * @param callId - The call id. + * @param didFail - If true, calls action.fail(); otherwise calls action.fulfill(). + */ + fulfillEndCallAction(callId: string, didFail: boolean): void; + + registerVoipToken(): void; + + stopService(): Promise; + + /** + * Single entry point for adding event listeners. + * Automatically routes to the appropriate manager based on event type. + * + * @param eventName - The event name (EventName or VoipEventName) + * @param callback - The callback function that receives the event parameters + * @returns An object with a remove method to unsubscribe from the event + */ + addEventListener( + eventName: T, + callback: EventListener< + T extends EventName + ? EventParams[T] + : T extends VoipEventName + ? VoipEventParams[T] + : never + >, + ): { remove: () => void }; + + log(message: string, level: 'debug' | 'info' | 'warn' | 'error'): void; +} + +export type InternalIOSOptions = { + supportsVideo?: boolean; + maximumCallsPerCallGroup?: number; + maximumCallGroups?: number; + handleType?: 'generic' | 'number' | 'phone' | 'email'; + /** + * Sound to play when an incoming call is received. Must be a valid sound resource name in the project. + * @default '' (no sound) + */ + sound?: string; + /** + * Image to display when an incoming call is received. Must be a valid image resource name in the project. + * @default '' (no image) + */ + imageName?: string; + /** + * Enable calls history. When enabled, the call will be added to the calls history. + * @default false + */ + callsHistory?: boolean; + /** + * Timeout to display an incoming call. When the call is displayed for more than the timeout, the call will be rejected. + * @default 60000 (1 minute) + */ + displayCallTimeout?: number; +}; +type iOSOptions = Omit< + InternalIOSOptions, + 'maximumCallsPerCallGroup' | 'maximumCallGroups' | 'handleType' +>; + +export type InternalAndroidOptions = { + /** + * Incoming channel configuration. + * @default { id: 'incoming_calls_channel', name: 'Incoming calls', sound: '', vibration: false } + */ + incomingChannel?: { + id?: string; + name?: string; + sound?: string; + vibration?: boolean; + }; + /** + * Ongoing channel configuration. + * @default { id: 'ongoing_calls_channel', name: 'Ongoing calls' } + */ + ongoingChannel?: { + id?: string; + name?: string; + }; + /** + * Texts used for call state notifications while the system is connecting or declining the call. + * If not provided, platform defaults will be used. + */ + notificationTexts?: { + /** + * Text shown while optimistically accepting a call. + * @default "Connecting..." + */ + accepting?: string; + /** + * Text shown while optimistically rejecting a call. + * @default "Declining..." + */ + rejecting?: string; + }; +}; +type AndroidOptions = InternalAndroidOptions & NotificationTransformers; + +export type NotificationTransformers = { + titleTransformer?: (memberName: string, incoming: boolean) => string; +}; + +export type CallingExpOptions = { + ios?: iOSOptions; + android?: AndroidOptions; + /** + * Whether to enable ongoing calls. + * @default false + */ + enableOngoingCalls?: boolean; + /** + * Whether to reject calls when the user is busy. + * @default false + */ + shouldRejectCallWhenBusy?: boolean; +}; + +export type InfoDisplayOptions = { + displayTitle?: string; +}; + +export type EventData = { + eventName: EventName; + params: EventParams[EventName]; +}; + +export type VoipEventData = { + eventName: VoipEventName; + params: VoipEventParams[VoipEventName]; +}; + +export type EventName = + | 'answerCall' + | 'endCall' + | 'didDisplayIncomingCall' + | 'didToggleHoldCallAction' + | 'didChangeAudioRoute' + | 'didReceiveStartCallAction' + | 'didPerformSetMutedCallAction' + | 'didActivateAudioSession' + | 'didDeactivateAudioSession'; + +export type EventParams = { + answerCall: { + callId: string; + source: 'app' | 'sys'; + }; + endCall: { + callId: string; + cause: string; + source: 'app' | 'sys'; + }; + didDisplayIncomingCall: { + callId: string; + }; + didToggleHoldCallAction: { + callId: string; + hold: boolean; + }; + didPerformSetMutedCallAction: { + callId: string; + muted: boolean; + }; + didChangeAudioRoute: { + callId: string; + output: string; + }; + didReceiveStartCallAction: { + callId: string; + }; + didActivateAudioSession: undefined; + didDeactivateAudioSession: undefined; +}; + +export type VoipEventName = + | 'voipNotificationsRegistered' + | 'voipNotificationReceived'; + +export type VoipEventParams = { + voipNotificationsRegistered: { + token: string; + }; + voipNotificationReceived: { + aps: { + 'thread-id': string; + 'mutable-content': number; + alert: { + title: string; + }; + category: string; + sound: string; + }; + stream: { + sender: string; + created_by_id: string; + body: string; + title: string; + call_display_name: string; + created_by_display_name: string; + version: string; + type: string; + receiver_id: string; + call_cid: string; + video: string; + }; + }; +}; + +/** + * The reason for ending a call. These values are mapped to platform-specific + * constants on each platform: + * - iOS: `CXCallEndedReason` (CallKit) + * - Android: `DisconnectCause` (Telecom) + * + * @see https://developer.apple.com/documentation/callkit/cxcallendedreason + * @see https://developer.android.com/reference/android/telecom/DisconnectCause + */ +export type EndCallReason = + /** Call ended by the local user (e.g., hanging up). */ + | 'local' + /** Call ended by the remote party, or outgoing call was not answered. */ + | 'remote' + /** Call was rejected/declined by the user. */ + | 'rejected' + /** Remote party was busy. */ + | 'busy' + /** Call was answered on another device. */ + | 'answeredElsewhere' + /** No response to an incoming call. */ + | 'missed' + /** Call failed due to an error (e.g., network issue). */ + | 'error' + /** Call was canceled before the remote party could answer. */ + | 'canceled' + /** Call restricted (e.g., airplane mode, dialing restrictions). */ + | 'restricted' + /** Unknown or unspecified disconnect reason. */ + | 'unknown'; diff --git a/packages/react-native-callingx/src/utils/constants.ts b/packages/react-native-callingx/src/utils/constants.ts new file mode 100644 index 0000000000..e85e6701ce --- /dev/null +++ b/packages/react-native-callingx/src/utils/constants.ts @@ -0,0 +1,75 @@ +import type { + InternalAndroidOptions, + InternalIOSOptions, + EndCallReason, +} from '../types'; +import type { DeepRequired } from './types'; + +export const defaultiOSOptions: Required = { + supportsVideo: true, + maximumCallsPerCallGroup: 1, + maximumCallGroups: 1, + handleType: 'generic', + sound: '', + imageName: '', + callsHistory: false, + displayCallTimeout: 60000, // 1 minute +}; + +export const defaultAndroidOptions: Omit< + DeepRequired, + 'notificationTexts' +> = { + incomingChannel: { + id: 'stream_incoming_calls_channel', + name: 'Incoming calls', + sound: '', + vibration: false, + }, + ongoingChannel: { + id: 'stream_ongoing_calls_channel', + name: 'Ongoing calls', + }, +}; + +// iOS: maps to CXCallEndedReason raw values. +// See https://developer.apple.com/documentation/callkit/cxcallendedreason +// CXCallEndedReason: failed=1, remoteEnded=2, unanswered=3, answeredElsewhere=4, declinedElsewhere=5 +export const iosEndCallReasonMap: Record = { + local: -1, // special: uses endCall() instead of endCallWithReason() + remote: 2, // .remoteEnded + rejected: 5, // .declinedElsewhere + busy: 3, // .unanswered + answeredElsewhere: 4, // .answeredElsewhere + missed: 3, // .unanswered + error: 1, // .failed + canceled: 2, // .remoteEnded (caller canceled before answer) + restricted: 1, // .failed (no iOS equivalent) + unknown: 1, // .failed (no iOS equivalent) +}; + +// Android: maps to a limited subset of android.telecom.DisconnectCause constants +// that are allowed when using the CallControl / core-telecom APIs. +// +// Per platform docs, only the following codes are valid when disconnecting a call: +// - DisconnectCause.LOCAL +// - DisconnectCause.REMOTE +// - DisconnectCause.REJECTED +// - DisconnectCause.MISSED +// +// Numeric values (from android.telecom.DisconnectCause): +// LOCAL = 2, REMOTE = 3, REJECTED = 6, MISSED = 5 +// +// We therefore collapse all high-level EndCallReason variants to this allowed set. +export const androidEndCallReasonMap: Record = { + local: 2, // LOCAL + remote: 3, // REMOTE + rejected: 6, // REJECTED + busy: 6, // map busy -> REJECTED + answeredElsewhere: 3, // map answeredElsewhere -> REMOTE + missed: 5, // MISSED + error: 2, // map error -> LOCAL + canceled: 2, // map canceled -> LOCAL + restricted: 6, // map restricted -> REJECTED + unknown: 2, // map unknown -> LOCAL +}; diff --git a/packages/react-native-callingx/src/utils/headlessTask.ts b/packages/react-native-callingx/src/utils/headlessTask.ts new file mode 100644 index 0000000000..cbbf6700e3 --- /dev/null +++ b/packages/react-native-callingx/src/utils/headlessTask.ts @@ -0,0 +1,52 @@ +import { AppRegistry } from 'react-native'; + +export const HEADLESS_TASK_NAME = 'HandleCallBackgroundState'; + +export type ManagableTask = ( + taskData: any, + stopTask: () => void, +) => Promise; + +type HeadlessTask = (taskData: any) => Promise; + +export const defaultBackgroundTask: ManagableTask = ( + taskData: any, + stopTask: () => void, +) => { + return new Promise((resolve) => { + console.log('Default background task data', taskData); + let i = 0; + const totalIterations = 5; + + function loop() { + if (i < totalIterations) { + setTimeout(() => { + console.log(`Iteration: ${i + 1}`); + i++; + loop(); + }, 1000); + } else { + console.log('Default background task finished'); + resolve(undefined); + stopTask(); + } + } + + loop(); + }); +}; + +let headlessTask: HeadlessTask = (taskData: any) => + defaultBackgroundTask(taskData, () => { + console.log('Cancel callback called'); + }); + +export const setHeadlessTask = (task: HeadlessTask) => { + headlessTask = task; +}; + +export const registerHeadlessTask = () => { + AppRegistry.registerHeadlessTask(HEADLESS_TASK_NAME, () => { + return headlessTask; + }); +}; diff --git a/packages/react-native-callingx/src/utils/types.ts b/packages/react-native-callingx/src/utils/types.ts new file mode 100644 index 0000000000..7dd8dee102 --- /dev/null +++ b/packages/react-native-callingx/src/utils/types.ts @@ -0,0 +1,5 @@ +export type DeepRequired = { + [K in keyof T]-?: NonNullable extends object + ? DeepRequired> + : NonNullable; +}; diff --git a/packages/react-native-callingx/src/utils/utils.ts b/packages/react-native-callingx/src/utils/utils.ts new file mode 100644 index 0000000000..a04b53cbf6 --- /dev/null +++ b/packages/react-native-callingx/src/utils/utils.ts @@ -0,0 +1,9 @@ +export const isVoipEvent = (eventName: string) => { + return ( + eventName === 'voipNotificationsRegistered' || + eventName === 'voipNotificationReceived' + ); +}; + +// @ts-expect-error - RN$Bridgeless is not properly typed +export const isTurboModuleEnabled = global.RN$Bridgeless === true; diff --git a/packages/react-native-callingx/tsconfig.build.json b/packages/react-native-callingx/tsconfig.build.json new file mode 100644 index 0000000000..3469944116 --- /dev/null +++ b/packages/react-native-callingx/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig", + "exclude": ["lib"] +} diff --git a/packages/react-native-callingx/tsconfig.json b/packages/react-native-callingx/tsconfig.json new file mode 100644 index 0000000000..6d5506c5f8 --- /dev/null +++ b/packages/react-native-callingx/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "rootDir": ".", + "paths": { + "react-native-callingx": ["./src/index"] + }, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "customConditions": ["react-native-strict-api"], + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react-jsx", + "lib": ["ESNext"], + "module": "ESNext", + "moduleResolution": "bundler", + "noEmit": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noImplicitUseStrict": false, + "noStrictGenericChecks": false, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ESNext", + "verbatimModuleSyntax": true + } +} diff --git a/packages/react-native-sdk/android/src/main/AndroidManifest.xml b/packages/react-native-sdk/android/src/main/AndroidManifest.xml index 2205d8dd49..833dd9a5ec 100644 --- a/packages/react-native-sdk/android/src/main/AndroidManifest.xml +++ b/packages/react-native-sdk/android/src/main/AndroidManifest.xml @@ -2,6 +2,13 @@ package="com.streamvideo.reactnative"> - + + + + diff --git a/packages/react-native-sdk/android/src/main/AndroidManifestNew.xml b/packages/react-native-sdk/android/src/main/AndroidManifestNew.xml index a2f47b6057..26d3791256 100644 --- a/packages/react-native-sdk/android/src/main/AndroidManifestNew.xml +++ b/packages/react-native-sdk/android/src/main/AndroidManifestNew.xml @@ -1,2 +1,13 @@ + + + + + + + diff --git a/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/StreamVideoReactNativeModule.kt b/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/StreamVideoReactNativeModule.kt index a1ac2ed7a3..c4adb8fd98 100644 --- a/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/StreamVideoReactNativeModule.kt +++ b/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/StreamVideoReactNativeModule.kt @@ -17,6 +17,7 @@ import android.os.Build import android.os.PowerManager import android.util.Base64 import android.util.Log +import androidx.core.content.ContextCompat import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext @@ -27,8 +28,8 @@ import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEm import com.oney.WebRTCModule.WebRTCModule import com.oney.WebRTCModule.WebRTCModuleOptions import com.streamvideo.reactnative.screenshare.ScreenAudioCapture +import com.streamvideo.reactnative.keepalive.StreamCallKeepAliveHeadlessService import com.streamvideo.reactnative.util.CallAlivePermissionsHelper -import com.streamvideo.reactnative.util.CallAliveServiceChecker import com.streamvideo.reactnative.util.PiPHelper import com.streamvideo.reactnative.util.RingtoneUtil import com.streamvideo.reactnative.util.YuvFrame @@ -122,11 +123,47 @@ class StreamVideoReactNativeModule(reactContext: ReactApplicationContext) : promise.resolve(false) return } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - val isForegroundServiceDeclared = CallAliveServiceChecker.isForegroundServiceDeclared(reactApplicationContext) - promise.resolve(isForegroundServiceDeclared) - } else { + // Service is declared in the SDK's own AndroidManifest and merged by default. + // Permissions are expected to be provided by the app (or via Expo config plugin). + promise.resolve(true) + } + + + @ReactMethod + fun startKeepCallAliveService( + callCid: String, + channelId: String, + channelName: String, + title: String, + body: String, + smallIconName: String?, + promise: Promise + ) { + try { + val intent = StreamCallKeepAliveHeadlessService.buildStartIntent( + reactApplicationContext, + callCid, + channelId, + channelName, + title, + body, + smallIconName + ) + ContextCompat.startForegroundService(reactApplicationContext, intent) promise.resolve(true) + } catch (e: Exception) { + promise.reject(NAME, "Failed to start keep call alive foreground service", e) + } + } + + @ReactMethod + fun stopKeepCallAliveService(promise: Promise) { + try { + val intent = StreamCallKeepAliveHeadlessService.buildStopIntent(reactApplicationContext) + val stopped = reactApplicationContext.stopService(intent) + promise.resolve(stopped) + } catch (e: Exception) { + promise.reject(NAME, "Failed to stop keep call alive foreground service", e) } } diff --git a/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/audio/utils/WebRtcAudioUtils.kt b/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/audio/utils/WebRtcAudioUtils.kt index 35d6c18639..acaeca89bf 100644 --- a/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/audio/utils/WebRtcAudioUtils.kt +++ b/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/audio/utils/WebRtcAudioUtils.kt @@ -55,14 +55,78 @@ object WebRtcAudioUtils { * what might be the root cause. */ fun logAudioState(tag: String, reactContext: ReactContext) { + Log.d(tag, getAudioStateLog(reactContext)) + } + + /** + * Returns a string containing information about the current audio state. + * Similar to logAudioState but returns the information instead of logging it. + */ + fun getAudioStateLog(reactContext: ReactContext): String { + val audioManager = reactContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager + val sb = StringBuilder() + + // Volume control stream reactContext.currentActivity?.let { - Log.d(tag, "volumeControlStream: " + streamTypeToString(it.volumeControlStream)) + sb.appendLine("volumeControlStream: ${streamTypeToString(it.volumeControlStream)}") } - val audioManager = reactContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager - logDeviceInfo(tag) - logAudioStateBasic(tag, reactContext, audioManager) - logAudioStateVolume(tag, audioManager) - logAudioDeviceInfo(tag, audioManager) + + // Device info + sb.appendLine("Android SDK: ${Build.VERSION.SDK_INT}, Release: ${Build.VERSION.RELEASE}, Brand: ${Build.BRAND}, Device: ${Build.DEVICE}, Id: ${Build.ID}, Hardware: ${Build.HARDWARE}, Manufacturer: ${Build.MANUFACTURER}, Model: ${Build.MODEL}, Product: ${Build.PRODUCT}") + + // Basic audio state + sb.appendLine("Audio State: audio mode: ${modeToString(audioManager.mode)}, has mic: ${hasMicrophone(reactContext)}, mic muted: ${audioManager.isMicrophoneMute}, music active: ${audioManager.isMusicActive}, speakerphone: ${audioManager.isSpeakerphoneOn}, BT SCO: ${audioManager.isBluetoothScoOn}") + + // Volume info + val fixedVolume = audioManager.isVolumeFixed + sb.appendLine(" fixed volume=$fixedVolume") + if (!fixedVolume) { + val streams = intArrayOf( + AudioManager.STREAM_VOICE_CALL, + AudioManager.STREAM_MUSIC, + AudioManager.STREAM_RING, + AudioManager.STREAM_ALARM, + AudioManager.STREAM_NOTIFICATION, + AudioManager.STREAM_SYSTEM + ) + for (stream in streams) { + val info = StringBuilder() + info.append(" ${streamTypeToString(stream)}: ") + info.append("volume=${audioManager.getStreamVolume(stream)}") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + info.append(", min=${audioManager.getStreamMinVolume(stream)}") + } + info.append(", max=${audioManager.getStreamMaxVolume(stream)}") + info.append(", muted=${audioManager.isStreamMute(stream)}") + sb.appendLine(info.toString()) + } + } + + // Audio devices + val inputDevices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS) + val outputDevices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS) + val devices = inputDevices + outputDevices + if (devices.isNotEmpty()) { + sb.appendLine("Audio Devices:") + for (device in devices) { + val info = StringBuilder() + info.append(" ${deviceTypeToString(device.type)}") + info.append(if (device.isSource) "(in): " else "(out): ") + if (device.channelCounts.isNotEmpty()) { + info.append("channels=${device.channelCounts.contentToString()}, ") + } + if (device.encodings.isNotEmpty()) { + info.append("encodings=${device.encodings.contentToString()}, ") + } + if (device.sampleRates.isNotEmpty()) { + info.append("sample rates=${device.sampleRates.contentToString()}, ") + } + info.append("id=${device.id}") + sb.appendLine(info.toString()) + } + } + + return sb.toString() } /** Converts AudioDeviceInfo types to local string representation. */ diff --git a/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/callmanager/StreamInCallManagerModule.kt b/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/callmanager/StreamInCallManagerModule.kt index 08d6afe31b..e15b30192e 100644 --- a/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/callmanager/StreamInCallManagerModule.kt +++ b/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/callmanager/StreamInCallManagerModule.kt @@ -168,10 +168,12 @@ class StreamInCallManagerModule(reactContext: ReactApplicationContext) : @ReactMethod fun logAudioState() { - WebRtcAudioUtils.logAudioState( - TAG, - reactApplicationContext, - ) + Log.d(TAG, getAudioStateLog()) + } + + @ReactMethod(isBlockingSynchronousMethod = true) + fun getAudioStateLog(): String { + return WebRtcAudioUtils.getAudioStateLog(reactApplicationContext) } @Suppress("unused") diff --git a/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/keepalive/KeepAliveNotification.kt b/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/keepalive/KeepAliveNotification.kt new file mode 100644 index 0000000000..afc375e49c --- /dev/null +++ b/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/keepalive/KeepAliveNotification.kt @@ -0,0 +1,83 @@ +package com.streamvideo.reactnative.keepalive + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import androidx.core.app.NotificationCompat + +internal object KeepAliveNotification { + private const val DEFAULT_CHANNEL_DESCRIPTION = "Stream call keep-alive" + + fun ensureChannel( + context: Context, + channelId: String, + channelName: String + ) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return + val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val existing = manager.getNotificationChannel(channelId) + if (existing != null) return + + val channel = NotificationChannel( + channelId, + channelName, + NotificationManager.IMPORTANCE_LOW + ).apply { + description = DEFAULT_CHANNEL_DESCRIPTION + setShowBadge(false) + } + manager.createNotificationChannel(channel) + } + + fun buildOngoingNotification( + context: Context, + channelId: String, + title: String, + body: String, + smallIconName: String? + ): Notification { + val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) + val pendingIntentFlags = + PendingIntent.FLAG_UPDATE_CURRENT or + PendingIntent.FLAG_IMMUTABLE + val contentIntent = if (launchIntent != null) { + PendingIntent.getActivity(context, 0, launchIntent, pendingIntentFlags) + } else { + // Fallback: empty intent to avoid crash if launch activity is missing for some reason + PendingIntent.getActivity(context, 0, Intent(), pendingIntentFlags) + } + + val iconResId = resolveSmallIconResId(context, smallIconName) + return NotificationCompat.Builder(context, channelId) + .setContentTitle(title) + .setContentText(body) + .setOngoing(true) + .setOnlyAlertOnce(true) + .setCategory(NotificationCompat.CATEGORY_CALL) + .setContentIntent(contentIntent) + .setSmallIcon(iconResId) + .build() + } + + private fun resolveSmallIconResId(context: Context, smallIconName: String?): Int { + val resources = context.resources + val packageName = context.packageName + if (!smallIconName.isNullOrBlank()) { + val id = resources.getIdentifier(smallIconName, "drawable", packageName) + if (id != 0) return id + } + // Default to the app icon + return try { + val appInfo = context.packageManager.getApplicationInfo(packageName, 0) + appInfo.icon + } catch (_: PackageManager.NameNotFoundException) { + android.R.drawable.ic_dialog_info + } + } +} + diff --git a/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/keepalive/StreamCallKeepAliveHeadlessService.kt b/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/keepalive/StreamCallKeepAliveHeadlessService.kt new file mode 100644 index 0000000000..1cb401616b --- /dev/null +++ b/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/keepalive/StreamCallKeepAliveHeadlessService.kt @@ -0,0 +1,149 @@ +package com.streamvideo.reactnative.keepalive + +import android.Manifest +import android.content.Intent +import android.content.pm.PackageManager +import android.content.pm.ServiceInfo +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.app.ServiceCompat +import androidx.core.content.ContextCompat +import com.facebook.react.HeadlessJsTaskService +import com.facebook.react.bridge.Arguments +import com.facebook.react.jstasks.HeadlessJsTaskConfig + +/** + * Foreground service that runs a React Native HeadlessJS task to keep a call alive. + * + */ +class StreamCallKeepAliveHeadlessService : HeadlessJsTaskService() { + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + val safeIntent = intent ?: Intent() + val channelId = safeIntent.getStringExtra(EXTRA_CHANNEL_ID) ?: DEFAULT_CHANNEL_ID + val channelName = safeIntent.getStringExtra(EXTRA_CHANNEL_NAME) ?: DEFAULT_CHANNEL_NAME + val title = safeIntent.getStringExtra(EXTRA_TITLE) ?: DEFAULT_TITLE + val body = safeIntent.getStringExtra(EXTRA_BODY) ?: DEFAULT_BODY + val smallIconName = safeIntent.getStringExtra(EXTRA_SMALL_ICON_NAME) + + KeepAliveNotification.ensureChannel(this, channelId, channelName) + val notification = KeepAliveNotification.buildOngoingNotification( + context = this, + channelId = channelId, + title = title, + body = body, + smallIconName = smallIconName + ) + + startForegroundCompat(notification) + + // Ensure HeadlessJS task is started + return super.onStartCommand(safeIntent, flags, startId) + } + + override fun getTaskConfig(intent: Intent?): HeadlessJsTaskConfig? { + val callCid = intent?.getStringExtra(EXTRA_CALL_CID) ?: return null + val data = Arguments.createMap().apply { + putString("callCid", callCid) + } + // We intentionally allow long-running work (the JS task can return a never-resolving Promise). + return HeadlessJsTaskConfig( + TASK_NAME, + data, + 0, // timeout (0 = no timeout) + true // allowedInForeground + ) + } + + override fun onDestroy() { + super.onDestroy() + stopForeground(STOP_FOREGROUND_REMOVE) + } + + @RequiresApi(Build.VERSION_CODES.R) + private fun computeForegroundServiceTypes(): Int { + var types = ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK + + val hasCameraPermission = + ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED + if (hasCameraPermission) { + types = types or ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA + } + + val hasMicrophonePermission = + ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED + if (hasMicrophonePermission) { + types = types or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE + } + + return types + } + + private fun startForegroundCompat(notification: android.app.Notification) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val types = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) computeForegroundServiceTypes() + else ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK + startForeground(NOTIFICATION_ID, notification, types) + } else { + startForeground(NOTIFICATION_ID, notification) + } + } catch (e: Exception) { + // Avoid crashing the app if the system rejects starting a foreground service (e.g. + // background start restrictions, invalid notification/channel, or permission issues). + Log.e( + TAG, + "startForegroundCompat: Failed to start foreground service: ${e.message}", + e + ) + } + } + + companion object { + private const val TAG = "StreamCallKeepAliveHeadlessService" + + const val TASK_NAME = "StreamVideoKeepCallAlive" + + const val EXTRA_CALL_CID = "callCid" + const val EXTRA_CHANNEL_ID = "channelId" + const val EXTRA_CHANNEL_NAME = "channelName" + const val EXTRA_TITLE = "title" + const val EXTRA_BODY = "body" + const val EXTRA_SMALL_ICON_NAME = "smallIconName" + + private const val NOTIFICATION_ID = 6061 + + private const val DEFAULT_CHANNEL_ID = "stream_call_foreground_service" + private const val DEFAULT_CHANNEL_NAME = "Call in progress" + private const val DEFAULT_TITLE = "Call in progress" + private const val DEFAULT_BODY = "Tap to return to the call" + + fun buildStartIntent( + context: android.content.Context, + callCid: String, + channelId: String, + channelName: String, + title: String, + body: String, + smallIconName: String? + ): Intent { + return Intent(context, StreamCallKeepAliveHeadlessService::class.java).apply { + putExtra(EXTRA_CALL_CID, callCid) + putExtra(EXTRA_CHANNEL_ID, channelId) + putExtra(EXTRA_CHANNEL_NAME, channelName) + putExtra(EXTRA_TITLE, title) + putExtra(EXTRA_BODY, body) + if (!smallIconName.isNullOrBlank()) { + putExtra(EXTRA_SMALL_ICON_NAME, smallIconName) + } + } + } + + fun buildStopIntent(context: android.content.Context): Intent { + return Intent(context, StreamCallKeepAliveHeadlessService::class.java) + } + } +} + diff --git a/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/util/CallAliveServiceChecker.kt b/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/util/CallAliveServiceChecker.kt deleted file mode 100644 index b46b0fe914..0000000000 --- a/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/util/CallAliveServiceChecker.kt +++ /dev/null @@ -1,95 +0,0 @@ -package com.streamvideo.reactnative.util - -import android.content.ComponentName -import android.content.pm.PackageManager -import android.content.pm.ServiceInfo -import android.os.Build -import android.util.Log -import androidx.annotation.RequiresApi -import com.facebook.react.bridge.ReactApplicationContext - -@RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) -object CallAliveServiceChecker { - private const val NAME = "StreamVideoReactNative" - - fun isForegroundServiceDeclared(context: ReactApplicationContext): Boolean { - val packageManager = context.packageManager - val packageName = context.packageName // Get the package name of your app - val componentName = ComponentName( - packageName, - "app.notifee.core.ForegroundService" - ) // Use service name string - - try { - val serviceInfo = - packageManager.getServiceInfo(componentName, PackageManager.GET_META_DATA) - - val expectedForegroundServiceTypes = - ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE or - ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK or - ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA or - ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE - - val actualForegroundServiceTypes = serviceInfo.foregroundServiceType - - if (actualForegroundServiceTypes == expectedForegroundServiceTypes) { - return true - } else { - Log.w( - NAME, - "android:foregroundServiceType does not match: expected=${ - foregroundServiceTypeToString( - expectedForegroundServiceTypes - ) - }, actual=${foregroundServiceTypeToString(actualForegroundServiceTypes)}" - ) - return false - } - - } catch (e: PackageManager.NameNotFoundException) { - Log.d(NAME, "Service not found: " + e.message) - return false // Service not declared - } - } - - private fun foregroundServiceTypeToString(foregroundServiceType: Int): String { - val types = mutableListOf() - if (foregroundServiceType and ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE != 0) { - types.add("shortService") - } - if (foregroundServiceType and ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC != 0) { - types.add("dataSync") - } - if (foregroundServiceType and ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA != 0) { - types.add("camera") - } - if (foregroundServiceType and ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE != 0) { - types.add("microphone") - } - if (foregroundServiceType and ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE != 0) { - types.add("connectedDevice") - } - if (foregroundServiceType and ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION != 0) { - types.add("location") - } - if (foregroundServiceType and ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK != 0) { - types.add("mediaPlayback") - } - if (foregroundServiceType and ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION != 0) { - types.add("mediaProjection") - } - if (foregroundServiceType and ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL != 0) { - types.add("phoneCall") - } - if (foregroundServiceType and ServiceInfo.FOREGROUND_SERVICE_TYPE_HEALTH != 0) { - types.add("health") - } - if (foregroundServiceType and ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING != 0) { - types.add("remoteMessaging") - } - if (foregroundServiceType and ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED != 0) { - types.add("systemExempted") - } - return types.joinToString("|") - } -} diff --git a/packages/react-native-sdk/expo-config-plugin/__tests__/withAndroidManifest.test.ts b/packages/react-native-sdk/expo-config-plugin/__tests__/withAndroidManifest.test.ts index 9b68104ab2..3301048ce2 100644 --- a/packages/react-native-sdk/expo-config-plugin/__tests__/withAndroidManifest.test.ts +++ b/packages/react-native-sdk/expo-config-plugin/__tests__/withAndroidManifest.test.ts @@ -27,17 +27,12 @@ jest.mock('@expo/config-plugins', () => { const readAndroidManifestAsync = AndroidConfig.Manifest.readAndroidManifestAsync; -const getMainApplicationOrThrow = - AndroidConfig.Manifest.getMainApplicationOrThrow; - const getMainActivityOrThrow = AndroidConfig.Manifest.getMainActivityOrThrow; const sampleManifestPath = getFixturePath('AndroidManifest.xml'); const props: ConfigProps = { - ringingPushNotifications: { - disableVideoIos: false, - }, + ringing: true, androidPictureInPicture: true, androidKeepCallAlive: true, }; @@ -58,15 +53,6 @@ describe('withStreamVideoReactNativeSDKManifest', () => { props, ) as CustomExpoConfig; - const mainApp = getMainApplicationOrThrow(updatedConfig.modResults); - - expect( - mainApp.service?.some( - (service) => - service.$['android:name'] === 'app.notifee.core.ForegroundService', - ), - ).toBeTruthy(); - const mainActivity = getMainActivityOrThrow(updatedConfig.modResults); expect( @@ -105,15 +91,6 @@ describe('withStreamVideoReactNativeSDKManifest', () => { props, ) as CustomExpoConfig; - const mainApp = getMainApplicationOrThrow(updatedConfig.modResults); - - expect( - mainApp.service?.filter( - (service) => - service.$['android:name'] === 'app.notifee.core.ForegroundService', - ).length, - ).toBe(1); - modifiedConfig = updatedConfig; }); diff --git a/packages/react-native-sdk/expo-config-plugin/__tests__/withAndroidPermissions.test.ts b/packages/react-native-sdk/expo-config-plugin/__tests__/withAndroidPermissions.test.ts index 6e4a9fe6a3..68c1ce7700 100644 --- a/packages/react-native-sdk/expo-config-plugin/__tests__/withAndroidPermissions.test.ts +++ b/packages/react-native-sdk/expo-config-plugin/__tests__/withAndroidPermissions.test.ts @@ -14,7 +14,7 @@ describe('withStreamVideoReactNativeSDKAndroidPermissions', () => { }; const props: ConfigProps = { enableScreenshare: true, - ringingPushNotifications: { disableVideoIos: false }, + ringing: true, androidKeepCallAlive: true, }; diff --git a/packages/react-native-sdk/expo-config-plugin/__tests__/withAppDelegate.test.ts b/packages/react-native-sdk/expo-config-plugin/__tests__/withAppDelegate.test.ts index 92b368eba9..fdf7c69378 100644 --- a/packages/react-native-sdk/expo-config-plugin/__tests__/withAppDelegate.test.ts +++ b/packages/react-native-sdk/expo-config-plugin/__tests__/withAppDelegate.test.ts @@ -111,10 +111,7 @@ describe('withStreamVideoReactNativeSDKAppDelegate', () => { const props: ConfigProps = { iOSEnableMultitaskingCameraAccess: true, addNoiseCancellation: true, - ringingPushNotifications: { - disableVideoIos: true, - includesCallsInRecentsIos: true, - }, + ringing: true, }; const updatedConfig = withAppDelegate(config, props) as CustomExpoConfig; @@ -133,34 +130,14 @@ describe('withStreamVideoReactNativeSDKAppDelegate', () => { expect(updatedConfig.modResults.contents).toMatch( /options.enableMultitaskingCameraAccess = YES/, ); - expect(updatedConfig.modResults.contents).toMatch(/#import "RNCallKeep.h"/); expect(updatedConfig.modResults.contents).toMatch( /#import /, ); - expect(updatedConfig.modResults.contents).toMatch( - /#import "RNVoipPushNotificationManager.h"/, - ); - expect(updatedConfig.modResults.contents).toMatch(/@"supportsVideo": @NO/); - expect(updatedConfig.modResults.contents).toMatch( - /@"includesCallsInRecents": @YES/, - ); expect(updatedConfig.modResults.contents).toMatch( /didUpdatePushCredentials:credentials/, ); expect(updatedConfig.modResults.contents).toMatch( - /didReceiveIncomingPushWithPayload:payload/, - ); - expect(updatedConfig.modResults.contents).toMatch(/reportNewIncomingCall/); - - expect(updatedConfig.modResults.contents).toMatch( - /#import /, - ); - - expect(updatedConfig.modResults.contents).toMatch( - /audioSessionDidActivate/, - ); - expect(updatedConfig.modResults.contents).toMatch( - /audioSessionDidDeactivate/, + /didReceiveIncomingPush:payload/, ); modifiedConfigObjC = updatedConfig; @@ -183,10 +160,7 @@ describe('withStreamVideoReactNativeSDKAppDelegate', () => { const props: ConfigProps = { iOSEnableMultitaskingCameraAccess: true, addNoiseCancellation: true, - ringingPushNotifications: { - disableVideoIos: true, - includesCallsInRecentsIos: true, - }, + ringing: true, }; const updatedConfig = withAppDelegate(config, props) as CustomExpoConfig; @@ -200,34 +174,17 @@ describe('withStreamVideoReactNativeSDKAppDelegate', () => { ); expect(updatedConfig.modResults.contents).toMatch(/PKPushRegistryDelegate/); expect(updatedConfig.modResults.contents).toMatch(/^import WebRTC/m); - expect(updatedConfig.modResults.contents).toMatch(/^import RNCallKeep/m); expect(updatedConfig.modResults.contents).toMatch(/^import PushKit/m); - expect(updatedConfig.modResults.contents).toMatch( - /^import RNVoipPushNotification/m, - ); // Check Swift implementation expect(updatedConfig.modResults.contents).toMatch( /options.enableMultitaskingCameraAccess = true/, ); - expect(updatedConfig.modResults.contents).toMatch(/"supportsVideo": false/); - expect(updatedConfig.modResults.contents).toMatch( - /"includesCallsInRecents": false/, - ); - expect(updatedConfig.modResults.contents).toMatch( - /RNVoipPushNotificationManager.didUpdate/, - ); - expect(updatedConfig.modResults.contents).toMatch( - /RNVoipPushNotificationManager.didReceiveIncomingPush/, - ); - expect(updatedConfig.modResults.contents).toMatch( - /RNCallKeep.reportNewIncomingCall/, - ); expect(updatedConfig.modResults.contents).toMatch( - /audioSessionDidActivate/, + /StreamVideoReactNative.didUpdate/, ); expect(updatedConfig.modResults.contents).toMatch( - /audioSessionDidDeactivate/, + /StreamVideoReactNative.didReceiveIncomingPush/, ); modifiedConfigSwift = updatedConfig; @@ -236,10 +193,7 @@ describe('withStreamVideoReactNativeSDKAppDelegate', () => { it('objc - should not modify config if already added', () => { const props: ConfigProps = { iOSEnableMultitaskingCameraAccess: true, - ringingPushNotifications: { - disableVideoIos: true, - includesCallsInRecentsIos: true, - }, + ringing: true, }; const updatedConfig = withAppDelegate( @@ -256,10 +210,7 @@ describe('withStreamVideoReactNativeSDKAppDelegate', () => { it('swift - should not modify config if already added', () => { const props: ConfigProps = { iOSEnableMultitaskingCameraAccess: true, - ringingPushNotifications: { - disableVideoIos: true, - includesCallsInRecentsIos: true, - }, + ringing: true, }; const updatedConfig = withAppDelegate( @@ -288,10 +239,7 @@ describe('withStreamVideoReactNativeSDKAppDelegate', () => { }, }; const props: ConfigProps = { - ringingPushNotifications: { - disableVideoIos: true, - includesCallsInRecentsIos: false, - }, + ringing: true, }; expect(() => withAppDelegate(config, props)).toThrow(); }); @@ -311,10 +259,7 @@ describe('withStreamVideoReactNativeSDKAppDelegate', () => { }, }; const props: ConfigProps = { - ringingPushNotifications: { - disableVideoIos: true, - includesCallsInRecentsIos: false, - }, + ringing: true, }; expect(() => withAppDelegate(config, props)).toThrow(); }); diff --git a/packages/react-native-sdk/expo-config-plugin/__tests__/withMainActivity.test.ts b/packages/react-native-sdk/expo-config-plugin/__tests__/withMainActivity.test.ts index 97281f1049..ef0fcf5abf 100644 --- a/packages/react-native-sdk/expo-config-plugin/__tests__/withMainActivity.test.ts +++ b/packages/react-native-sdk/expo-config-plugin/__tests__/withMainActivity.test.ts @@ -43,9 +43,7 @@ describe('withStreamVideoReactNativeSDKAppDelegate', () => { const props: ConfigProps = { androidPictureInPicture: true, enableScreenshare: true, - ringingPushNotifications: { - showWhenLockedAndroid: true, - }, + ringing: true, }; const updatedConfig = withMainActivity(config, props) as CustomExpoConfig; diff --git a/packages/react-native-sdk/expo-config-plugin/__tests__/withiOSInfoPlist.test.ts b/packages/react-native-sdk/expo-config-plugin/__tests__/withiOSInfoPlist.test.ts index 1fbcda78b5..ee13e91e67 100644 --- a/packages/react-native-sdk/expo-config-plugin/__tests__/withiOSInfoPlist.test.ts +++ b/packages/react-native-sdk/expo-config-plugin/__tests__/withiOSInfoPlist.test.ts @@ -129,10 +129,7 @@ describe('withStreamVideoReactNativeSDKiOSInfoPList', () => { }, }; const props: ConfigProps = { - ringingPushNotifications: { - disableVideoIos: true, - includesCallsInRecentsIos: true, - }, + ringing: true, }; const modifiedConfig = withStreamVideoReactNativeSDKiOSInfoPList( config, diff --git a/packages/react-native-sdk/expo-config-plugin/src/common/types.ts b/packages/react-native-sdk/expo-config-plugin/src/common/types.ts index b6822b0192..b4a4c2b3e1 100644 --- a/packages/react-native-sdk/expo-config-plugin/src/common/types.ts +++ b/packages/react-native-sdk/expo-config-plugin/src/common/types.ts @@ -1,12 +1,6 @@ -export type RingingPushNotifications = { - disableVideoIos?: boolean; - includesCallsInRecentsIos?: boolean; - showWhenLockedAndroid?: boolean; -}; - export type ConfigProps = | { - ringingPushNotifications?: RingingPushNotifications; + ringing?: boolean; enableNonRingingPushNotifications?: boolean; androidPictureInPicture?: boolean; androidKeepCallAlive?: boolean; diff --git a/packages/react-native-sdk/expo-config-plugin/src/withAndroidManifest.ts b/packages/react-native-sdk/expo-config-plugin/src/withAndroidManifest.ts index 3bb3520744..f65481957a 100644 --- a/packages/react-native-sdk/expo-config-plugin/src/withAndroidManifest.ts +++ b/packages/react-native-sdk/expo-config-plugin/src/withAndroidManifest.ts @@ -5,43 +5,7 @@ import { } from '@expo/config-plugins'; import { type ConfigProps } from './common/types'; -const { - prefixAndroidKeys, - getMainApplicationOrThrow, - getMainActivityOrThrow, - ensureToolsAvailable, -} = AndroidConfig.Manifest; - -// extract the type from array -type Unpacked = - T extends Array ? U : T extends ReadonlyArray ? U : T; -// extract the service type -type ManifestService = Unpacked< - NonNullable ->; - -function getNotifeeService(isKeepCallAliveEnabled = false) { - /* We add this service to the AndroidManifest.xml: - - */ - let foregroundServiceType = 'shortService'; - if (isKeepCallAliveEnabled) { - foregroundServiceType = - 'mediaPlayback|camera|microphone|' + foregroundServiceType; - } - let head = prefixAndroidKeys({ - name: 'app.notifee.core.ForegroundService', - stopWithTask: 'true', - foregroundServiceType, - }); - head = { ...head, 'tools:replace': 'android:foregroundServiceType' }; - return { - $: head, - } as ManifestService; -} +const { getMainActivityOrThrow } = AndroidConfig.Manifest; const withStreamVideoReactNativeSDKManifest: ConfigPlugin = ( configuration, @@ -49,20 +13,6 @@ const withStreamVideoReactNativeSDKManifest: ConfigPlugin = ( ) => { return withAndroidManifest(configuration, (config) => { const androidManifest = config.modResults; - const mainApplication = getMainApplicationOrThrow(androidManifest); - if (props?.ringingPushNotifications || props?.androidKeepCallAlive) { - ensureToolsAvailable(androidManifest); - /* Add the notifee foreground Service */ - let services = mainApplication.service ?? []; - // we filter out the existing notifee service (if any) so that we can override it - services = services.filter( - (service) => - service.$['android:name'] !== 'app.notifee.core.ForegroundService', - ); - services.push(getNotifeeService(!!props?.androidKeepCallAlive)); - mainApplication.service = services; - } - if (props?.androidPictureInPicture) { const mainActivity = getMainActivityOrThrow(androidManifest); const currentConfigChangesArray = mainActivity.$['android:configChanges'] diff --git a/packages/react-native-sdk/expo-config-plugin/src/withAndroidPermissions.ts b/packages/react-native-sdk/expo-config-plugin/src/withAndroidPermissions.ts index 8dd911c9a1..83343bb4b5 100644 --- a/packages/react-native-sdk/expo-config-plugin/src/withAndroidPermissions.ts +++ b/packages/react-native-sdk/expo-config-plugin/src/withAndroidPermissions.ts @@ -10,11 +10,7 @@ const withStreamVideoReactNativeSDKAndroidPermissions: ConfigPlugin< 'android.permission.BLUETOOTH_ADMIN', 'android.permission.WAKE_LOCK', ]; - if ( - props?.androidKeepCallAlive || - props?.ringingPushNotifications || - props?.enableScreenshare - ) { + if (props?.androidKeepCallAlive || props?.enableScreenshare) { permissions.push( 'android.permission.POST_NOTIFICATIONS', 'android.permission.FOREGROUND_SERVICE', @@ -25,16 +21,13 @@ const withStreamVideoReactNativeSDKAndroidPermissions: ConfigPlugin< ); } } - if (props?.androidKeepCallAlive || props?.ringingPushNotifications) { + if (props?.androidKeepCallAlive) { permissions.push( 'android.permission.FOREGROUND_SERVICE_CAMERA', 'android.permission.FOREGROUND_SERVICE_MICROPHONE', 'android.permission.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK', ); } - if (props?.ringingPushNotifications?.showWhenLockedAndroid) { - permissions.push('android.permission.USE_FULL_SCREEN_INTENT'); - } const config = AndroidConfig.Permissions.withPermissions( configuration, permissions, diff --git a/packages/react-native-sdk/expo-config-plugin/src/withAppDelegate.ts b/packages/react-native-sdk/expo-config-plugin/src/withAppDelegate.ts index d12c43dad5..4a5a1e574c 100644 --- a/packages/react-native-sdk/expo-config-plugin/src/withAppDelegate.ts +++ b/packages/react-native-sdk/expo-config-plugin/src/withAppDelegate.ts @@ -12,17 +12,14 @@ import { insertContentsInsideObjcFunctionBlock, } from '@expo/config-plugins/build/ios/codeMod'; -import { - type ConfigProps, - type RingingPushNotifications, -} from './common/types'; +import { type ConfigProps } from './common/types'; import addNewLinesToAppDelegateObjc from './common/addNewLinesToAppDelegateObjc'; import { addToSwiftBridgingHeaderFile } from './common/addToSwiftBridgingHeaderFile'; const withAppDelegate: ConfigPlugin = (configuration, props) => { return withAppDelegateUtil(configuration, (config) => { if ( - !props?.ringingPushNotifications && + !props?.ringing && !props?.iOSEnableMultitaskingCameraAccess && !props?.addNoiseCancellation ) { @@ -42,22 +39,15 @@ const withAppDelegate: ConfigPlugin = (configuration, props) => { props.iOSEnableMultitaskingCameraAccess, props.addNoiseCancellation, ); - if (props?.ringingPushNotifications) { + if (props?.ringing) { config.modResults.contents = addObjcImports( config.modResults.contents, - [ - '"RNCallKeep.h"', - '', - '"RNVoipPushNotificationManager.h"', - '"StreamVideoReactNative.h"', - '', - ], + ['', '"StreamVideoReactNative.h"'], ); config.modResults.contents = addDidFinishLaunchingWithOptionsRingingObjc( config.modResults.contents, - props.ringingPushNotifications, ); config.modResults.contents = addDidUpdatePushCredentialsObjc( @@ -67,10 +57,6 @@ const withAppDelegate: ConfigPlugin = (configuration, props) => { config.modResults.contents = addDidReceiveIncomingPushCallbackObjc( config.modResults.contents, ); - - config.modResults.contents = addAudioSessionMethodsObjc( - config.modResults.contents, - ); } return config; } catch (error: any) { @@ -80,7 +66,7 @@ const withAppDelegate: ConfigPlugin = (configuration, props) => { } } else { try { - if (props?.ringingPushNotifications) { + if (props?.ringing) { // make it public class AppDelegate: ExpoAppDelegate, PKPushRegistryDelegate { const regex = /(class\s+AppDelegate[^{]*)(\s*\{)/; config.modResults.contents = config.modResults.contents.replace( @@ -130,15 +116,14 @@ const withAppDelegate: ConfigPlugin = (configuration, props) => { props.iOSEnableMultitaskingCameraAccess, props.addNoiseCancellation, ); - if (props?.ringingPushNotifications) { + if (props?.ringing) { config.modResults.contents = addSwiftImports( config.modResults.contents, - ['RNCallKeep', 'PushKit', 'RNVoipPushNotification'], + ['PushKit', 'stream_video_react_native'], ); config.modResults.contents = addDidFinishLaunchingWithOptionsRingingSwift( config.modResults.contents, - props.ringingPushNotifications, ); config.modResults.contents = addDidUpdatePushCredentialsSwift( @@ -148,10 +133,6 @@ const withAppDelegate: ConfigPlugin = (configuration, props) => { config.modResults.contents = addDidReceiveIncomingPushCallbackSwift( config.modResults.contents, ); - - config.modResults.contents = addAudioSessionMethodsSwift( - config.modResults.contents, - ); } return config; } catch (error: any) { @@ -231,33 +212,10 @@ function addDidFinishLaunchingWithOptionsObjc( return contents; } -function addDidFinishLaunchingWithOptionsRingingSwift( - contents: string, - ringingPushNotifications: RingingPushNotifications, -) { +function addDidFinishLaunchingWithOptionsRingingSwift(contents: string) { const functionSelector = 'application(_:didFinishLaunchingWithOptions:)'; - const supportsVideoString = ringingPushNotifications.disableVideoIos - ? 'false' - : 'true'; - const includesCallsInRecents = - ringingPushNotifications.includesCallsInRecentsIos ? 'false' : 'true'; - const setupCallKeep = ` let localizedAppName = Bundle.main.localizedInfoDictionary?["CFBundleDisplayName"] as? String - let appName = Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String - RNCallKeep.setup([ - "appName": localizedAppName != nil ? localizedAppName! : appName as Any, - "supportsVideo": ${supportsVideoString}, - "includesCallsInRecents": ${includesCallsInRecents}, - ])`; - if (!contents.includes('RNCallKeep.setup')) { - contents = insertContentsInsideSwiftFunctionBlock( - contents, - functionSelector, - setupCallKeep, - { position: 'head' }, - ); - } // call the setup of voip push notification - const voipSetupMethod = 'RNVoipPushNotificationManager.voipRegistration()'; + const voipSetupMethod = 'StreamVideoReactNative.voipRegistration()'; if (!contents.includes(voipSetupMethod)) { contents = insertContentsInsideSwiftFunctionBlock( contents, @@ -269,34 +227,10 @@ function addDidFinishLaunchingWithOptionsRingingSwift( return contents; } -function addDidFinishLaunchingWithOptionsRingingObjc( - contents: string, - ringingPushNotifications: RingingPushNotifications, -) { +function addDidFinishLaunchingWithOptionsRingingObjc(contents: string) { const functionSelector = 'application:didFinishLaunchingWithOptions:'; - // call the setup RNCallKeep - const supportsVideoString = ringingPushNotifications.disableVideoIos - ? '@NO' - : '@YES'; - const includesCallsInRecents = - ringingPushNotifications.includesCallsInRecentsIos ? '@YES' : '@NO'; - const setupCallKeep = `NSString *localizedAppName = [[[NSBundle mainBundle] localizedInfoDictionary] objectForKey:@"CFBundleDisplayName"]; - NSString *appName = [[[NSBundle mainBundle] infoDictionary]objectForKey :@"CFBundleDisplayName"]; - [RNCallKeep setup:@{ - @"appName": localizedAppName != nil ? localizedAppName : appName, - @"supportsVideo": ${supportsVideoString}, - @"includesCallsInRecents": ${includesCallsInRecents}, - }];`; - if (!contents.includes('[RNCallKeep setup:@')) { - contents = insertContentsInsideObjcFunctionBlock( - contents, - functionSelector, - setupCallKeep, - { position: 'head' }, - ); - } // call the setup of voip push notification - const voipSetupMethod = '[RNVoipPushNotificationManager voipRegistration];'; + const voipSetupMethod = '[StreamVideoReactNative voipRegistration];'; if (!contents.includes(voipSetupMethod)) { contents = insertContentsInsideObjcFunctionBlock( contents, @@ -310,7 +244,7 @@ function addDidFinishLaunchingWithOptionsRingingObjc( function addDidUpdatePushCredentialsSwift(contents: string) { const updatedPushCredentialsMethod = - 'RNVoipPushNotificationManager.didUpdate(credentials, forType: type.rawValue)'; + 'StreamVideoReactNative.didUpdate(credentials, forType: type.rawValue)'; if (!contents.includes(updatedPushCredentialsMethod)) { const functionSelector = 'pushRegistry(_:didUpdate:for:)'; @@ -344,7 +278,7 @@ function addDidUpdatePushCredentialsSwift(contents: string) { function addDidUpdatePushCredentialsObjc(contents: string) { const updatedPushCredentialsMethod = - '[RNVoipPushNotificationManager didUpdatePushCredentials:credentials forType:(NSString *)type];'; + '[StreamVideoReactNative didUpdatePushCredentials:credentials forType: (NSString *) type];'; if (!contents.includes(updatedPushCredentialsMethod)) { const functionSelector = 'pushRegistry:didUpdatePushCredentials:forType:'; const codeblock = findObjcFunctionCodeBlock(contents, functionSelector); @@ -366,148 +300,10 @@ function addDidUpdatePushCredentialsObjc(contents: string) { return contents; } -function addAudioSessionMethodsSwift(contents: string) { - const audioSessionDidActivateMethod = - 'RTCAudioSession.sharedInstance().audioSessionDidActivate(AVAudioSession.sharedInstance())'; - if (!contents.includes(audioSessionDidActivateMethod)) { - const functionSelector = 'provider(_:didActivate:)'; - if (!contents.includes('didActivateAudioSession')) { - contents = insertContentsInsideSwiftClassBlock( - contents, - 'class AppDelegate', - ` - func provider(_ provider: CXProvider, didActivateAudioSession audioSession: AVAudioSession) { - ${audioSessionDidActivateMethod} - } - `, - { position: 'tail' }, - ); - } else { - contents = insertContentsInsideSwiftFunctionBlock( - contents, - functionSelector, - audioSessionDidActivateMethod, - { position: 'tail' }, - ); - } - } - const audioSessionDidDeactivateMethod = - 'RTCAudioSession.sharedInstance().audioSessionDidDeactivate(AVAudioSession.sharedInstance())'; - - if (!contents.includes(audioSessionDidDeactivateMethod)) { - const functionSelector = 'provider(_:didDeactivate:)'; - if (!contents.includes('didDeactivateAudioSession')) { - contents = insertContentsInsideSwiftClassBlock( - contents, - 'class AppDelegate', - ` - func provider(_ provider: CXProvider, didDeactivateAudioSession audioSession: AVAudioSession) { - ${audioSessionDidDeactivateMethod} - } - `, - { position: 'tail' }, - ); - } else { - contents = insertContentsInsideSwiftFunctionBlock( - contents, - functionSelector, - audioSessionDidDeactivateMethod, - { position: 'tail' }, - ); - } - } - return contents; -} - -function addAudioSessionMethodsObjc(contents: string) { - const audioSessionDidActivateMethod = - '[[RTCAudioSession sharedInstance] audioSessionDidActivate:[AVAudioSession sharedInstance]];'; - if (!contents.includes(audioSessionDidActivateMethod)) { - const functionSelector = 'provider:didActivateAudioSession:audioSession:'; - const codeblock = findObjcFunctionCodeBlock(contents, functionSelector); - if (!codeblock) { - contents = addNewLinesToAppDelegateObjc(contents, [ - '- (void) provider:(CXProvider *) provider didActivateAudioSession:(AVAudioSession *) audioSession {', - ' ' /* indentation */ + audioSessionDidActivateMethod, - '}', - ]); - } else { - contents = insertContentsInsideObjcFunctionBlock( - contents, - functionSelector, - audioSessionDidActivateMethod, - { position: 'tail' }, - ); - } - } - const audioSessionDidDeactivateMethod = - '[[RTCAudioSession sharedInstance] audioSessionDidDeactivate:[AVAudioSession sharedInstance]];'; - - if (!contents.includes(audioSessionDidDeactivateMethod)) { - const functionSelector = 'provider:didDeactivateAudioSession:audioSession:'; - const codeblock = findObjcFunctionCodeBlock(contents, functionSelector); - if (!codeblock) { - contents = addNewLinesToAppDelegateObjc(contents, [ - '- (void) provider:(CXProvider *) provider didDeactivateAudioSession:(AVAudioSession *) audioSession {', - ' ' /* indentation */ + audioSessionDidDeactivateMethod, - '}', - ]); - } else { - contents = insertContentsInsideObjcFunctionBlock( - contents, - functionSelector, - audioSessionDidDeactivateMethod, - { position: 'tail' }, - ); - } - } - return contents; -} - function addDidReceiveIncomingPushCallbackSwift(contents: string) { const onIncomingPush = ` - guard let stream = payload.dictionaryPayload["stream"] as? [String: Any], - let createdCallerName = stream["created_by_display_name"] as? String, - let cid = stream["call_cid"] as? String else { - completion() - return - } - - // Check if user is busy BEFORE registering the call - let shouldReject = StreamVideoReactNative.shouldRejectCallWhenBusy() - let hasAnyActiveCall = StreamVideoReactNative.hasAnyActiveCall() - - if shouldReject && hasAnyActiveCall { - // Complete the VoIP notification without showing CallKit UI - completion() - return - } - - let uuid = UUID().uuidString - let videoIncluded = stream["video"] as? String - let hasVideo = videoIncluded == "false" ? false : true - - StreamVideoReactNative.registerIncomingCall(cid, uuid: uuid) - - RNVoipPushNotificationManager.addCompletionHandler(uuid, completionHandler: completion) - - RNVoipPushNotificationManager.didReceiveIncomingPush(with: payload, forType: type.rawValue) - - RNCallKeep.reportNewIncomingCall(uuid, - handle: createdCallerName, - handleType: "generic", - hasVideo: hasVideo, - localizedCallerName: createdCallerName, - supportsHolding: false, - supportsDTMF: false, - supportsGrouping: false, - supportsUngrouping: false, - fromPushKit: true, - payload: stream, - withCompletionHandler: nil)`; - if ( - !contents.includes('RNVoipPushNotificationManager.didReceiveIncomingPush') - ) { + StreamVideoReactNative.didReceiveIncomingPush(payload, forType: type.rawValue, completionHandler: completion)`; + if (!contents.includes('StreamVideoReactNative.didReceiveIncomingPush')) { const functionSelector = 'pushRegistry(_:didReceiveIncomingPushWith:for:completion:)'; const codeblock = findSwiftFunctionCodeBlock(contents, functionSelector); @@ -541,53 +337,10 @@ function addDidReceiveIncomingPushCallbackSwift(contents: string) { function addDidReceiveIncomingPushCallbackObjc(contents: string) { const onIncomingPush = ` - // process the payload and store it in the native module's cache - NSDictionary *stream = payload.dictionaryPayload[@"stream"]; - NSString *uuid = [[NSUUID UUID] UUIDString]; - NSString *createdCallerName = stream[@"created_by_display_name"]; - NSString *cid = stream[@"call_cid"]; - - // Check if user is busy BEFORE registering the call - BOOL shouldReject = [StreamVideoReactNative shouldRejectCallWhenBusy]; - BOOL hasAnyActiveCall = [StreamVideoReactNative hasAnyActiveCall]; - - if (shouldReject && hasAnyActiveCall) { - // Complete the VoIP notification without showing CallKit UI - completion(); - return; - } - - NSString *videoIncluded = stream[@"video"]; - BOOL hasVideo = [videoIncluded isEqualToString:@"false"] ? NO : YES; - - // store the call cid and uuid in the native module's cache - [StreamVideoReactNative registerIncomingCall:cid uuid:uuid]; - - // set the completion handler - this one is called by the JS SDK - [RNVoipPushNotificationManager addCompletionHandler:uuid completionHandler:completion]; - - // send event to JS - the JS SDK will handle the rest and call the 'completionHandler' - [RNVoipPushNotificationManager didReceiveIncomingPushWithPayload:payload forType:(NSString *)type]; - - // display the incoming call notification - [RNCallKeep reportNewIncomingCall: uuid - handle: createdCallerName - handleType: @"generic" - hasVideo: hasVideo - localizedCallerName: createdCallerName - supportsHolding: NO - supportsDTMF: NO - supportsGrouping: NO - supportsUngrouping: NO - fromPushKit: YES - payload: stream - withCompletionHandler: nil]; + // process the payload and display the incoming call notification + [StreamVideoReactNative didReceiveIncomingPush:payload forType: (NSString *)type completionHandler:completion]; `; - if ( - !contents.includes( - '[RNVoipPushNotificationManager didReceiveIncomingPushWithPayload', - ) - ) { + if (!contents.includes('[StreamVideoReactNative didReceiveIncomingPush')) { const functionSelector = 'pushRegistry:didReceiveIncomingPushWithPayload:forType:withCompletionHandler:'; const codeblock = findObjcFunctionCodeBlock(contents, functionSelector); diff --git a/packages/react-native-sdk/expo-config-plugin/src/withMainActivity.ts b/packages/react-native-sdk/expo-config-plugin/src/withMainActivity.ts index d6e14e353f..ff44e5a2e3 100644 --- a/packages/react-native-sdk/expo-config-plugin/src/withMainActivity.ts +++ b/packages/react-native-sdk/expo-config-plugin/src/withMainActivity.ts @@ -41,7 +41,7 @@ const withStreamVideoReactNativeSDKMainActivity: ConfigPlugin = ( ); } - if (props?.ringingPushNotifications?.showWhenLockedAndroid) { + if (props?.ringing) { config.modResults.contents = addInsideOnCreateLockscreen( config.modResults.contents, isMainActivityJava, diff --git a/packages/react-native-sdk/expo-config-plugin/src/withiOSInfoPlist.ts b/packages/react-native-sdk/expo-config-plugin/src/withiOSInfoPlist.ts index bfbd55a2b9..b2ee1e4728 100644 --- a/packages/react-native-sdk/expo-config-plugin/src/withiOSInfoPlist.ts +++ b/packages/react-native-sdk/expo-config-plugin/src/withiOSInfoPlist.ts @@ -15,7 +15,7 @@ const withStreamVideoReactNativeSDKiOSInfoPList: ConfigPlugin = ( } } addBackgroundMode('audio'); - if (props?.ringingPushNotifications) { + if (props?.ringing) { addBackgroundMode('voip'); addBackgroundMode('fetch'); addBackgroundMode('processing'); @@ -23,10 +23,7 @@ const withStreamVideoReactNativeSDKiOSInfoPList: ConfigPlugin = ( '$(PRODUCT_BUNDLE_IDENTIFIER)', ]; } - if ( - props?.enableNonRingingPushNotifications || - props?.ringingPushNotifications - ) { + if (props?.enableNonRingingPushNotifications || props?.ringing) { addBackgroundMode('remote-notification'); } return config; diff --git a/packages/react-native-sdk/ios/StreamInCallManager.m b/packages/react-native-sdk/ios/StreamInCallManager.m index cefa76a91b..99915d362f 100644 --- a/packages/react-native-sdk/ios/StreamInCallManager.m +++ b/packages/react-native-sdk/ios/StreamInCallManager.m @@ -23,6 +23,8 @@ @interface RCT_EXTERN_MODULE(StreamInCallManager, RCTEventEmitter) RCT_EXTERN_METHOD(logAudioState) +RCT_EXTERN__BLOCKING_SYNCHRONOUS_METHOD(getAudioStateLog) + RCT_EXTERN_METHOD(muteAudioOutput) RCT_EXTERN_METHOD(unmuteAudioOutput) diff --git a/packages/react-native-sdk/ios/StreamInCallManager.swift b/packages/react-native-sdk/ios/StreamInCallManager.swift index 3c571624c1..e2aea6d979 100644 --- a/packages/react-native-sdk/ios/StreamInCallManager.swift +++ b/packages/react-native-sdk/ios/StreamInCallManager.swift @@ -116,7 +116,7 @@ class StreamInCallManager: RCTEventEmitter { } } else { intendedCategory = .playAndRecord - intendedMode = defaultAudioDevice == .speaker ? .videoChat : .voiceChat + intendedMode = .voiceChat // XCode 16 and older don't expose .allowBluetoothHFP // https://forums.swift.org/t/xcode-26-avaudiosession-categoryoptions-allowbluetooth-deprecated/80956 @@ -132,6 +132,7 @@ class StreamInCallManager: RCTEventEmitter { rtcConfig.category = intendedCategory.rawValue rtcConfig.mode = intendedMode.rawValue rtcConfig.categoryOptions = intendedOptions + // This ensures WebRTC's internal state stays consistent during interruptions/route changes RTCAudioSessionConfiguration.setWebRTC(rtcConfig) let session = RTCAudioSession.sharedInstance() @@ -140,7 +141,7 @@ class StreamInCallManager: RCTEventEmitter { session.unlockForConfiguration() } do { - try session.setCategory(intendedCategory, mode: intendedMode, options: intendedOptions) + try session.setConfiguration(rtcConfig) if (wasRecording) { try adm.setRecording(wasRecording) } @@ -266,6 +267,11 @@ class StreamInCallManager: RCTEventEmitter { @objc func logAudioState() { + log(getAudioStateLog()) + } + + @objc(getAudioStateLog) + func getAudioStateLog() -> String { let session = AVAudioSession.sharedInstance() let adm = getAudioDeviceModule() @@ -278,7 +284,7 @@ class StreamInCallManager: RCTEventEmitter { let rtcAVSession = rtcSession.session let logString = """ - Audio State: + AVAudioSession State: Category: \(session.category.rawValue) Mode: \(session.mode.rawValue) Output Port: \(session.currentRoute.outputs.first?.portName ?? "N/A") @@ -286,10 +292,16 @@ class StreamInCallManager: RCTEventEmitter { Category Options: \(session.categoryOptions) InputNumberOfChannels: \(session.inputNumberOfChannels) OutputNumberOfChannels: \(session.outputNumberOfChannels) - AdmIsPlaying: \(adm.isPlaying) - AdmIsRecording: \(adm.isRecording) + + AudioDeviceModule State: + IsPlaying: \(adm.isPlaying) + IsRecording: \(adm.isRecording) + IsVoiceProcessingAGCEnabled: \(adm.isVoiceProcessingAGCEnabled) + IsVoiceProcessingBypassed: \(adm.isVoiceProcessingBypassed) + IsVoiceProcessingEnabled: \(adm.isVoiceProcessingEnabled) + IsStereoPlayoutEnabled: \(adm.isStereoPlayoutEnabled) - RTC Audio State: + RTCAudioSession State: Wrapped Category: \(rtcAVSession.category.rawValue) Wrapped Mode: \(rtcAVSession.mode.rawValue) Wrapped Output Port: \(rtcAVSession.currentRoute.outputs.first?.portName ?? "N/A") @@ -300,7 +312,7 @@ class StreamInCallManager: RCTEventEmitter { IsActive: \(rtcSession.isActive) ActivationCount: \(rtcSession.activationCount) """ - log(logString) + return logString } @objc(muteAudioOutput) diff --git a/packages/react-native-sdk/ios/StreamVideoReactNative-Bridging-Header.h b/packages/react-native-sdk/ios/StreamVideoReactNative-Bridging-Header.h index d434561a65..d95e1a1866 100644 --- a/packages/react-native-sdk/ios/StreamVideoReactNative-Bridging-Header.h +++ b/packages/react-native-sdk/ios/StreamVideoReactNative-Bridging-Header.h @@ -12,4 +12,6 @@ #import #import #import "WebRTCModule.h" -#import "WebRTCModuleOptions.h" \ No newline at end of file +#import "WebRTCModuleOptions.h" + + diff --git a/packages/react-native-sdk/ios/StreamVideoReactNative.h b/packages/react-native-sdk/ios/StreamVideoReactNative.h index 9683580f3b..d49d3a0892 100644 --- a/packages/react-native-sdk/ios/StreamVideoReactNative.h +++ b/packages/react-native-sdk/ios/StreamVideoReactNative.h @@ -1,16 +1,19 @@ #import #import +#import @interface StreamVideoReactNative : RCTEventEmitter - (void)screenShareEventReceived:(NSString *)event; -+ (void)registerIncomingCall:(NSString *)cid uuid:(NSString *)uuid; - + (void)setup DEPRECATED_MSG_ATTRIBUTE("No need to use setup() anymore"); -+ (BOOL)shouldRejectCallWhenBusy; - + (BOOL)hasAnyActiveCall; ++ (void)voipRegistration; + ++ (void)didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(NSString *)type; + ++ (void)didReceiveIncomingPush:(PKPushPayload *)payload forType:(NSString *)type completionHandler: (void (^_Nullable)(void)) completion; + @end diff --git a/packages/react-native-sdk/ios/StreamVideoReactNative.m b/packages/react-native-sdk/ios/StreamVideoReactNative.m index 7dd4fd9988..848cee9071 100644 --- a/packages/react-native-sdk/ios/StreamVideoReactNative.m +++ b/packages/react-native-sdk/ios/StreamVideoReactNative.m @@ -4,6 +4,7 @@ #import #import #import +#import #import "StreamVideoReactNative.h" #import "WebRTCModule.h" #import "WebRTCModuleOptions.h" @@ -12,21 +13,21 @@ #import // Import Swift-generated header for ScreenShareAudioMixer -#if __has_include() -#import +#if __has_feature(modules) +@import stream_react_native_webrtc.Swift; #elif __has_include("stream_react_native_webrtc-Swift.h") #import "stream_react_native_webrtc-Swift.h" +#elif __has_include() +#import #endif // Do not change these consts, it is what is used react-native-webrtc NSNotificationName const kBroadcastStartedNotification = @"iOS_BroadcastStarted"; NSNotificationName const kBroadcastStoppedNotification = @"iOS_BroadcastStopped"; -static NSMutableDictionary *_incomingCallUUIDsByCallID = nil; -static NSMutableDictionary *_incomingCallCidsByUUID = nil; -static dispatch_queue_t _dictionaryQueue = nil; +static NSString *const DEFAULT_DISPLAY_NAME = @"Unknown Caller"; -static BOOL _shouldRejectCallWhenBusy = NO; +static dispatch_queue_t _dictionaryQueue = nil; void broadcastNotificationCallback(CFNotificationCenterRef center, void *observer, @@ -68,11 +69,200 @@ +(void)initializeSharedDictionaries { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _dictionaryQueue = dispatch_queue_create("com.stream.video.dictionary", DISPATCH_QUEUE_SERIAL); - _incomingCallUUIDsByCallID = [NSMutableDictionary dictionary]; - _incomingCallCidsByUUID = [NSMutableDictionary dictionary]; }); } ++(BOOL)canRegisterCall { + Class callingxClass = NSClassFromString(@"Callingx"); + if (!callingxClass) { + #if DEBUG + NSLog(@"[StreamVideoReactNative][canRegisterCall] Callingx not available"); + #endif + return YES; + } + + SEL selector = @selector(canRegisterCall); + if (![callingxClass respondsToSelector:selector]) { + #if DEBUG + NSLog(@"[StreamVideoReactNative][canRegisterCall] Callingx does not respond to canRegisterCall selector"); + #endif + return YES; + } + + NSMethodSignature *signature = [callingxClass methodSignatureForSelector:selector]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; + [invocation setTarget:callingxClass]; + [invocation setSelector:selector]; + [invocation invoke]; + + BOOL canRegister = NO; + [invocation getReturnValue:&canRegister]; + + #if DEBUG + NSLog(@"[StreamVideoReactNative][canRegisterCall] canRegisterCall = %@", canRegister ? @"YES" : @"NO"); + #endif + + return canRegister; +} + ++(void)voipRegistration { + Class voipManagerClass = NSClassFromString(@"Callingx.VoipNotificationsManager"); + if (!voipManagerClass) { + // Fallback: Try the unmangled name (might work depending on Swift version) + voipManagerClass = NSClassFromString(@"VoipNotificationsManager"); + } + + if (!voipManagerClass) { + #if DEBUG + NSLog(@"[StreamVideoReactNative][voipRegistration] VoipNotificationsManager not available"); + #endif + return; + } + + SEL selector = @selector(voipRegistration); + if (![voipManagerClass respondsToSelector:selector]) { + #if DEBUG + NSLog(@"[StreamVideoReactNative][voipRegistration] VoipNotificationsManager does not respond to voipRegistration"); + #endif + return; + } + + [voipManagerClass voipRegistration]; +} + ++(void)didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(NSString *)type { + Class voipManagerClass = NSClassFromString(@"Callingx.VoipNotificationsManager"); + if (!voipManagerClass) { + // Fallback: Try the unmangled name (might work depending on Swift version) + voipManagerClass = NSClassFromString(@"VoipNotificationsManager"); + } + + if (!voipManagerClass) { + #if DEBUG + NSLog(@"[StreamVideoReactNative][didUpdatePushCredentials] VoipNotificationsManager not available"); + #endif + return; + } + + SEL selector = @selector(didUpdatePushCredentials:forType:); + if (![voipManagerClass respondsToSelector:selector]) { + #if DEBUG + NSLog(@"[StreamVideoReactNative][didUpdatePushCredentials] VoipNotificationsManager does not respond to didUpdatePushCredentials:forType:"); + #endif + return; + } + + [voipManagerClass didUpdatePushCredentials:credentials forType:type]; +} + ++(void)didReceiveIncomingPush:(PKPushPayload *)payload forType:(NSString *)type completionHandler: (void (^_Nullable)(void)) completion { + NSDictionary *streamPayload = payload.dictionaryPayload[@"stream"]; + if (!streamPayload) { + #if DEBUG + NSLog(@"[StreamVideoReactNative][didReceiveIncomingPush] Stream payload not found"); + #endif + if (completion) { + completion(); + } + return; + } + + NSString *callCid = streamPayload[@"call_cid"]; + if (!callCid) { + #if DEBUG + NSLog(@"[StreamVideoReactNative][didReceiveIncomingPush] Missing required field: call_cid"); + #endif + if (completion) { + completion(); + } + return; + } + + if (![StreamVideoReactNative canRegisterCall]) { + if (completion) { + completion(); + } + return; + } + + [StreamVideoReactNative reportNewIncomingCall:streamPayload forType:type completionHandler:completion]; + [StreamVideoReactNative didReceiveIncomingPushWithPayload:payload forType:type]; +} + ++(void)reportNewIncomingCall:(NSDictionary *)streamPayload forType:(NSString *)type completionHandler: (void (^_Nullable)(void)) completion { + Class callingxClass = NSClassFromString(@"Callingx"); + if (!callingxClass) { + NSLog(@"[StreamVideoReactNative][didReceiveIncomingPush] Callingx not available"); + return; + } + + SEL selector = @selector(reportNewIncomingCall:handle:handleType:hasVideo:localizedCallerName:supportsHolding:supportsDTMF:supportsGrouping:supportsUngrouping:payload:withCompletionHandler:); + if (![callingxClass respondsToSelector:selector]) { + #if DEBUG + NSLog(@"[StreamVideoReactNative][didReceiveIncomingPush] Callingx does not respond to selector"); + #endif + return; + } + + NSString *callCid = streamPayload[@"call_cid"]; + NSString *callDisplayName = streamPayload[@"call_display_name"]; + NSString *createdByDisplayName = streamPayload[@"created_by_display_name"]; + NSString *createdCallerName = callDisplayName.length > 0 ? callDisplayName : createdByDisplayName; + NSString *localizedCallerName = createdCallerName.length > 0 ? createdCallerName : DEFAULT_DISPLAY_NAME; + NSString *createdById = streamPayload[@"created_by_id"]; + NSString *handle = createdById.length > 0 ? createdById : localizedCallerName; + NSString *videoIncluded = streamPayload[@"video"]; + BOOL hasVideo = [videoIncluded isEqualToString:@"false"] ? NO : YES; + NSString *handleType = @"generic"; + BOOL supportsHolding = NO; + BOOL supportsDTMF = NO; + BOOL supportsGrouping = NO; + BOOL supportsUngrouping = NO; + void (^completionHandler)(void) = completion; + + NSMethodSignature *signature = [callingxClass methodSignatureForSelector:selector]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; + [invocation setTarget:callingxClass]; + [invocation setSelector:selector]; + [invocation setArgument:&callCid atIndex:2]; + [invocation setArgument:&handle atIndex:3]; + [invocation setArgument:&handleType atIndex:4]; + [invocation setArgument:&hasVideo atIndex:5]; + [invocation setArgument:&localizedCallerName atIndex:6]; + [invocation setArgument:&supportsHolding atIndex:7]; + [invocation setArgument:&supportsDTMF atIndex:8]; + [invocation setArgument:&supportsGrouping atIndex:9]; + [invocation setArgument:&supportsUngrouping atIndex:10]; + [invocation setArgument:&streamPayload atIndex:11]; + [invocation setArgument:&completionHandler atIndex:12]; + [invocation invoke]; +} + ++(void)didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(NSString *)type { + Class voipManagerClass = NSClassFromString(@"Callingx.VoipNotificationsManager"); + if (!voipManagerClass) { + // Fallback: Try the unmangled name (might work depending on Swift version) + voipManagerClass = NSClassFromString(@"VoipNotificationsManager"); + } + + if (!voipManagerClass) { + #if DEBUG + NSLog(@"[StreamVideoReactNative][didReceiveIncomingPushWithPayload] VoipNotificationsManager not available"); + #endif + return; + } + + SEL selector = @selector(didReceiveIncomingPushWithPayload:forType:); + if (![voipManagerClass respondsToSelector:selector]) { + #if DEBUG + NSLog(@"[StreamVideoReactNative][didReceiveIncomingPushWithPayload] VoipNotificationsManager does not respond to didReceiveIncomingPushWithPayload:forType:"); + #endif + return; + } + + [voipManagerClass didReceiveIncomingPushWithPayload:payload forType:type]; +} + -(instancetype)init { if ((self = [super init])) { _notificationCenter = CFNotificationCenterGetDarwinNotifyCenter(); @@ -199,71 +389,6 @@ -(void)screenShareEventReceived:(NSString*)event { } } -+(void)registerIncomingCall:(NSString *)cid uuid:(NSString *)uuid { - [StreamVideoReactNative initializeSharedDictionaries]; - dispatch_sync(_dictionaryQueue, ^{ - -#ifdef DEBUG - NSLog(@"registerIncomingCall cid:%@ -> uuid:%@",cid,uuid); -#endif - NSString *lowercaseUUID = [uuid lowercaseString]; - _incomingCallUUIDsByCallID[cid] = lowercaseUUID; - _incomingCallCidsByUUID[lowercaseUUID] = cid; - }); -} - -RCT_EXPORT_METHOD(getIncomingCallUUid:(NSString *)cid - resolver:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) -{ - dispatch_sync(_dictionaryQueue, ^{ - NSString *uuid = _incomingCallUUIDsByCallID[cid]; - if (uuid) { - resolve(uuid); - } else { - NSString *errorString = [NSString stringWithFormat:@"requested incoming call not found for cid: %@", cid]; - reject(@"access_failure", errorString, nil); - } - }); -} - -RCT_EXPORT_METHOD(getIncomingCallCid:(NSString *)uuid - resolver:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) -{ - dispatch_sync(_dictionaryQueue, ^{ - NSString *lowercaseUUID = [uuid lowercaseString]; - NSString *foundCid = _incomingCallCidsByUUID[lowercaseUUID]; - - if (foundCid) { - resolve(foundCid); - } else { - NSString *errorString = [NSString stringWithFormat:@"requested incoming call not found for uuid: %@", uuid]; - reject(@"access_failure", errorString, nil); - } - }); -} - -RCT_EXPORT_METHOD(removeIncomingCall:(NSString *)cid - resolver:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) -{ - dispatch_sync(_dictionaryQueue, ^{ - NSString *uuid = _incomingCallUUIDsByCallID[cid]; - if (uuid) { -#ifdef DEBUG - NSLog(@"removeIncomingCall cid:%@ -> uuid:%@",cid,uuid); -#endif - - [_incomingCallUUIDsByCallID removeObjectForKey:cid]; - [_incomingCallCidsByUUID removeObjectForKey:uuid]; - resolve(@YES); - } else { - resolve(@NO); - } - }); -} - RCT_EXPORT_METHOD(captureRef:(nonnull NSNumber *)reactTag options:(NSDictionary *)options resolver:(RCTPromiseResolveBlock)resolve @@ -401,17 +526,7 @@ -(void)batteryStateDidChange:(NSNotification *)notification { ]; } -+(BOOL)shouldRejectCallWhenBusy { - return _shouldRejectCallWhenBusy; -} - -RCT_EXPORT_METHOD(setShouldRejectCallWhenBusy:(BOOL)shouldReject) { - _shouldRejectCallWhenBusy = shouldReject; -#ifdef DEBUG - NSLog(@"setShouldRejectCallWhenBusy: %@", shouldReject ? @"YES" : @"NO"); -#endif -} - +//current implementation will return any registered calls not only stream calls + (BOOL)hasAnyActiveCall { CXCallObserver *callObserver = [[CXCallObserver alloc] init]; diff --git a/packages/react-native-sdk/package.json b/packages/react-native-sdk/package.json index 2eb67426dd..1f75821f13 100644 --- a/packages/react-native-sdk/package.json +++ b/packages/react-native-sdk/package.json @@ -64,6 +64,7 @@ "@react-native-firebase/app": ">=17.5.0", "@react-native-firebase/messaging": ">=17.5.0", "@stream-io/noise-cancellation-react-native": ">=0.1.0", + "@stream-io/react-native-callingx": ">=0.1.0", "@stream-io/react-native-webrtc": ">=137.1.3", "@stream-io/video-filters-react-native": ">=0.1.0", "expo": ">=47.0.0", @@ -71,11 +72,9 @@ "expo-notifications": "*", "react": ">=17.0.0", "react-native": ">=0.73.0", - "react-native-callkeep": ">=4.3.11", "react-native-gesture-handler": ">=2.8.0", "react-native-reanimated": ">=2.7.0", - "react-native-svg": ">=13.6.0", - "react-native-voip-push-notification": ">=3.3.1" + "react-native-svg": ">=13.6.0" }, "peerDependenciesMeta": { "@notifee/react-native": { @@ -93,6 +92,9 @@ "@stream-io/noise-cancellation-react-native": { "optional": true }, + "@stream-io/react-native-callingx": { + "optional": true + }, "@stream-io/video-filters-react-native": { "optional": true }, @@ -105,17 +107,11 @@ "expo-notifications": { "optional": true }, - "react-native-callkeep": { - "optional": true - }, "react-native-gesture-handler": { "optional": true }, "react-native-reanimated": { "optional": true - }, - "react-native-voip-push-notification": { - "optional": true } }, "devDependencies": { @@ -130,6 +126,7 @@ "@react-native-firebase/messaging": "^23.4.0", "@react-native/babel-preset": "^0.81.5", "@stream-io/noise-cancellation-react-native": "workspace:^", + "@stream-io/react-native-callingx": "workspace:^", "@stream-io/react-native-webrtc": "137.1.3", "@stream-io/video-filters-react-native": "workspace:^", "@testing-library/jest-native": "^5.4.3", @@ -146,11 +143,9 @@ "react": "19.1.0", "react-native": "^0.81.5", "react-native-builder-bob": "~0.23", - "react-native-callkeep": "^4.3.16", "react-native-gesture-handler": "^2.28.0", "react-native-reanimated": "~4.1.2", "react-native-svg": "^15.14.0", - "react-native-voip-push-notification": "3.3.3", "react-native-worklets": "^0.5.0", "react-test-renderer": "19.1.0", "rimraf": "^6.0.1", diff --git a/packages/react-native-sdk/src/hooks/push/index.ts b/packages/react-native-sdk/src/hooks/push/index.ts index e3b6fdf11e..195b9cecd5 100644 --- a/packages/react-native-sdk/src/hooks/push/index.ts +++ b/packages/react-native-sdk/src/hooks/push/index.ts @@ -1,5 +1,4 @@ import { useIosVoipPushEventsSetupEffect } from './useIosVoipPushEventsSetupEffect'; -import { useProcessPushCallEffect } from './useProcessPushCallEffect'; import { useInitAndroidTokenAndRest } from './useInitAndroidTokenAndRest'; import { useIosInitRemoteNotifications } from './useIosInitRemoteNotifications'; import { useProcessPushNonRingingCallEffect } from './useProcessPushNonRingingCallEffect'; @@ -12,6 +11,5 @@ export const usePushRegisterEffect = () => { useIosInitRemoteNotifications(); useIosVoipPushEventsSetupEffect(); useProcessPushNonRingingCallEffect(); - useProcessPushCallEffect(); useInitAndroidTokenAndRest(); }; diff --git a/packages/react-native-sdk/src/hooks/push/useCallingExpWithCallingStateEffect.ts b/packages/react-native-sdk/src/hooks/push/useCallingExpWithCallingStateEffect.ts new file mode 100644 index 0000000000..2229e7baef --- /dev/null +++ b/packages/react-native-sdk/src/hooks/push/useCallingExpWithCallingStateEffect.ts @@ -0,0 +1,193 @@ +import { Call, CallingState, videoLoggerSystem } from '@stream-io/video-client'; +import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings'; +import { useEffect, useMemo } from 'react'; +import { filter, take } from 'rxjs/operators'; +import { getCallDisplayName } from '../../utils/internal/callingx/callingx'; +import { getCallingxLibIfAvailable } from '../../utils/push/libs/callingx'; + +const logger = videoLoggerSystem.getLogger('callingx'); + +/** + * This hook is used to inform sync call state with CallKit/Telecom (i.e. start call, end call, mute/unmute call). + */ +export const useCallingExpWithCallingStateEffect = () => { + const { useMicrophoneState, useParticipants, useCallMembers } = + useCallStateHooks(); + + const activeCall = useCall(); + const { isMute, microphone } = useMicrophoneState(); + const callMembers = useCallMembers(); + const participants = useParticipants(); + + const activeCallCid = activeCall?.cid; + const createdByUserId = activeCall?.state.createdBy?.id; + const callCustomDisplayName = activeCall?.state.custom?.display_name; + const currentUserId = activeCall?.currentUserId; + const isIncoming = + (activeCall?.ringing && !activeCall?.isCreatedByMe) || false; + + const callDisplayName = useMemo( + () => + callCustomDisplayName ?? + getCallDisplayName(callMembers, participants, currentUserId), + [callMembers, participants, currentUserId, callCustomDisplayName], + ); + + useEffect(() => { + const callingx = getCallingxLibIfAvailable(); + if (!callingx?.isSetup || !activeCall) { + return; + } + // need to capture RINGING -> Joining -> Joined state change for the first time + // and inform callingx that the call is active + const shouldMakeCallActive = (call: Call): boolean => { + // only for outgoing calls or non-ringing ongoing calls in callingx + // Note: incoming calls are handled by callingx pending states instead + return ( + (call.ringing && call.isCreatedByMe) || + (!call.ringing && callingx.isOngoingCallsEnabled) + ); + }; + const subscription = activeCall.state.callingState$ + .pipe( + filter( + (callingState) => + shouldMakeCallActive(activeCall) && + callingState === CallingState.JOINED && + callingx.isCallTracked(activeCall.cid), + ), + take(1), // only need to capture the first joined state for outgoing calls + // then subscription completes and is automatically unsubscribed + ) + .subscribe(() => { + callingx.setCurrentCallActive(activeCall.cid); + }); + return () => { + subscription.unsubscribe(); + }; + }, [activeCall]); + + useEffect(() => { + return () => { + const callingx = getCallingxLibIfAvailable(); + if (!callingx?.isSetup || !activeCallCid) { + return; + } + + const isCallTracked = callingx.isCallTracked(activeCallCid); + if (!isCallTracked) { + logger.debug( + `useCallingExpWithCallingStateEffect:No active call cid to end in calling exp: ${activeCallCid} isCallTracked: ${isCallTracked}`, + ); + return; + } + //if incoming stream call was unmounted, we need to end the call in CallKit/Telecom + logger.debug( + `useCallingExpWithCallingStateEffect: Ending call in callingx: ${activeCallCid}`, + ); + callingx + .endCallWithReason(activeCallCid, 'local') + .catch((error: unknown) => { + logger.error( + `useCallingExpWithCallingStateEffect: Error ending call in callingx: ${activeCallCid}`, + error, + ); + }); + }; + }, [activeCallCid]); + + useEffect(() => { + const callingx = getCallingxLibIfAvailable(); + if (!callingx?.isSetup || !activeCallCid) { + return; + } + + const isCallTracked = callingx.isCallTracked(activeCallCid); + if (!isCallTracked) { + logger.debug( + `useCallingExpWithCallingStateEffect:No active call cid to update callingx: ${activeCallCid} isCallTracked: ${isCallTracked}`, + ); + return; + } + + callingx.updateDisplay( + activeCallCid, + createdByUserId ?? callDisplayName, + callDisplayName, + isIncoming, + ); + }, [activeCallCid, createdByUserId, callDisplayName, isIncoming]); + + // Sync microphone mute state from app → CallKit + useEffect(() => { + const callingx = getCallingxLibIfAvailable(); + if (!callingx?.isSetup || !activeCallCid) { + return; + } + + const isCallTracked = callingx.isCallTracked(activeCallCid); + if (!isCallTracked) { + logger.debug( + `useCallingExpWithCallingStateEffect: No active call cid to set muted in calling exp: ${activeCallCid} isCallTracked: ${isCallTracked}`, + ); + return; + } + + callingx.setMutedCall(activeCallCid, isMute); + }, [activeCallCid, isMute]); + + // Sync mute state from CallKit → app (only for system-initiated mute actions) + useEffect(() => { + const callingx = getCallingxLibIfAvailable(); + if (!callingx?.isSetup || !activeCallCid) { + logger.debug( + `useCallingExpWithCallingStateEffect: No active call cid to set muted in calling exp: ${activeCallCid} callingx isSetup: ${callingx?.isSetup}`, + ); + return; + } + + // Listen to mic toggle events from CallKit/Telecom and update stream call microphone state. + // Only system-initiated mute actions (e.g. user tapped mute on the native CallKit UI) + // are sent here — app-initiated actions are filtered out on the native side to prevent + // the feedback loop: app mutes mic → setMutedCall → CallKit delegate → event to JS → loop. + const subscription = callingx.addEventListener( + 'didPerformSetMutedCallAction', + async (event: { callId: string; muted: boolean }) => { + const { callId, muted } = event; + + const isCallTracked = callingx.isCallTracked(activeCallCid); + if (!isCallTracked || callId !== activeCallCid) { + logger.debug( + `useCallingExpWithCallingStateEffect: No active call cid to set muted in calling exp: ${activeCallCid} isCallTracked: ${isCallTracked} callId: ${callId}`, + ); + return; + } + + const isCurrentlyMuted = microphone.state.status === 'disabled'; + if (isCurrentlyMuted === muted) { + logger.debug( + `useCallingExpWithCallingStateEffect: Mic toggle is already in the desired state: ${muted} for call: ${activeCallCid}`, + ); + return; + } + + try { + if (muted) { + await microphone.disable(); + } else { + await microphone.enable(); + } + } catch (error: unknown) { + logger.error( + `useCallingExpWithCallingStateEffect: Error toggling mic in calling exp: ${activeCallCid}`, + error, + ); + } + }, + ); + + return () => { + subscription.remove(); + }; + }, [activeCallCid, microphone]); +}; diff --git a/packages/react-native-sdk/src/hooks/push/useIosCallkeepWithCallingStateEffect.ts b/packages/react-native-sdk/src/hooks/push/useIosCallkeepWithCallingStateEffect.ts deleted file mode 100644 index 24b6de9b4d..0000000000 --- a/packages/react-native-sdk/src/hooks/push/useIosCallkeepWithCallingStateEffect.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { - CallingState, - RxUtils, - videoLoggerSystem, -} from '@stream-io/video-client'; -import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings'; -import { NativeModules, Platform } from 'react-native'; -import { useEffect, useState } from 'react'; -import { StreamVideoRN } from '../../utils'; -import { getCallKeepLib } from '../../utils/push/libs'; -import { - voipCallkeepAcceptedCallOnNativeDialerMap$, - voipCallkeepCallOnForegroundMap$, - voipPushNotificationCallCId$, -} from '../../utils/push/internal/rxSubjects'; - -const isNonActiveCallingState = (callingState: CallingState) => { - return ( - callingState === CallingState.IDLE || - callingState === CallingState.UNKNOWN || - callingState === CallingState.LEFT - ); -}; - -const isAcceptedCallingState = (callingState: CallingState) => { - return ( - callingState === CallingState.JOINING || - callingState === CallingState.JOINED - ); -}; - -const unsubscribeCallkeepEvents = async (activeCallCid: string | undefined) => { - const voipPushNotificationCallCId = RxUtils.getCurrentValue( - voipPushNotificationCallCId$, - ); - if (activeCallCid && activeCallCid === voipPushNotificationCallCId) { - // callkeep events should not be listened anymore so clear the call cid - voipPushNotificationCallCId$.next(undefined); - } - return await NativeModules.StreamVideoReactNative?.removeIncomingCall( - activeCallCid, - ); -}; - -const logger = videoLoggerSystem.getLogger( - 'useIosCallkeepWithCallingStateEffect', -); -const log = (message: string) => { - logger.warn(message); -}; - -/** - * This hook is used to inform the callkeep library that the call has been joined or ended. - */ -export const useIosCallkeepWithCallingStateEffect = () => { - const activeCall = useCall(); - const { useCallCallingState } = useCallStateHooks(); - const callingState = useCallCallingState(); - const [acceptedForegroundCallkeepMap, setAcceptedForegroundCallkeepMap] = - useState<{ - uuid: string; - cid: string; - }>(); - - useEffect(() => { - return () => { - const pushConfig = StreamVideoRN.getConfig().push; - if ( - Platform.OS !== 'ios' || - !pushConfig || - !pushConfig.ios?.pushProviderName - ) { - return; - } - if (!pushConfig.android.incomingCallChannel) { - // TODO: remove this check and find a better way once we have telecom integration for android - return; - } - - const callkeep = getCallKeepLib(); - // if the component is unmounted and the callID was not reported to callkeep, then report it now - if (acceptedForegroundCallkeepMap) { - log( - `Ending call in callkeep: ${acceptedForegroundCallkeepMap.cid}, reason: component unmounted and call was present in acceptedForegroundCallkeepMap`, - ); - unsubscribeCallkeepEvents(acceptedForegroundCallkeepMap.cid).then(() => - callkeep.endCall(acceptedForegroundCallkeepMap.uuid), - ); - } - }; - }, [acceptedForegroundCallkeepMap]); - - const activeCallCid = activeCall?.cid; - - useEffect(() => { - return () => { - const pushConfig = StreamVideoRN.getConfig().push; - if ( - Platform.OS !== 'ios' || - !pushConfig || - !pushConfig.ios?.pushProviderName || - !activeCallCid - ) { - return; - } - if (!pushConfig.android.incomingCallChannel) { - // TODO: remove this check and find a better way once we have telecom integration for android - return; - } - const nativeDialerAcceptedCallMap = RxUtils.getCurrentValue( - voipCallkeepAcceptedCallOnNativeDialerMap$, - ); - const foregroundIncomingCallkeepMap = RxUtils.getCurrentValue( - voipCallkeepCallOnForegroundMap$, - ); - const callkeep = getCallKeepLib(); - if (activeCallCid === nativeDialerAcceptedCallMap?.cid) { - log( - `Ending call in callkeep: ${activeCallCid}, reason: activeCallCid changed or was removed and call was present in nativeDialerAcceptedCallMap`, - ); - unsubscribeCallkeepEvents(activeCallCid).then(() => - callkeep.endCall(nativeDialerAcceptedCallMap.uuid), - ); - // no need to keep this reference anymore - voipCallkeepAcceptedCallOnNativeDialerMap$.next(undefined); - } else if (activeCallCid === foregroundIncomingCallkeepMap?.cid) { - log( - `Ending call in callkeep: ${activeCallCid}, reason: activeCallCid changed or was removed and call was present in foregroundIncomingCallkeepMap`, - ); - unsubscribeCallkeepEvents(activeCallCid).then(() => - callkeep.endCall(foregroundIncomingCallkeepMap.uuid), - ); - } - }; - }, [activeCallCid]); - - const pushConfig = StreamVideoRN.getConfig().push; - if ( - Platform.OS !== 'ios' || - !pushConfig || - !pushConfig.ios.pushProviderName || - !activeCallCid - ) { - return; - } - if (!pushConfig.android.incomingCallChannel) { - // TODO: remove this check and find a better way once we have telecom integration for android - return; - } - - /** - * Check if current call is still needed to be accepted in callkeep - */ - if ( - isAcceptedCallingState(callingState) && - acceptedForegroundCallkeepMap?.cid !== activeCallCid - ) { - const callkeep = getCallKeepLib(); - // push notification was displayed - // but the call has been accepted through the app and not through the native dialer - const foregroundCallkeepMap = RxUtils.getCurrentValue( - voipCallkeepCallOnForegroundMap$, - ); - if (foregroundCallkeepMap && foregroundCallkeepMap.cid === activeCallCid) { - log( - // @ts-expect-error - types issue - `Accepting call in callkeep: ${activeCallCid}, reason: callingstate went to ${CallingState[callingState]} and call was present in foregroundCallkeepMap`, - ); - // no need to keep this reference anymore - voipCallkeepCallOnForegroundMap$.next(undefined); - NativeModules.StreamVideoReactNative?.removeIncomingCall( - activeCallCid, - ).then(() => callkeep.answerIncomingCall(foregroundCallkeepMap.uuid)); - // this call should be accepted in callkeep - setAcceptedForegroundCallkeepMap(foregroundCallkeepMap); - } - } - - /** - * Check if current call is still needed to be ended in callkeep - */ - if (isNonActiveCallingState(callingState)) { - const callkeep = getCallKeepLib(); - - // this was a previously joined call which had push notification displayed - // the call was accepted through the app and not through native dialer - // the call was left using the leave button in the app and not through native dialer - if (activeCallCid === acceptedForegroundCallkeepMap?.cid) { - log( - // @ts-expect-error - types issue - `Ending call in callkeep: ${activeCallCid}, reason: callingstate went to ${CallingState[callingState]} and call was present in acceptedForegroundCallkeepMap`, - ); - unsubscribeCallkeepEvents(activeCallCid).then(() => - callkeep.endCall(acceptedForegroundCallkeepMap.uuid), - ); - setAcceptedForegroundCallkeepMap(undefined); - return; - } - // this was a call which had push notification displayed but never joined - // the user rejected in the app and not from native dialer - const foregroundIncomingCallkeepMap = RxUtils.getCurrentValue( - voipCallkeepCallOnForegroundMap$, - ); - if (activeCallCid === foregroundIncomingCallkeepMap?.cid) { - log( - // @ts-expect-error - types issue - `Ending call in callkeep: ${activeCallCid}, reason: callingstate went to ${CallingState[callingState]} and call was present in foregroundIncomingCallkeepMap`, - ); - unsubscribeCallkeepEvents(activeCallCid).then(() => - callkeep.endCall(foregroundIncomingCallkeepMap.uuid), - ); - // no need to keep this reference anymore - voipCallkeepCallOnForegroundMap$.next(undefined); - return; - } - // this was a previously joined call - // it was an accepted call from native dialer and not from the app - // the user left using the leave button in the app - const nativeDialerAcceptedCallMap = RxUtils.getCurrentValue( - voipCallkeepAcceptedCallOnNativeDialerMap$, - ); - if (activeCallCid === nativeDialerAcceptedCallMap?.cid) { - log( - // @ts-expect-error - types issue - `Ending call in callkeep: ${activeCallCid}, reason: callingstate went to ${CallingState[callingState]} and call was present in nativeDialerAcceptedCallMap`, - ); - unsubscribeCallkeepEvents(activeCallCid).then(() => - callkeep.endCall(nativeDialerAcceptedCallMap.uuid), - ); - // no need to keep this reference anymore - voipCallkeepAcceptedCallOnNativeDialerMap$.next(undefined); - return; - } - } -}; diff --git a/packages/react-native-sdk/src/hooks/push/useIosVoipPushEventsSetupEffect.ts b/packages/react-native-sdk/src/hooks/push/useIosVoipPushEventsSetupEffect.ts index f26156537e..f6a35dc3a3 100644 --- a/packages/react-native-sdk/src/hooks/push/useIosVoipPushEventsSetupEffect.ts +++ b/packages/react-native-sdk/src/hooks/push/useIosVoipPushEventsSetupEffect.ts @@ -1,6 +1,4 @@ import { type MutableRefObject, useEffect, useRef, useState } from 'react'; -import { getVoipPushNotificationLib } from '../../utils/push/libs'; - import { Platform } from 'react-native'; import { StreamVideoRN } from '../../utils'; import { onVoipNotificationReceived } from '../../utils/push/internal/ios'; @@ -10,6 +8,7 @@ import { } from '@stream-io/video-react-bindings'; import { setPushLogoutCallback } from '../../utils/internal/pushLogoutCallback'; import { StreamVideoClient, videoLoggerSystem } from '@stream-io/video-client'; +import { getCallingxLibIfAvailable } from '../../utils/push/libs'; const logger = videoLoggerSystem.getLogger('useIosVoipPushEventsSetupEffect'); @@ -28,6 +27,7 @@ function setLogoutCallback( lastVoipTokenRef.current = { token: '', userId: '' }; try { await client.removeDevice(token); + logger.debug('PushLogoutCallback - Removed voip token', token); } catch (err) { logger.warn('PushLogoutCallback - Failed to remove voip token', err); } @@ -89,24 +89,12 @@ export const useIosVoipPushEventsSetupEffect = () => { useEffect(() => { const pushConfig = StreamVideoRN.getConfig().push; const pushProviderName = pushConfig?.ios.pushProviderName; - if (Platform.OS !== 'ios' || !client || !pushProviderName) { - return; - } - if (!pushConfig.android.incomingCallChannel) { - // TODO: remove this check and find a better way once we have telecom integration for android - logger.debug( - 'android incomingCallChannel is not defined, so skipping the useIosVoipPushEventsSetupEffect', - ); + const callingx = getCallingxLibIfAvailable(); + + if (Platform.OS !== 'ios' || !client || !pushProviderName || !callingx) { return; } - const voipPushNotification = getVoipPushNotificationLib(); - - // even though we do this natively, we have to still register here again - // natively this will make sure "register" event for JS is sent with the last push token - // Necessary if client changed before we got the event here or user logged out and logged in again - voipPushNotification.registerVoipToken(); - const onTokenReceived = (token: string) => { const userId = client.streamClient._user?.id ?? ''; if (client.streamClient.anonymous || !token || !userId) { @@ -145,24 +133,24 @@ export const useIosVoipPushEventsSetupEffect = () => { }); }; // fired when PushKit give us the latest token - voipPushNotification.addEventListener('register', (token) => { - onTokenReceived(token); - }); + const voipRegisterListener = callingx.addEventListener( + 'voipNotificationsRegistered', + ({ token }) => { + onTokenReceived(token); + }, + ); - // this will fire when there are events occured before js bridge initialized - voipPushNotification.addEventListener('didLoadWithEvents', (events) => { - if (!events || !Array.isArray(events) || events.length < 1) { - return; - } - for (const voipPushEvent of events) { - const { name, data } = voipPushEvent; - if (name === 'RNVoipPushRemoteNotificationsRegisteredEvent') { - onTokenReceived(data); - } else if (name === 'RNVoipPushRemoteNotificationReceivedEvent') { - onVoipNotificationReceived(data, pushConfig); - } + // this will return events that were fired before js bridge initialized + callingx.getInitialVoipEvents().forEach(({ eventName, params }) => { + if (eventName === 'voipNotificationsRegistered' && 'token' in params) { + onTokenReceived(params.token); + } else if (eventName === 'voipNotificationReceived') { + onVoipNotificationReceived(params, pushConfig); } }); + + callingx.registerVoipToken(); + lastListener.count += 1; const currentListenerCount = lastListener.count; @@ -175,8 +163,7 @@ export const useIosVoipPushEventsSetupEffect = () => { return; } logger.debug(`Voip event listeners are removed for user: ${userId}`); - voipPushNotification.removeEventListener('didLoadWithEvents'); - voipPushNotification.removeEventListener('register'); + voipRegisterListener.remove(); }; }, [client]); }; diff --git a/packages/react-native-sdk/src/hooks/push/useProcessPushCallEffect.ts b/packages/react-native-sdk/src/hooks/push/useProcessPushCallEffect.ts deleted file mode 100644 index 58b4f0b934..0000000000 --- a/packages/react-native-sdk/src/hooks/push/useProcessPushCallEffect.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { - pushAcceptedIncomingCallCId$, - pushAndroidBackgroundDeliveredIncomingCallCId$, - pushRejectedIncomingCallCId$, - pushTappedIncomingCallCId$, -} from '../../utils/push/internal/rxSubjects'; -import { useEffect } from 'react'; -import { StreamVideoRN } from '../../utils'; -import { - useConnectedUser, - useStreamVideoClient, -} from '@stream-io/video-react-bindings'; -import { BehaviorSubject } from 'rxjs'; -import { distinctUntilChanged, filter } from 'rxjs/operators'; -import { processCallFromPush } from '../../utils/push/internal/utils'; -import { StreamVideoClient, videoLoggerSystem } from '@stream-io/video-client'; -import type { StreamVideoConfig } from '../../utils/StreamVideoRN/types'; - -/** - * This hook is used to process the incoming call data via push notifications using the relevant rxjs subjects - * It either joins or leaves the call based on the user's action. - * Note: this effect cannot work when push notifications are received when the app is in quit state or in other words when the client is not connected with a websocket. - * So we essentially run this effect only when the client is connected with a websocket. - */ -export const useProcessPushCallEffect = () => { - const client = useStreamVideoClient(); - const connectedUserId = useConnectedUser()?.id; - // The Effect to join/reject call automatically when incoming call was received and processed from push notification - useEffect(() => { - const pushConfig = StreamVideoRN.getConfig().push; - if (!pushConfig || !client || !connectedUserId) { - return; - } - - videoLoggerSystem - .getLogger('useProcessPushCallEffect') - .debug( - `Adding subscriptions to process incoming call from push notification`, - ); - - // if the user accepts the call from push notification we join the call - const acceptedCallSubscription = createCallSubscription( - pushAcceptedIncomingCallCId$, - client, - pushConfig, - 'accept', - ); - - // if the user rejects the call from push notification we leave the call - const declinedCallSubscription = createCallSubscription( - pushRejectedIncomingCallCId$, - client, - pushConfig, - 'decline', - ); - - // if the user taps the call from push notification we do nothing as the only thing is to get the call which adds it to the client - const pressedCallSubscription = createCallSubscription( - pushTappedIncomingCallCId$, - client, - pushConfig, - 'pressed', - ); - - const backgroundIncomingDeliveredCallSubscription = createCallSubscription( - pushAndroidBackgroundDeliveredIncomingCallCId$, - client, - pushConfig, - 'backgroundDelivered', - ); - - return () => { - acceptedCallSubscription.unsubscribe(); - declinedCallSubscription.unsubscribe(); - pressedCallSubscription.unsubscribe(); - backgroundIncomingDeliveredCallSubscription.unsubscribe(); - }; - }, [client, connectedUserId]); -}; - -/** - * A type guard to check if the cid is not undefined - */ -function cidIsNotUndefined(cid: string | undefined): cid is string { - return cid !== undefined; -} - -/** - * The common logic to create a subscription for the given call cid and action - */ -const createCallSubscription = ( - behaviourSubjectWithCallCid: BehaviorSubject, - client: StreamVideoClient, - pushConfig: NonNullable, - action: 'accept' | 'decline' | 'pressed' | 'backgroundDelivered', -) => { - return behaviourSubjectWithCallCid - .pipe(distinctUntilChanged(), filter(cidIsNotUndefined)) - .subscribe(async (callCId) => { - videoLoggerSystem - .getLogger('useProcessPushCallEffect') - .debug( - `Processing call from push notification with action: ${action} and callCId: ${callCId}`, - ); - await processCallFromPush(client, callCId, action, pushConfig); - behaviourSubjectWithCallCid.next(undefined); // remove the current call id to avoid processing again - }); -}; diff --git a/packages/react-native-sdk/src/hooks/useAndroidKeepCallAliveEffect.ts b/packages/react-native-sdk/src/hooks/useAndroidKeepCallAliveEffect.ts index 1506a18f37..0ec2087253 100644 --- a/packages/react-native-sdk/src/hooks/useAndroidKeepCallAliveEffect.ts +++ b/packages/react-native-sdk/src/hooks/useAndroidKeepCallAliveEffect.ts @@ -5,76 +5,45 @@ import { AppState, type AppStateStatus, NativeModules, + PermissionsAndroid, Platform, } from 'react-native'; -import { Call, CallingState, videoLoggerSystem } from '@stream-io/video-client'; -import { - getKeepCallAliveForegroundServiceTypes, - getNotifeeLibNoThrowForKeepCallAlive, -} from '../utils/push/libs/notifee'; - -const notifeeLib = getNotifeeLibNoThrowForKeepCallAlive(); -const callToPassToForegroundService: { current: Call | undefined } = { - current: undefined, -}; +import { CallingState, videoLoggerSystem } from '@stream-io/video-client'; +import { keepCallAliveCallRef } from '../utils/keepCallAliveHeadlessTask'; +import { getCallingxLibIfAvailable } from '../utils/push/libs'; -function setForegroundService() { - if (Platform.OS === 'ios' || !notifeeLib) return; - NativeModules.StreamVideoReactNative.isCallAliveConfigured().then( - (isConfigured: boolean) => { - if (!isConfigured) { - const logger = videoLoggerSystem.getLogger( - 'setForegroundService method', - ); - logger.info( - 'KeepCallAlive is not configured. Skipping foreground service setup.', - ); - return; - } - notifeeLib.default.registerForegroundService(() => { - const task = new Promise((resolve) => { - const logger = videoLoggerSystem.getLogger( - 'setForegroundService method', - ); - logger.info('Foreground service running for call in progress'); - // any task to run from SDK in the foreground service must be added - resolve(true); - }); - const videoConfig = StreamVideoRN.getConfig(); - const foregroundServiceConfig = videoConfig.foregroundService; - const { taskToRun } = foregroundServiceConfig.android; - const call = callToPassToForegroundService.current; - if (!call) { - const logger = videoLoggerSystem.getLogger( - 'setForegroundService method', - ); - logger.warn('No call to pass to foreground service'); - return task.then(() => new Promise(() => {})); - } - callToPassToForegroundService.current = undefined; - return task.then(() => taskToRun(call)); - }); - }, - ); +async function stopForegroundServiceNoThrow() { + const logger = videoLoggerSystem.getLogger('stopForegroundServiceNoThrow'); + try { + await NativeModules.StreamVideoReactNative.stopKeepCallAliveService(); + } catch (e) { + logger.warn('Failed to stop keep-call-alive foreground service', e); + } } async function startForegroundService(call_cid: string) { - const isCallAliveConfigured = - await NativeModules.StreamVideoReactNative.isCallAliveConfigured(); + const logger = videoLoggerSystem.getLogger('startForegroundService'); + const isCallAliveConfigured = await (async () => { + try { + return await NativeModules.StreamVideoReactNative.isCallAliveConfigured(); + } catch (e) { + logger.warn('Failed to check whether KeepCallAlive is configured', e); + return false; + } + })(); if (!isCallAliveConfigured) { - const logger = videoLoggerSystem.getLogger('startForegroundService'); logger.info( 'KeepCallAlive is not configured. Skipping foreground service setup.', ); return; } - // check for notification permission and then start the foreground service - if (!notifeeLib) return; - const settings = await notifeeLib.default.getNotificationSettings(); - if ( - settings.authorizationStatus !== notifeeLib.AuthorizationStatus.AUTHORIZED - ) { - const logger = videoLoggerSystem.getLogger('startForegroundService'); + // Check for notification permission (Android 13+) before starting the service. + const hasPostNotificationsPermission = + Number(Platform.Version) < 33 || + (await PermissionsAndroid.check( + PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS, + )); + if (!hasPostNotificationsPermission) { logger.info( 'Notification permission not granted, can not start foreground service to keep the call alive', ); @@ -83,38 +52,28 @@ async function startForegroundService(call_cid: string) { const videoConfig = StreamVideoRN.getConfig(); const foregroundServiceConfig = videoConfig.foregroundService; const notificationTexts = foregroundServiceConfig.android.notificationTexts; - const channelId = foregroundServiceConfig.android.channel.id; - await notifeeLib.default.createChannel( - foregroundServiceConfig.android.channel, - ); - const foregroundServiceTypes = await getKeepCallAliveForegroundServiceTypes(); + const channel = foregroundServiceConfig.android.channel; + const smallIconName = videoConfig.push?.android.smallIcon; + // NOTE: we use requestAnimationFrame to ensure that the foreground service is started after all the current UI operations are done // this is a workaround for the crash - android.app.RemoteServiceException$ForegroundServiceDidNotStartInTimeException: Context.startForegroundService() did not then call Service.startForeground() // this crash was reproducible only in some android devices - requestAnimationFrame(() => { - notifeeLib.default.displayNotification({ - id: call_cid, - title: notificationTexts.title, - body: notificationTexts.body, - android: { - channelId, - smallIcon: videoConfig.push?.android.smallIcon, - foregroundServiceTypes, - asForegroundService: true, - ongoing: true, // user cannot dismiss the notification - colorized: true, - pressAction: { - id: 'default', - launchActivity: 'default', // open the app when the notification is pressed - }, - }, - }); + requestAnimationFrame(async () => { + try { + await NativeModules.StreamVideoReactNative.startKeepCallAliveService( + call_cid, + channel.id, + channel.name, + notificationTexts.title, + notificationTexts.body, + smallIconName ?? null, + ); + } catch (e) { + logger.warn('Failed to start keep-call-alive foreground service', e); + } }); } -// flag to check if setForegroundService has already been run once -let isSetForegroundServiceRan = false; - /** * This hook is used to keep the call alive in the background for Android. * It starts a foreground service to keep the call alive as soon as the call is joined @@ -125,7 +84,7 @@ export const useAndroidKeepCallAliveEffect = () => { const foregroundServiceStartedRef = useRef(false); const call = useCall(); - callToPassToForegroundService.current = call; + keepCallAliveCallRef.current = call; const activeCallCid = call?.cid; const { useCallCallingState } = useCallStateHooks(); const callingState = useCallCallingState(); @@ -133,6 +92,7 @@ export const useAndroidKeepCallAliveEffect = () => { const isOutgoingCall = callingState === CallingState.RINGING && call?.isCreatedByMe; const isCallJoined = callingState === CallingState.JOINED; + const isRingingCall = call?.ringing; const shouldStartForegroundService = !foregroundServiceStartedRef.current && (isOutgoingCall || isCallJoined); @@ -141,7 +101,14 @@ export const useAndroidKeepCallAliveEffect = () => { if (Platform.OS === 'ios' || !activeCallCid) { return undefined; } - if (!notifeeLib) return undefined; + + const callingx = getCallingxLibIfAvailable(); + if ( + callingx?.isSetup && + (isRingingCall || (!isRingingCall && callingx?.isOngoingCallsEnabled)) + ) { + return undefined; + } // start foreground service as soon as the call is joined if (shouldStartForegroundService) { @@ -149,23 +116,6 @@ export const useAndroidKeepCallAliveEffect = () => { if (foregroundServiceStartedRef.current) { return; } - if (!isSetForegroundServiceRan) { - isSetForegroundServiceRan = true; - setForegroundService(); - } - const notifee = notifeeLib.default; - const displayedNotifications = - await notifee.getDisplayedNotifications(); - const activeCallNotification = displayedNotifications.find( - (notification) => notification.id === activeCallCid, - ); - if (activeCallNotification) { - callToPassToForegroundService.current = undefined; - // this means that we have a incoming call notification shown as foreground service and we must stop it - notifee.stopForegroundService(); - notifee.cancelDisplayedNotification(activeCallCid); - } - // check for notification permission and then start the foreground service await startForegroundService(activeCallCid); foregroundServiceStartedRef.current = true; @@ -174,7 +124,6 @@ export const useAndroidKeepCallAliveEffect = () => { // ensure that app is active before running the function if (AppState.currentState === 'active') { run(); - return undefined; } const sub = AppState.addEventListener( 'change', @@ -188,46 +137,31 @@ export const useAndroidKeepCallAliveEffect = () => { return () => { sub.remove(); }; - } else if (callingState === CallingState.RINGING) { - return () => { - // cancel any notifee displayed notification when the call has transitioned out of ringing - // NOTE: cancels only the non fg service notifications - notifeeLib.default.cancelDisplayedNotification(activeCallCid); - }; } else if ( callingState === CallingState.IDLE || callingState === CallingState.LEFT ) { if (foregroundServiceStartedRef.current) { - callToPassToForegroundService.current = undefined; + keepCallAliveCallRef.current = undefined; // stop foreground service when the call is not active - notifeeLib.default.stopForegroundService(); + stopForegroundServiceNoThrow(); foregroundServiceStartedRef.current = false; - } else { - notifeeLib.default - .getDisplayedNotifications() - .then((displayedNotifications) => { - const activeCallNotification = displayedNotifications.find( - (notification) => notification.id === activeCallCid, - ); - if (activeCallNotification) { - callToPassToForegroundService.current = undefined; - // this means that we have a incoming call notification shown as foreground service and we must stop it - notifeeLib.default.stopForegroundService(); - } - }); } } return undefined; - }, [activeCallCid, callingState, shouldStartForegroundService]); + }, [ + activeCallCid, + callingState, + shouldStartForegroundService, + isRingingCall, + ]); useEffect(() => { return () => { // stop foreground service when this effect is unmounted if (foregroundServiceStartedRef.current) { - if (!notifeeLib) return; - callToPassToForegroundService.current = undefined; - notifeeLib.default.stopForegroundService(); + keepCallAliveCallRef.current = undefined; + stopForegroundServiceNoThrow(); foregroundServiceStartedRef.current = false; } }; diff --git a/packages/react-native-sdk/src/index.ts b/packages/react-native-sdk/src/index.ts index ed5bd2679c..ab55ca1eb5 100644 --- a/packages/react-native-sdk/src/index.ts +++ b/packages/react-native-sdk/src/index.ts @@ -9,6 +9,7 @@ import { registerGlobals } from '@stream-io/react-native-webrtc'; import Logger from '@stream-io/react-native-webrtc/src/Logger'; import { Platform } from 'react-native'; import { registerSDKGlobals } from './utils/internal/registerSDKGlobals'; +import './utils/keepCallAliveHeadlessTask'; // We're registering globals, because our video JS client is serving SDKs that use browser based webRTC functions. // This will result in creation of 2 global objects: `window` and `navigator` diff --git a/packages/react-native-sdk/src/modules/call-manager/CallManager.ts b/packages/react-native-sdk/src/modules/call-manager/CallManager.ts index 493b50f9d3..a9e1a4a780 100644 --- a/packages/react-native-sdk/src/modules/call-manager/CallManager.ts +++ b/packages/react-native-sdk/src/modules/call-manager/CallManager.ts @@ -1,7 +1,10 @@ import { NativeEventEmitter, NativeModules, Platform } from 'react-native'; import { AudioDeviceStatus, StreamInCallManagerConfig } from './types'; +import { getCallingxLibIfAvailable } from '../../utils/push/libs/callingx'; +import { videoLoggerSystem } from '@stream-io/video-client'; const NativeManager = NativeModules.StreamInCallManager; +const CallingxModule = getCallingxLibIfAvailable(); const invariant = (condition: boolean, message: string) => { if (!condition) throw new Error(message); @@ -72,6 +75,19 @@ class SpeakerManager { }; } +const shouldBypassForCallKit = (): boolean => { + if (Platform.OS !== 'ios') { + return false; + } + if (!CallingxModule) { + return false; + } + return ( + CallingxModule.isSetup && + (CallingxModule.hasRegisteredCall() || CallingxModule.isOngoingCallsEnabled) + ); +}; + export class CallManager { android = new AndroidCallManager(); ios = new IOSCallManager(); @@ -95,6 +111,14 @@ export class CallManager { * @param config.enableStereoAudioOutput Whether to enable stereo audio output. Only supported for listener audio role. */ start = (config?: StreamInCallManagerConfig): void => { + if (shouldBypassForCallKit()) { + videoLoggerSystem + .getLogger('CallManager') + .debug( + 'start: skipping start as callkit is handling the audio session', + ); + return; + } NativeManager.setAudioRole(config?.audioRole ?? 'communicator'); if (config?.audioRole === 'communicator') { const type = config.deviceEndpointType ?? 'speaker'; @@ -110,6 +134,12 @@ export class CallManager { * Stops the in call manager. */ stop = (): void => { + if (shouldBypassForCallKit()) { + videoLoggerSystem + .getLogger('CallManager') + .debug('stop: skipping stop as callkit is handling the audio session'); + return; + } NativeManager.stop(); }; @@ -118,4 +148,10 @@ export class CallManager { * in the native layer. */ logAudioState = (): void => NativeManager.logAudioState(); + + /** + * For debugging purposes, returns the current audio state as a string. + * @returns A string containing the current audio state information. + */ + getAudioStateLog = (): string => NativeManager.getAudioStateLog(); } diff --git a/packages/react-native-sdk/src/modules/call-manager/native-module.d.ts b/packages/react-native-sdk/src/modules/call-manager/native-module.d.ts index 746eb501d0..7661ec2374 100644 --- a/packages/react-native-sdk/src/modules/call-manager/native-module.d.ts +++ b/packages/react-native-sdk/src/modules/call-manager/native-module.d.ts @@ -82,6 +82,13 @@ export interface CallManager extends NativeModule { * Meant for debugging purposes. */ logAudioState: () => void; + + /** + * Get the current audio state as a string. + * Meant for debugging purposes. + * @returns A string containing the current audio state information. + */ + getAudioStateLog: () => string; } declare module 'react-native' { diff --git a/packages/react-native-sdk/src/providers/StreamCall/index.tsx b/packages/react-native-sdk/src/providers/StreamCall/index.tsx index 6798f0a2d8..4db6061375 100644 --- a/packages/react-native-sdk/src/providers/StreamCall/index.tsx +++ b/packages/react-native-sdk/src/providers/StreamCall/index.tsx @@ -1,13 +1,13 @@ import { StreamCallProvider } from '@stream-io/video-react-bindings'; import React, { type PropsWithChildren, useEffect } from 'react'; import { Call } from '@stream-io/video-client'; -import { useIosCallkeepWithCallingStateEffect } from '../../hooks/push/useIosCallkeepWithCallingStateEffect'; import { canAddPushWSSubscriptionsRef } from '../../utils/push/internal/utils'; import { useAndroidKeepCallAliveEffect } from '../../hooks/useAndroidKeepCallAliveEffect'; import { useScreenShareAudioMixing } from '../../hooks/useScreenShareAudioMixing'; import { AppStateListener } from './AppStateListener'; import { DeviceStats } from './DeviceStats'; import { pushUnsubscriptionCallbacks } from '../../utils/push/internal/constants'; +import { useCallingExpWithCallingStateEffect } from '../../hooks/push/useCallingExpWithCallingStateEffect'; // const PIP_CHANGE_EVENT = 'StreamVideoReactNative_PIP_CHANGE_EVENT'; @@ -35,7 +35,7 @@ export const StreamCall = ({ - + @@ -54,11 +54,11 @@ const AndroidKeepCallAlive = () => { }; /** - * This is a renderless component to end the call in callkeep for ios. - * useAndroidKeepCallAliveEffect needs to called inside a child of StreamCallProvider. + * This is a renderless component to sync state between stream call and CallKit/Telecom. + * useCallingExpWithCallingStateEffect needs to called inside a child of StreamCallProvider. */ -const IosInformCallkeepCallEnd = () => { - useIosCallkeepWithCallingStateEffect(); +const CallingExpWithCallingState = () => { + useCallingExpWithCallingStateEffect(); return null; }; diff --git a/packages/react-native-sdk/src/utils/StreamVideoRN/index.ts b/packages/react-native-sdk/src/utils/StreamVideoRN/index.ts index ca45cf4c50..f2a7e5f6ee 100644 --- a/packages/react-native-sdk/src/utils/StreamVideoRN/index.ts +++ b/packages/react-native-sdk/src/utils/StreamVideoRN/index.ts @@ -3,9 +3,14 @@ import pushLogoutCallbacks from '../internal/pushLogoutCallback'; import newNotificationCallbacks, { type NewCallNotificationCallback, } from '../internal/newNotificationCallbacks'; -import { setupIosCallKeepEvents } from '../push/setupIosCallKeepEvents'; import { setupIosVoipPushEvents } from '../push/setupIosVoipPushEvents'; +import { setupCallingExpEvents } from '../push/setupCallingExpEvents'; +import { + extractCallingExpOptions, + getCallingxLib, +} from '../push/libs/callingx'; import { NativeModules, Platform } from 'react-native'; +import { videoLoggerSystem } from '@stream-io/video-client'; // Utility type for deep partial type DeepPartial = { @@ -47,10 +52,7 @@ const DEFAULT_STREAM_VIDEO_CONFIG: StreamVideoConfig = { android: { channel: { id: 'stream_call_foreground_service', - name: 'To keep calls alive', - lights: false, - vibration: false, - importance: 3, + name: 'Ongoing calls', }, notificationTexts: { title: 'Call in progress', @@ -76,20 +78,6 @@ export class StreamVideoRN { this.config = deepMerge(this.config, updateConfig); } - static updateAndroidIncomingCallChannel( - updateChannel: Partial< - NonNullable['android']['incomingCallChannel'] - >, - ) { - const prevChannel = this.config.push?.android?.incomingCallChannel; - if (prevChannel) { - this.config.push!.android.incomingCallChannel = { - ...prevChannel, - ...updateChannel, - }; - } - } - /** * Set the push config for StreamVideoRN. * This method must be called **outside** of your application lifecycle, e.g. alongside your @@ -102,7 +90,28 @@ export class StreamVideoRN { * import App from './App'; * // Set push config * const pushConfig = {}; // construct your config - * StreamVideoRN.setPushConfig(pushConfig); + * // Set CallKit/Android Telecom API integration options. All params are optional. If not provided, the default values will be used. + * const callingExpOptions = { + * ios: { + * callsHistory: true, + * displayCallTimeout: 60000, + * sound: 'ringtone', + * imageName: 'callkit_icon', + * }, + * android: { + * incomingChannel: { + * id: 'stream_incoming_call_notifications', + * name: 'Call notifications', + * vibration: true, + * sound: 'default', + * }, + * titleTransformer: (memberName: string, incoming: boolean) => + * incoming + * ? `${memberName} is calling you` + * : `You are calling ${memberName}`, + * }, + * }; + * StreamVideoRN.setPushConfig(pushConfig, callingExpOptions); * AppRegistry.registerComponent('app', () => App); */ static setPushConfig(pushConfig: NonNullable) { @@ -110,20 +119,23 @@ export class StreamVideoRN { // Ignoring this config as push config was already set return; } - if ( - __DEV__ && - (pushConfig.navigateAcceptCall || pushConfig.navigateToIncomingCall) - ) { + + this.config.push = pushConfig; + + try { + const callingx = getCallingxLib(); + videoLoggerSystem + .getLogger('StreamVideoRN.setPushConfig') + .info(JSON.stringify(this.config)); + const options = extractCallingExpOptions(this.config); + callingx.setup(options); + } catch { throw new Error( - `Support for navigateAcceptCall or navigateToIncomingCall in pushConfig has been removed. - Please watch for incoming and outgoing calls in the root component of your app. - Please see https://getstream.io/video/docs/react-native/advanced/ringing-calls/#watch-for-incoming-and-outgoing-calls for more information.`, + 'react-native-callingx library is not installed. Please check our migration instructions: https://getstream.io/video/docs/react-native/migration-guides/1.32.0/.', ); } - this.config.push = pushConfig; - - setupIosCallKeepEvents(pushConfig); + setupCallingExpEvents(pushConfig); setupIosVoipPushEvents(pushConfig); } diff --git a/packages/react-native-sdk/src/utils/StreamVideoRN/types.ts b/packages/react-native-sdk/src/utils/StreamVideoRN/types.ts index 5c19e6012e..0b56a0ae8c 100644 --- a/packages/react-native-sdk/src/utils/StreamVideoRN/types.ts +++ b/packages/react-native-sdk/src/utils/StreamVideoRN/types.ts @@ -5,6 +5,18 @@ import { } from '@stream-io/video-client'; import type { AndroidChannel } from '@notifee/react-native'; +export type AndroidChannelConfig = { + id: string; + name: string; + sound?: string; + vibration?: boolean; +}; + +export type KeepAliveAndroidNotificationTexts = { + title: string; + body: string; +}; + export type NonRingingPushEvent = | 'call.live_started' | 'call.notification' @@ -32,6 +44,27 @@ export type StreamVideoConfig = { * @example "production-apn-video" or "staging-apn-video" based on the environment */ pushProviderName?: string; + supportsVideo?: boolean; + /** + * Sound to play when an incoming call is received. Must be a valid sound resource name in the project. + * @default '' (no sound) + */ + sound?: string; + /** + * Image to display when an incoming call is received. Must be a valid image resource name in the project. + * @default '' (no image) + */ + imageName?: string; + /** + * Enable calls history. When enabled, the call will be added to the calls history. + * @default false + */ + callsHistory?: boolean; + /** + * Timeout to display an incoming call. When the call is displayed for more than the timeout, the call will be rejected. + * @default 60000 (1 minute) + */ + displayCallTimeout?: number; }; android: { /** @@ -61,28 +94,30 @@ export type StreamVideoConfig = { * The notification channel to be used for incoming calls for Android. * @example * { - * id: 'stream_incoming_call', - * name: 'Incoming call notifications', - * importance: AndroidImportance.HIGH, + * id: 'incoming_calls_channel', + * name: 'Incoming calls', + * sound?: string; + * vibration?: boolean; * } */ - incomingCallChannel?: AndroidChannel; + incomingChannel?: AndroidChannelConfig; /** - * Functions to create the texts shown in the notification for incoming calls in Android. + * Texts used for call state notifications while the system is connecting or declining the call. + * If not provided, platform defaults will be used. * @example * { - * getTitle: (createdUserName: string) => `Incoming call from ${createdUserName}`, - * getBody: (createdUserName: string) => `Tap to answer the call` - * getAcceptButtonTitle?: () => `Accept`, - * getDeclineButtonTitle?: () => `Decline`, + * accepting: 'Connecting...', + * rejecting: 'Declining...', * } */ - incomingCallNotificationTextGetters?: { - getTitle: (createdUserName: string) => string; - getBody: (createdUserName: string) => string; - getAcceptButtonTitle?: () => string; - getDeclineButtonTitle?: () => string; + notificationTexts?: { + accepting?: string; + rejecting?: string; }; + /** + * The transformer to be used to transform the call title in the notification for ringing and ongoing calls for Android. + */ + titleTransformer?: (memberName: string, incoming: boolean) => string; /** * Functions to create the texts shown in the notification for non ringing calls in Android. * @example @@ -111,6 +146,16 @@ export type StreamVideoConfig = { getBody: (type: NonRingingPushEvent, createdUserName: string) => string; }; }; + /** + * Whether to enable ongoing calls. + * @default false + */ + enableOngoingCalls?: boolean; + /** + * Whether to reject calls when the user is busy. + * @default false + */ + shouldRejectCallWhenBusy?: boolean; /** * This function is used to create a custom video client. * This is used create a video client for incoming calls in the background and inform call events to the server. @@ -130,12 +175,6 @@ export type StreamVideoConfig = { * } */ createStreamVideoClient: () => Promise; - /** @deprecated This method will be removed in the future. Please watch for incoming and outgoing calls in the root component of your app. - Please see https://getstream.io/video/docs/react-native/advanced/ringing-calls/#watch-for-incoming-and-outgoing-calls for more information */ - navigateAcceptCall?: () => void; - /** @deprecated This method will be removed in the future. Please watch for incoming and outgoing calls in the root component of your app. - Please see https://getstream.io/video/docs/react-native/advanced/ringing-calls/#watch-for-incoming-and-outgoing-calls for more information */ - navigateToIncomingCall?: () => void; /** Callback that is called when a non ringing push notification was tapped */ onTapNonRingingCallNotification?: ( call_cid: string, @@ -147,14 +186,11 @@ export type StreamVideoConfig = { /** * The notification channel to keep call alive in the background for Android using a foreground service. */ - channel: AndroidChannel; + channel: Omit; /** * The texts shown in the notification to keep call alive in the background */ - notificationTexts: { - title: string; - body: string; - }; + notificationTexts: KeepAliveAndroidNotificationTexts; /** * The task to run in the foreground service * The task must resolve a promise once complete diff --git a/packages/react-native-sdk/src/utils/internal/callingx/audioSessionPromise.ts b/packages/react-native-sdk/src/utils/internal/callingx/audioSessionPromise.ts new file mode 100644 index 0000000000..e9a666890c --- /dev/null +++ b/packages/react-native-sdk/src/utils/internal/callingx/audioSessionPromise.ts @@ -0,0 +1,65 @@ +/** + * Module to manage pending promise for audio session activation. + * Used to wait for iOS CallKit's didActivateAudioSession event after starting a call. + */ + +import { videoLoggerSystem } from '@stream-io/video-client'; + +const logger = videoLoggerSystem.getLogger('callingx'); + +let pendingResolve: (() => void) | null = null; +let pendingTimeout: ReturnType | null = null; +let resolveSetTime: number | null = null; +/** + * Flag to check if the audio session is already activated. + * This solves race condition for a cold start case, when the audio session is activated before the promise is created. + */ +let isAudioSessionAlreadyActivated = false; + +const AUDIO_SESSION_TIMEOUT_MS = 5000; + +/** + * Creates a promise that resolves when the audio session is activated, + * or after a timeout to prevent hanging indefinitely. + * @returns Promise that resolves when audio session is activated or timeout occurs + */ +export function waitForAudioSessionActivation(): Promise { + if (isAudioSessionAlreadyActivated) { + isAudioSessionAlreadyActivated = false; + return Promise.resolve(); + } + + resolveSetTime = Date.now(); + return new Promise((resolve) => { + pendingResolve = resolve; + pendingTimeout = setTimeout(() => { + // Resolve on timeout to prevent hanging + logger.debug('audioSessionPromise timed out'); + resolvePendingAudioSession(); + }, AUDIO_SESSION_TIMEOUT_MS); + }); +} + +/** + * Resolves the pending audio session activation promise. + * Called when the didActivateAudioSession event fires or on timeout. + */ +export function resolvePendingAudioSession(): void { + if (pendingTimeout) { + clearTimeout(pendingTimeout); + pendingTimeout = null; + } + + if (pendingResolve) { + pendingResolve(); + if (resolveSetTime) { + const elapsedTime = Date.now() - resolveSetTime; + resolveSetTime = null; + logger.debug(`audioSessionPromise resolved in ${elapsedTime}ms`); + } + pendingResolve = null; + isAudioSessionAlreadyActivated = false; + } else { + isAudioSessionAlreadyActivated = true; + } +} diff --git a/packages/react-native-sdk/src/utils/internal/callingx/callingx.ts b/packages/react-native-sdk/src/utils/internal/callingx/callingx.ts new file mode 100644 index 0000000000..37b812d0c8 --- /dev/null +++ b/packages/react-native-sdk/src/utils/internal/callingx/callingx.ts @@ -0,0 +1,197 @@ +/*** + * Internal utils for callingx library usage from video-client. + * See @./registerSDKGlobals.ts for more usage details. + */ +import { Platform } from 'react-native'; +import type { EndCallReason } from '@stream-io/react-native-callingx'; +import { getCallingxLibIfAvailable } from '../../push/libs/callingx'; +import { waitForAudioSessionActivation } from './audioSessionPromise'; +import type { + Call, + MemberResponse, + StreamVideoParticipant, +} from '@stream-io/video-client'; +import { CallingState, videoLoggerSystem } from '@stream-io/video-client'; +const CallingxModule = getCallingxLibIfAvailable(); + +/** + * Gets the call display name. To be used for display in native call screen. + */ +export function getCallDisplayName( + callMembers: MemberResponse[] | undefined, + participants: StreamVideoParticipant[] | undefined, + currentUserId: string | undefined, +): string { + if (!callMembers || !participants || !currentUserId) { + return 'Call'; + } + + let names: string[] = []; + + if (callMembers.length > 0) { + // for ringing calls, members array contains all call members from the very early state and participants array is empty in the beginning + names = callMembers + .filter((member) => member.user.id !== currentUserId) + .map((member) => member.user.name) + .filter((name): name is string => name !== undefined); + } else if (participants.length > 0) { + // for non-ringing calls, members array is empty and we rely on participants array there + names = participants + .filter((participant) => participant.userId !== currentUserId) + .map((participant) => participant.name) + .filter(Boolean); + } + + // if no names are found, we use the name of the current user + if (names.length === 0) { + names = [ + participants.find((participant) => participant.userId === currentUserId) + ?.name ?? 'Call', + ]; + } + + return names.sort().join(', '); +} + +function getCallDisplayNameFromCall(call: Call): string { + return ( + call.state.custom?.display_name ?? + getCallDisplayName( + call.state.members, + call.state.participants, + call.currentUserId, + ) + ); +} + +export async function registerOutgoingCall(call: Call) { + if (!CallingxModule || !CallingxModule.isSetup) { + return; + } + + const isOutcomingCall = call.ringing && call.isCreatedByMe; + if (!isOutcomingCall) { + return; + } + + const logger = videoLoggerSystem.getLogger('callingx'); + + try { + logger.debug(`registerOutgoingCall: Registering outgoing call ${call.cid}`); + await CallingxModule.startCall( + call.cid, // unique id for call + call.state.createdBy?.id ?? getCallDisplayNameFromCall(call), // handle for native call UI (prefer createdBy user id, fallback to call display name) + getCallDisplayNameFromCall(call), // display name for display in call screen + call.state.settings?.video?.enabled ?? false, // is video call? + ); + } catch (error) { + logger.error( + `registerOutgoingCall: Error registering outgoing call in callingx: ${call.cid}`, + error, + ); + } +} + +/** + * Starts the call in the callingx library. + * It is done by client on every join + * Does either of the following: + * 1. Sets the state for outgoing calls in the callingx library + * 2. Displays the incoming call in the callingx library + * 3. Optionally for non-ringing calls also when ongoing calls are enabled. + */ +export async function joinCallingxCall(call: Call, activeCalls: Call[]) { + if (!CallingxModule || !CallingxModule.isSetup) { + return; + } + + const logger = videoLoggerSystem.getLogger('callingx'); + const isOutcomingCall = call.ringing && call.isCreatedByMe; + const isIncomingCall = call.ringing && !call.isCreatedByMe; + + if ( + isOutcomingCall || + (!call.ringing && CallingxModule.isOngoingCallsEnabled) + ) { + try { + logger.debug(`joinCallingxCall: Joining call ${call.cid}`); + await CallingxModule.startCall( + call.cid, // unique id for call + call.state.createdBy?.id ?? getCallDisplayNameFromCall(call), // handle for native call UI (prefer createdBy user id, fallback to call display name) + getCallDisplayNameFromCall(call), // display name for display in call screen + call.state.settings?.video?.enabled ?? false, // is video call? + ); + + // Wait for audio session activation on iOS only + if (Platform.OS === 'ios') { + await waitForAudioSessionActivation(); + } + } catch (error) { + logger.error( + `startCallingxCall: Error starting call in callingx: ${call.cid}`, + error, + ); + } + } else if (isIncomingCall) { + logger.debug(`joinCallingxCall: Joining incoming call ${call.cid}`); + try { + // Leave any existing active ringing calls before joining a new ringing call + const activeCallsToLeave = activeCalls.filter( + (c) => + c.cid !== call.cid && + c.ringing && + c.state.callingState !== CallingState.LEFT, + ); + for (const activeCall of activeCallsToLeave) { + logger.debug( + `leaving active call ${activeCall.cid} before joining ${call.cid}`, + ); + await activeCall.leave({ reason: 'cancel' }).catch((e) => { + logger.error(`failed to leave active call ${activeCall.cid}`, e); + }); + } + + // Awaits native CallKit/Telecom registration before answering. + // Safe to call even if the call is already registered (e.g. from VoIP push) -- + // iOS early-returns with no error, Android sends the registered broadcast. + await CallingxModule.displayIncomingCall( + call.cid, // unique id for call + call.state.createdBy?.id ?? getCallDisplayNameFromCall(call), // handle for native call UI (prefer createdBy user id, fallback to call display name) + getCallDisplayNameFromCall(call), // display name for display in call screen + call.state.settings?.video?.enabled ?? false, // is video call? + ); + + await CallingxModule.answerIncomingCall(call.cid); + + if (Platform.OS === 'ios') { + await waitForAudioSessionActivation(); + } + } catch (error) { + logger.error( + `Error displaying incoming call in callingx: ${call.cid}`, + error, + ); + } + } +} + +export async function endCallingxCall(call: Call, reason?: EndCallReason) { + if ( + !CallingxModule || + !CallingxModule.isSetup || + !CallingxModule.isCallTracked(call.cid) + ) { + return; + } + + const logger = videoLoggerSystem.getLogger('callingx'); + try { + logger.debug(`endCallingxCall: Ending call ${call.cid}`); + await CallingxModule.endCallWithReason(call.cid, reason ?? 'local'); + } catch (error) { + logger.error( + `endCallingxCall: Error ending call in callingx: ${call.cid}`, + error, + ); + } +} diff --git a/packages/react-native-sdk/src/utils/internal/registerSDKGlobals.ts b/packages/react-native-sdk/src/utils/internal/registerSDKGlobals.ts index 444caf5f4e..5a6e5bfdb3 100644 --- a/packages/react-native-sdk/src/utils/internal/registerSDKGlobals.ts +++ b/packages/react-native-sdk/src/utils/internal/registerSDKGlobals.ts @@ -1,23 +1,71 @@ -import type { StreamRNVideoSDKGlobals } from '@stream-io/video-client'; +import { StreamRNVideoSDKGlobals } from '@stream-io/video-client'; import { NativeModules, PermissionsAndroid, Platform } from 'react-native'; +import { getCallingxLibIfAvailable } from '../push/libs/callingx'; +import { + endCallingxCall, + registerOutgoingCall, + joinCallingxCall, +} from './callingx/callingx'; const StreamInCallManagerNativeModule = NativeModules.StreamInCallManager; const StreamVideoReactNativeModule = NativeModules.StreamVideoReactNative as { checkPermission: StreamRNVideoSDKGlobals['permissions']['check'] | undefined; }; +const CallingxModule = getCallingxLibIfAvailable(); + +/** + * Checks if StreamInCallManager should be bypassed because CallKit is handling + * the audio session via CallingX. + * + * On iOS, when CallingX is set up and has a registered call, the audio session + * is managed by CallKit through CallingxImpl.swift. + * In this case, StreamInCallManager should not run to avoid conflicting audio + * session configurations. + */ +const shouldBypassForCallKit = ({ + isRingingTypeCall, +}: { + isRingingTypeCall: boolean; +}): boolean => { + if (Platform.OS !== 'ios') { + return false; + } + if (!CallingxModule) { + return false; + } + const bypass = + CallingxModule.isSetup && + (isRingingTypeCall || CallingxModule.isOngoingCallsEnabled); + return bypass; +}; + const streamRNVideoSDKGlobals: StreamRNVideoSDKGlobals = { + callingX: { + joinCall: joinCallingxCall, + endCall: endCallingxCall, + registerOutgoingCall: registerOutgoingCall, + }, callManager: { - setup: ({ defaultDevice }) => { + setup: ({ defaultDevice, isRingingTypeCall }) => { + if (shouldBypassForCallKit({ isRingingTypeCall })) { + return; + } StreamInCallManagerNativeModule.setDefaultAudioDeviceEndpointType( defaultDevice, ); StreamInCallManagerNativeModule.setup(); }, - start: () => { + start: ({ isRingingTypeCall }) => { + if (shouldBypassForCallKit({ isRingingTypeCall })) { + return; + } StreamInCallManagerNativeModule.start(); }, - stop: () => { + stop: ({ isRingingTypeCall }) => { + if (shouldBypassForCallKit({ isRingingTypeCall })) { + return; + } StreamInCallManagerNativeModule.stop(); }, }, diff --git a/packages/react-native-sdk/src/utils/keepCallAliveHeadlessTask.ts b/packages/react-native-sdk/src/utils/keepCallAliveHeadlessTask.ts new file mode 100644 index 0000000000..2228b52174 --- /dev/null +++ b/packages/react-native-sdk/src/utils/keepCallAliveHeadlessTask.ts @@ -0,0 +1,54 @@ +import { AppRegistry, Platform } from 'react-native'; +import type { Call } from '@stream-io/video-client'; +import { videoLoggerSystem } from '@stream-io/video-client'; +import { StreamVideoRN } from './StreamVideoRN'; + +export const KEEP_CALL_ALIVE_HEADLESS_TASK_NAME = 'StreamVideoKeepCallAlive'; + +/** + * The keep-alive headless task needs access to the active `Call` instance. + * The keep-alive hook will set this reference before starting the native service. + */ +export const keepCallAliveCallRef: { current: Call | undefined } = { + current: undefined, +}; + +function registerKeepCallAliveHeadlessTaskOnce() { + if (Platform.OS !== 'android') return; + + AppRegistry.registerHeadlessTask( + KEEP_CALL_ALIVE_HEADLESS_TASK_NAME, + () => async (data: { callCid?: string } | undefined) => { + const logger = videoLoggerSystem.getLogger( + 'KEEP_CALL_ALIVE_HEADLESS_TASK', + ); + const callCid = data?.callCid; + + const call = keepCallAliveCallRef.current; + if (!call) { + logger.warn( + 'No active call instance available for keep-alive task; skipping.', + { callCid }, + ); + return; + } + if (callCid && call.cid && call.cid !== callCid) { + logger.warn( + 'Keep-alive task callCid does not match active call; skipping.', + { callCid, activeCallCid: call.cid }, + ); + return; + } + + const config = StreamVideoRN.getConfig(); + const taskToRun = config.foregroundService.android.taskToRun; + try { + await taskToRun(call); + } catch (e) { + logger.error('Keep-alive headless task failed', e); + } + }, + ); +} + +registerKeepCallAliveHeadlessTaskOnce(); diff --git a/packages/react-native-sdk/src/utils/push/android.ts b/packages/react-native-sdk/src/utils/push/android.ts index 0937fd3a26..8f543aaf18 100644 --- a/packages/react-native-sdk/src/utils/push/android.ts +++ b/packages/react-native-sdk/src/utils/push/android.ts @@ -1,5 +1,4 @@ import { - Call, CallingState, StreamVideoClient, videoLoggerSystem, @@ -15,30 +14,15 @@ import { getExpoNotificationsLibNoThrow, getFirebaseMessagingLib, getFirebaseMessagingLibNoThrow, - getIncomingCallForegroundServiceTypes, getNotifeeLibThrowIfNotInstalledForPush, type NotifeeLib, } from './libs'; -import { - pushAcceptedIncomingCallCId$, - pushAndroidBackgroundDeliveredIncomingCallCId$, - pushNonRingingCallData$, - pushRejectedIncomingCallCId$, - pushTappedIncomingCallCId$, -} from './internal/rxSubjects'; +import { pushNonRingingCallData$ } from './internal/rxSubjects'; import { pushUnsubscriptionCallbacks } from './internal/constants'; -import { - canAddPushWSSubscriptionsRef, - clearPushWSEventSubscriptions, - processCallFromPushInBackground, - shouldCallBeEnded, -} from './internal/utils'; +import { canListenToWS, shouldCallBeClosed } from './internal/utils'; import { setPushLogoutCallback } from '../internal/pushLogoutCallback'; -import { getAndroidDefaultRingtoneUrl } from '../getAndroidDefaultRingtoneUrl'; import { StreamVideoRN } from '../StreamVideoRN'; - -const ACCEPT_CALL_ACTION_ID = 'accept'; -const DECLINE_CALL_ACTION_ID = 'decline'; +import { getCallingxLib } from './libs/callingx'; type PushConfig = NonNullable; @@ -107,11 +91,10 @@ export async function initAndroidPushToken( await setDeviceToken(token); } } - // TODO: remove the incomingCallChannel check and find a better way once we have telecom integration for android - const messaging = - pushConfig.isExpo && !pushConfig.android.incomingCallChannel - ? getFirebaseMessagingLibNoThrow(true) - : getFirebaseMessagingLib(); + + const messaging = pushConfig.isExpo + ? getFirebaseMessagingLibNoThrow(true) + : getFirebaseMessagingLib(); if (messaging) { logger.debug(`setting firebase token listeners`); const unsubscribe = messaging().onTokenRefresh((refreshedToken) => @@ -127,10 +110,10 @@ export async function initAndroidPushToken( * Creates notification from the push message data. * For Ringing and Non-Ringing calls. */ + export const firebaseDataHandler = async ( data: FirebaseMessagingTypes.RemoteMessage['data'], ) => { - if (Platform.OS !== 'android') return; /* Example data from firebase "message": { "data": { @@ -146,227 +129,217 @@ export const firebaseDataHandler = async ( // other stuff } */ + if (Platform.OS !== 'android') return; + + const logger = videoLoggerSystem.getLogger('firebaseDataHandler'); const pushConfig = StreamVideoRN.getConfig().push; if (!pushConfig || !data || data.sender !== 'stream.video') { return; } - const notifeeLib = getNotifeeLibThrowIfNotInstalledForPush(); - const notifee = notifeeLib.default; - const settings = await notifee.getNotificationSettings(); - if (settings.authorizationStatus !== 1) { - const logger = videoLoggerSystem.getLogger('firebaseDataHandler'); - logger.debug( - `Notification permission not granted, unable to post ${data.type} notifications`, - ); - return; - } if (data.type === 'call.ring') { const call_cid = data.call_cid as string; - const created_by_id = data.created_by_id as string; - const receiver_id = data.receiver_id as string; - - const video_client = await pushConfig.createStreamVideoClient(); - await video_client?.onRingingCall(call_cid); + const callingx = getCallingxLib(); - const shouldCallBeClosed = (callToCheck: Call) => { - const { mustEndCall } = shouldCallBeEnded( - callToCheck, - created_by_id, - receiver_id, + const client = await pushConfig.createStreamVideoClient(); + if (!client) { + logger.debug( + `video client not found, skipping the call.ring notification`, ); - return mustEndCall; - }; + await callingx.stopService(); + return; + } - const canListenToWS = () => - canAddPushWSSubscriptionsRef.current && - AppState.currentState !== 'active'; const asForegroundService = canListenToWS(); if (asForegroundService) { // Listen to call events from WS through fg service // note: this will replace the current empty fg service runner - notifee.registerForegroundService(() => { - return new Promise(async () => { - const client = await pushConfig.createStreamVideoClient(); - if (!client) { - videoLoggerSystem - .getLogger('firebaseMessagingOnMessageHandler') - .debug( - `Closing fg service as there is no client to create from push config`, - ); - notifee.stopForegroundService(); - return; - } - const callFromPush = await client.onRingingCall(call_cid); - let _shouldCallBeClosed = shouldCallBeClosed(callFromPush); - if (_shouldCallBeClosed) { - videoLoggerSystem - .getLogger('firebaseMessagingOnMessageHandler') - .debug( - `Closing fg service callCid: ${call_cid} shouldCallBeClosed: ${_shouldCallBeClosed}`, + //we need to start service (e.g. by calling display incoming call) and than launch bg task, consider making those steps independent + await callingx.startBackgroundTask((_: unknown, stopTask: () => void) => { + return new Promise((resolve) => { + const finishBackgroundTask = () => { + callingx.log( + `Finishing background task for callCid: ${call_cid}`, + 'debug', + ); + resolve(undefined); + stopTask(); + }; + + (async () => { + try { + const _client = await pushConfig.createStreamVideoClient(); + if (!_client) { + logger.debug( + `Closing fg service as there is no client to create from push config`, + ); + finishBackgroundTask(); + return; + } + + const callFromPush = await _client.onRingingCall(call_cid); + const { mustEndCall, endCallReason } = shouldCallBeClosed( + callFromPush, + data, ); - notifee.stopForegroundService(); - return; - } - const unsubscribeFunctions: Array<() => void> = []; - // check if service needs to be closed if accept/decline event was done on another device - const unsubscribe = callFromPush.on('all', (event) => { - const _canListenToWS = canListenToWS(); - if (!_canListenToWS) { - videoLoggerSystem - .getLogger('firebaseMessagingOnMessageHandler') - .debug( - `Closing fg service from event callCid: ${call_cid} canListenToWS: ${_canListenToWS}`, - { event }, + if (mustEndCall) { + logger.debug( + `Closing fg service callCid: ${call_cid} endCallReason: ${endCallReason}`, ); - unsubscribeFunctions.forEach((fn) => fn()); - notifee.stopForegroundService(); - return; - } - _shouldCallBeClosed = shouldCallBeClosed(callFromPush); - if (_shouldCallBeClosed) { - videoLoggerSystem - .getLogger('firebaseMessagingOnMessageHandler') - .debug( - `Closing fg service from event callCid: ${call_cid} canListenToWS: ${_canListenToWS} shouldCallBeClosed: ${_shouldCallBeClosed}`, - { event }, + + callingx.log( + `Ending call with callCid: ${call_cid} endCallReason: ${endCallReason}`, + 'debug', ); - unsubscribeFunctions.forEach((fn) => fn()); - notifee.stopForegroundService(); - } - }); - // check if service needs to be closed if call was left - const subscription = callFromPush.state.callingState$.subscribe( - (callingState) => { - if ( - callingState === CallingState.IDLE || - callingState === CallingState.LEFT - ) { - videoLoggerSystem - .getLogger('firebaseMessagingOnMessageHandler') - .debug( - `Closing fg service from callingState callCid: ${call_cid} callingState: ${callingState}`, - ); - unsubscribeFunctions.forEach((fn) => fn()); - notifee.stopForegroundService(); + callingx.endCallWithReason(call_cid, endCallReason); + resolve(undefined); + return; } - }, - ); - unsubscribeFunctions.push(unsubscribe); - unsubscribeFunctions.push(() => subscription.unsubscribe()); - pushUnsubscriptionCallbacks.get(call_cid)?.forEach((cb) => cb()); - pushUnsubscriptionCallbacks.set(call_cid, unsubscribeFunctions); - }); - }); - } - const incomingCallChannel = pushConfig.android.incomingCallChannel; - const incomingCallNotificationTextGetters = - pushConfig.android.incomingCallNotificationTextGetters; - if (!incomingCallChannel || !incomingCallNotificationTextGetters) { - const logger = videoLoggerSystem.getLogger( - 'firebaseMessagingOnMessageHandler', - ); - logger.error( - "Can't show incoming call notification as either or both incomingCallChannel and incomingCallNotificationTextGetters were not provided", - ); - return; - } - /* - * Sound has to be set on channel level for android 8 and above and cant be updated later after creation! - * For android 7 and below, sound should be set on notification level - */ - // set default ringtone if not provided - if (!incomingCallChannel.sound) { - incomingCallChannel.sound = await getAndroidDefaultRingtoneUrl(); - } - await notifee.createChannel(incomingCallChannel); - const { getTitle, getBody, getAcceptButtonTitle, getDeclineButtonTitle } = - incomingCallNotificationTextGetters; - const createdUserName = data.created_by_display_name as string; - const title = getTitle(createdUserName); - const body = getBody(createdUserName); + const unsubscribeFunctions: Array<() => void> = []; + // check if service needs to be closed if accept/decline event was done on another device + const unsubscribe = callFromPush.on('all', (event) => { + const _canListenToWS = canListenToWS(); + if (!_canListenToWS) { + logger.debug( + `Closing fg service from event callCid: ${call_cid} canListenToWS: ${_canListenToWS}`, + { event }, + ); + unsubscribeFunctions.forEach((fn) => fn()); - videoLoggerSystem - .getLogger('firebaseMessagingOnMessageHandler') - .debug( - `Displaying incoming call notification with callCid: ${call_cid} title: ${title} body: ${body} asForegroundService: ${asForegroundService}`, - ); + finishBackgroundTask(); + return; + } - const channelId = incomingCallChannel.id; - await notifee.displayNotification({ - id: call_cid, - title: getTitle(createdUserName), - body: getBody(createdUserName), - data, - android: { - channelId, - smallIcon: pushConfig.android.smallIcon, - importance: 4, // high importance - foregroundServiceTypes: getIncomingCallForegroundServiceTypes(), - asForegroundService, - ongoing: true, - sound: incomingCallChannel.sound, - vibrationPattern: incomingCallChannel.vibrationPattern, - loopSound: true, - pressAction: { - id: 'default', - launchActivity: 'default', // open the app when the notification is pressed - }, - actions: [ - { - title: getDeclineButtonTitle?.() ?? 'Decline', - pressAction: { - id: DECLINE_CALL_ACTION_ID, - }, - }, - { - title: getAcceptButtonTitle?.() ?? 'Accept', - pressAction: { - id: ACCEPT_CALL_ACTION_ID, - launchActivity: 'default', // open the app when the notification is pressed - }, - }, - ], - category: notifeeLib.AndroidCategory.CALL, - fullScreenAction: { - id: 'stream_ringing_incoming_call', - }, - timeoutAfter: 60000, // 60 seconds, after which the notification will be dismissed automatically - }, - }); + const { + mustEndCall: mustEndCallFromEvent, + endCallReason: endCallReasonFromEvent, + } = shouldCallBeClosed(callFromPush, data); + if (mustEndCallFromEvent) { + logger.debug( + `Closing fg service from event callCid: ${call_cid} canListenToWS: ${_canListenToWS} shouldCallBeClosed`, + { event }, + ); + unsubscribeFunctions.forEach((fn) => fn()); + + callingx.endCallWithReason(call_cid, endCallReasonFromEvent); + resolve(undefined); + } + }); + + // check if service needs to be closed if call was left + const stateSubscription = + callFromPush.state.callingState$.subscribe((callingState) => { + if ( + callingState === CallingState.IDLE || + callingState === CallingState.LEFT + ) { + logger.debug( + `Closing fg service from callingState callCid: ${call_cid} callingState: ${callingState}`, + ); + unsubscribeFunctions.forEach((fn) => fn()); + callingx.log( + `Ending call with callCid: ${call_cid} callingState: ${callingState}`, + 'debug', + ); + resolve(undefined); + } + }); + + const endCallSubscription = callingx.addEventListener( + 'endCall', + async ({ callId }: { callId: string }) => { + unsubscribeFunctions.forEach((fn) => fn()); + try { + await callFromPush.leave({ + reject: + callFromPush.state.callingState === + CallingState.RINGING, + reason: 'decline', + }); + } catch (error) { + logger.error( + `Failed to leave call with callCid: ${call_cid} error: ${error}`, + ); + } finally { + callingx.log( + `Ending call with callCid: ${call_cid} callId: ${callId}`, + 'debug', + ); + resolve(undefined); + } + }, + ); + + //stop background task when app comes to foreground + const appStateSubscription = AppState.addEventListener( + 'change', + (nextAppState) => { + const _canListenToWS = canListenToWS(); + callingx.log( + `AppState changed to: ${nextAppState} for callCid: ${call_cid} canListenToWS: ${_canListenToWS}`, + 'debug', + ); + if (!_canListenToWS) { + unsubscribeFunctions.forEach((fn) => fn()); + finishBackgroundTask(); + return; + } + }, + ); + + unsubscribeFunctions.push(unsubscribe); + unsubscribeFunctions.push(() => stateSubscription.unsubscribe()); + unsubscribeFunctions.push(() => endCallSubscription.remove()); + unsubscribeFunctions.push(() => appStateSubscription.remove()); + pushUnsubscriptionCallbacks.get(call_cid)?.forEach((cb) => cb()); + pushUnsubscriptionCallbacks.set(call_cid, unsubscribeFunctions); + } catch (error) { + callingx.log( + `Failed to start background task with callCid: ${call_cid} error: ${error}`, + 'error', + ); + finishBackgroundTask(); + } + })(); + }); + }); + } if (asForegroundService) { // no need to check if call has be closed as that will be handled by the fg service return; } - // check if call needs to be closed if accept/decline event was done - // before the notification was shown - const client = await pushConfig.createStreamVideoClient(); - if (!client) { - return; - } const callFromPush = await client.onRingingCall(call_cid); - if (shouldCallBeClosed(callFromPush)) { - videoLoggerSystem - .getLogger('firebaseMessagingOnMessageHandler') - .debug( - `Removing incoming call notification immediately with callCid: ${call_cid} as it should be closed`, - ); - notifee.cancelDisplayedNotification(call_cid); + const { mustEndCall, endCallReason } = shouldCallBeClosed( + callFromPush, + data, + ); + if (mustEndCall) { + logger.debug( + `Removing incoming call notification immediately with callCid: ${call_cid} as it should be closed`, + ); + callingx.endCallWithReason(call_cid, endCallReason); } } else { + const notifeeLib = getNotifeeLibThrowIfNotInstalledForPush(); + const notifee = notifeeLib.default; + const settings = await notifee.getNotificationSettings(); + if (settings.authorizationStatus !== 1) { + logger.debug( + `Notification permission not granted, unable to post ${data.type} notifications`, + ); + return; + } + // the other types are call.live_started and call.notification const callChannel = pushConfig.android.callChannel; const callNotificationTextGetters = pushConfig.android.callNotificationTextGetters; if (!callChannel || !callNotificationTextGetters) { - const logger = videoLoggerSystem.getLogger( - 'firebaseMessagingOnMessageHandler', - ); logger.debug( "Can't show call notification as either or both callChannel and callNotificationTextGetters is not provided", ); @@ -382,14 +355,12 @@ export const firebaseDataHandler = async ( const title = getTitle(type, createdUserName); const body = getBody(type, createdUserName); - videoLoggerSystem - .getLogger('firebaseMessagingOnMessageHandler') - .debug( - `Displaying NonRingingPushEvent ${type} notification with title: ${title} body: ${body}`, - ); + logger.debug( + `Displaying NonRingingPushEvent ${type} notification with title: ${title} body: ${body}`, + ); await notifee.displayNotification({ - title: getTitle(type, createdUserName), - body: getBody(type, createdUserName), + title, + body, data, android: { sound: callChannel.sound, @@ -409,16 +380,10 @@ export const firebaseDataHandler = async ( } }; -export const onAndroidNotifeeEvent = async ({ - event, - isBackground, -}: { - event: Event; - isBackground: boolean; -}) => { +export const onAndroidNotifeeEvent = async ({ event }: { event: Event }) => { if (Platform.OS !== 'android') return; const { type, detail } = event; - const { notification, pressAction } = detail; + const { notification } = detail; const notificationId = notification?.id; const data = notification?.data; const pushConfig = StreamVideoRN.getConfig().push; @@ -434,92 +399,14 @@ export const onAndroidNotifeeEvent = async ({ // we can safely cast to string because the data is from "stream.video" const call_cid = data.call_cid as string; - if (data.type === 'call.ring') { - // check if we have observers for the call cid (this means the app is in the foreground state) - const hasObservers = - pushAcceptedIncomingCallCId$.observed && - pushRejectedIncomingCallCId$.observed; - - const notifeeLib = getNotifeeLibThrowIfNotInstalledForPush(); - const notifee = notifeeLib.default; - // Check if we need to decline the call - const didPressDecline = - type === notifeeLib.EventType.ACTION_PRESS && - pressAction?.id === DECLINE_CALL_ACTION_ID; - const didDismiss = type === notifeeLib.EventType.DISMISSED; - const mustDecline = didPressDecline || didDismiss; - // Check if we need to accept the call - const mustAccept = - type === notifeeLib.EventType.ACTION_PRESS && - pressAction?.id === ACCEPT_CALL_ACTION_ID; - - if ( - mustAccept || - mustDecline || - type === notifeeLib.EventType.ACTION_PRESS - ) { - videoLoggerSystem - .getLogger('onAndroidNotifeeEvent') - .debug( - `clearPushWSEventSubscriptions for callCId: ${call_cid} mustAccept: ${mustAccept} mustDecline: ${mustDecline}`, - ); - clearPushWSEventSubscriptions(call_cid); - notifee.stopForegroundService(); - } - - if (mustAccept) { - videoLoggerSystem - .getLogger('onAndroidNotifeeEvent') - .debug(`pushAcceptedIncomingCallCId$ added with callCId: ${call_cid}`); - pushAcceptedIncomingCallCId$.next(call_cid); - // NOTE: accept will be handled by the app with rxjs observers as the app will go to foreground always - } else if (mustDecline) { - videoLoggerSystem - .getLogger('onAndroidNotifeeEvent') - .debug(`pushRejectedIncomingCallCId$ added with callCId: ${call_cid}`); - pushRejectedIncomingCallCId$.next(call_cid); - if (hasObservers) { - // if we had observers we can return here as the observers will handle the call as the app is in the foreground state - videoLoggerSystem - .getLogger('onAndroidNotifeeEvent') - .debug( - `Skipped processCallFromPushInBackground for Declining call with callCId: ${call_cid} as the app is in the foreground state`, - ); - return; - } - videoLoggerSystem - .getLogger('onAndroidNotifeeEvent') - .debug( - `start processCallFromPushInBackground - Declining call with callCId: ${call_cid}`, - ); - await processCallFromPushInBackground(pushConfig, call_cid, 'decline'); - } else { - if (type === notifeeLib.EventType.PRESS) { - videoLoggerSystem - .getLogger('onAndroidNotifeeEvent') - .debug(`pushTappedIncomingCallCId$ added with callCId: ${call_cid}`); - pushTappedIncomingCallCId$.next(call_cid); - // pressed state will be handled by the app with rxjs observers as the app will go to foreground always - } else if (isBackground && type === notifeeLib.EventType.DELIVERED) { - videoLoggerSystem - .getLogger('onAndroidNotifeeEvent') - .debug( - `pushAndroidBackgroundDeliveredIncomingCallCId$ added with callCId: ${call_cid}`, - ); - pushAndroidBackgroundDeliveredIncomingCallCId$.next(call_cid); - // background delivered state will be handled by the app with rxjs observers as processing needs to happen only when app is opened - } - } - } else { - const notifeeLib = getNotifeeLibThrowIfNotInstalledForPush(); - if (type === notifeeLib.EventType.PRESS) { - videoLoggerSystem - .getLogger('onAndroidNotifeeEvent') - .debug(`onTapNonRingingCallNotification with callCId: ${call_cid}`); - pushConfig.onTapNonRingingCallNotification?.( - call_cid, - data.type as NonRingingPushEvent, - ); - } + const notifeeLib = getNotifeeLibThrowIfNotInstalledForPush(); + if (type === notifeeLib.EventType.PRESS) { + videoLoggerSystem + .getLogger('onAndroidNotifeeEvent') + .debug(`onTapNonRingingCallNotification with callCId: ${call_cid}`); + pushConfig.onTapNonRingingCallNotification?.( + call_cid, + data.type as NonRingingPushEvent, + ); } }; diff --git a/packages/react-native-sdk/src/utils/push/internal/ios.ts b/packages/react-native-sdk/src/utils/push/internal/ios.ts index 8801017b45..ba6a5b120e 100644 --- a/packages/react-native-sdk/src/utils/push/internal/ios.ts +++ b/packages/react-native-sdk/src/utils/push/internal/ios.ts @@ -1,10 +1,9 @@ -import { AppState, NativeModules, Platform } from 'react-native'; -import { getCallKeepLib, getVoipPushNotificationLib } from '../libs'; -import { voipPushNotificationCallCId$ } from './rxSubjects'; +import { Platform } from 'react-native'; import { pushUnsubscriptionCallbacks } from './constants'; -import { canAddPushWSSubscriptionsRef, shouldCallBeEnded } from './utils'; +import { canListenToWS, shouldCallBeClosed } from './utils'; import { StreamVideoConfig } from '../../StreamVideoRN/types'; import { videoLoggerSystem } from '@stream-io/video-client'; +import { getCallingxLib } from '../libs/callingx'; export const onVoipNotificationReceived = async ( notification: any, @@ -32,71 +31,57 @@ export const onVoipNotificationReceived = async ( "version": "v2" } } */ + const logger = videoLoggerSystem.getLogger( + 'callingx - onVoipNotificationReceived', + ); + const sender = notification?.stream?.sender; const type = notification?.stream?.type; // do not process any other notifications other than stream.video or ringing if (sender !== 'stream.video' && type !== 'call.ring') { return; } + const call_cid = notification?.stream?.call_cid; if (!call_cid || Platform.OS !== 'ios' || !pushConfig.ios.pushProviderName) { return; } - const logger = videoLoggerSystem.getLogger('setupIosVoipPushEvents'); - const client = await pushConfig.createStreamVideoClient(); - if (!client) { + const callingx = getCallingxLib(); + if (callingx.isCallTracked(call_cid)) { + //same call_cid is already tracked, so we skip the notification logger.debug( - 'client not found, not processing call.ring voip push notification', + `the same call_cid ${call_cid} is already tracked, skipping the call.ring notification`, ); return; } - const shouldRejectCallWhenBusy = client['rejectCallWhenBusy'] ?? false; - if (shouldRejectCallWhenBusy) { - // inform the iOS native module that we should reject call when busy - NativeModules.StreamVideoReactNative.setShouldRejectCallWhenBusy( - shouldRejectCallWhenBusy, - ); - } - const callFromPush = await client.onRingingCall(call_cid); - let uuid = ''; - try { - uuid = - await NativeModules?.StreamVideoReactNative?.getIncomingCallUUid( - call_cid, - ); - } catch (error) { - logger.error('Error in getting call uuid from native module', error); - } - if (!uuid) { - logger.error( - `Not processing call.ring push notification, as no uuid found for call_cid: ${call_cid}`, + + const client = await pushConfig.createStreamVideoClient(); + if (!client) { + logger.debug( + 'client not found, not processing call.ring voip push notification', ); return; } - const created_by_id = notification?.stream?.created_by_id; - const receiver_id = notification?.stream?.receiver_id; + + const callFromPush = await client.onRingingCall(call_cid); + function closeCallIfNecessary() { - const { mustEndCall, callkeepReason } = shouldCallBeEnded( + const { mustEndCall, endCallReason } = shouldCallBeClosed( callFromPush, - created_by_id, - receiver_id, + notification?.stream, ); if (mustEndCall) { - const callkeep = getCallKeepLib(); logger.debug( - `callkeep.reportEndCallWithUUID for uuid: ${uuid}, call_cid: ${call_cid}, reason: ${callkeepReason}`, + `callingx.endCallWithReason for call_cid: ${call_cid} endCallReason: ${endCallReason}`, ); - callkeep.reportEndCallWithUUID(uuid, callkeepReason); - const voipPushNotification = getVoipPushNotificationLib(); - voipPushNotification.onVoipNotificationCompleted(uuid); + callingx.endCallWithReason(call_cid, endCallReason); return true; } return false; } + const closed = closeCallIfNecessary(); - const canListenToWS = () => - canAddPushWSSubscriptionsRef.current && AppState.currentState !== 'active'; if (!closed && canListenToWS()) { const unsubscribe = callFromPush.on('all', (event) => { const _canListenToWS = canListenToWS(); @@ -121,10 +106,9 @@ export const onVoipNotificationReceived = async ( pushUnsubscriptionCallbacks.get(call_cid)?.forEach((cb) => cb()); pushUnsubscriptionCallbacks.set(call_cid, [unsubscribe]); } - // send the info to this subject, it is listened by callkeep events - // callkeep events will then accept/reject the call + + // callingx event listeners (setupCallingExpEvents) will handle accept/reject logger.debug( - `call_cid:${call_cid} uuid:${uuid} received and processed from call.ring push notification`, + `call_cid:${call_cid} received and processed from call.ring push notification`, ); - voipPushNotificationCallCId$.next(call_cid); }; diff --git a/packages/react-native-sdk/src/utils/push/internal/rxSubjects.ts b/packages/react-native-sdk/src/utils/push/internal/rxSubjects.ts index 5b9eab7f58..adb531fb8c 100644 --- a/packages/react-native-sdk/src/utils/push/internal/rxSubjects.ts +++ b/packages/react-native-sdk/src/utils/push/internal/rxSubjects.ts @@ -8,64 +8,3 @@ import type { NonRingingPushEvent } from '../../StreamVideoRN/types'; export const pushNonRingingCallData$ = new BehaviorSubject< { cid: string; type: NonRingingPushEvent } | undefined >(undefined); - -/** - * This rxjs subject is used to store the call cid of the accepted incoming call from push notification - * Note: it is should be subscribed only when a user has connected to the websocket of Stream - */ -export const pushAcceptedIncomingCallCId$ = new BehaviorSubject< - string | undefined ->(undefined); - -/** - * This rxjs subject is used to store the call cid of the tapped incoming call from push notification it is neither accepted nor rejected yet - * Note: it should be subscribed only when a user has connected to the websocket of Stream - */ -export const pushTappedIncomingCallCId$ = new BehaviorSubject< - string | undefined ->(undefined); - -/** - * This rxjs subject is used to store the call cid of the delivered incoming call from push notification it is neither accepted nor rejected yet - * Used so that the call is navigated to when app is open from being killed - * Note: it should be subscribed only when a user has connected to the websocket of Stream - */ -export const pushAndroidBackgroundDeliveredIncomingCallCId$ = - new BehaviorSubject(undefined); - -/** - * This rxjs subject is used to store the call cid of the accepted incoming call from push notification - * Note: it should be subscribed only when a user has connected to the websocket of Stream - */ -export const pushRejectedIncomingCallCId$ = new BehaviorSubject< - string | undefined ->(undefined); - -/** - * This rxjs subject is used to store the call cid of the incoming call from ios voip pushkit notification - */ -export const voipPushNotificationCallCId$ = new BehaviorSubject< - string | undefined ->(undefined); - -/** The pair of cid of a call and its corresponding uuid created in the native side */ -type CallkeepMap = { - uuid: string; - cid: string; -}; - -/* - * This rxjs subject should only used to store the CallkeepMap - * for the incoming call when on foreground - * or in other words, when we get didDisplayIncomingCall from callkeep lib - */ -export const voipCallkeepCallOnForegroundMap$ = new BehaviorSubject< - CallkeepMap | undefined ->(undefined); - -/* - * This rxjs subject should only used to store the CallkeepMap when it was accepted in the native dialer - */ -export const voipCallkeepAcceptedCallOnNativeDialerMap$ = new BehaviorSubject< - CallkeepMap | undefined ->(undefined); diff --git a/packages/react-native-sdk/src/utils/push/internal/utils.ts b/packages/react-native-sdk/src/utils/push/internal/utils.ts index c31ce73c78..11c7becda8 100644 --- a/packages/react-native-sdk/src/utils/push/internal/utils.ts +++ b/packages/react-native-sdk/src/utils/push/internal/utils.ts @@ -10,9 +10,12 @@ import type { } from '../../StreamVideoRN/types'; import { onNewCallNotification } from '../../internal/newNotificationCallbacks'; import { pushUnsubscriptionCallbacks } from './constants'; +import { AppState } from 'react-native'; +import type { EndCallReason } from '@stream-io/react-native-callingx'; type PushConfig = NonNullable; +const logger = videoLoggerSystem.getLogger('callingx'); type CanAddPushWSSubscriptionsRef = { current: boolean }; /** @@ -24,44 +27,40 @@ export const shouldCallBeEnded = ( created_by_id: string | undefined, receiver_id: string | undefined, ) => { - /* callkeep reasons for ending a call - FAILED: 1, - REMOTE_ENDED: 2, - UNANSWERED: 3, - ANSWERED_ELSEWHERE: 4, - DECLINED_ELSEWHERE: 5, - MISSED: 6 - */ const callSession = callFromPush.state.session; const rejected_by = callSession?.rejected_by; const accepted_by = callSession?.accepted_by; let mustEndCall = false; - let callkeepReason = 0; - if (created_by_id && rejected_by) { + let endCallReason: EndCallReason = 'unknown'; + + if (callFromPush.state.endedAt) { + mustEndCall = true; + endCallReason = 'remote'; + } else if (created_by_id && rejected_by) { if (rejected_by[created_by_id]) { - // call was cancelled by the caller + // call was cancelled by the caller before the receiver could answer mustEndCall = true; - callkeepReason = 2; + endCallReason = 'canceled'; } } else if (receiver_id && rejected_by) { if (rejected_by[receiver_id]) { // call was rejected by the receiver in some other device mustEndCall = true; - callkeepReason = 5; + endCallReason = 'rejected'; } } else if (receiver_id && accepted_by) { if (accepted_by[receiver_id]) { // call was accepted by the receiver in some other device mustEndCall = true; - callkeepReason = 4; + endCallReason = 'answeredElsewhere'; } } videoLoggerSystem .getLogger('shouldCallBeEnded') .debug( - `callCid: ${callFromPush.cid} mustEndCall: ${mustEndCall} callkeepReason: ${callkeepReason}`, + `callCid: ${callFromPush.cid} mustEndCall: ${mustEndCall} endCallReason: ${endCallReason}`, ); - return { mustEndCall, callkeepReason }; + return { mustEndCall, endCallReason }; }; /* An action for the notification or callkeep and app does not have JS context setup yet, so we need to do two steps: @@ -71,71 +70,95 @@ export const shouldCallBeEnded = ( export const processCallFromPushInBackground = async ( pushConfig: PushConfig, call_cid: string, - action: Parameters[2], + action: 'accept' | 'decline' | 'pressed' | 'backgroundDelivered', + /** + * Callback to inform iOS CallKit that the action can be fulfilled + * Needed for iOS CallKit fullfillment of action + * as per ios docs "Instead, wait until you establish a connection and then fulfill the object." + * This means we wait until call.get() is done and call.join() or call.leave() is invoked (not completed) to fulfill the action + */ + onIOSActionCanBeFulfilled: (didFail: boolean) => void, ) => { let videoClient: StreamVideoClient | undefined; try { videoClient = await pushConfig.createStreamVideoClient(); if (!videoClient) { - return; + throw new Error('createStreamVideoClient returned null'); } } catch (e) { - const logger = videoLoggerSystem.getLogger( - 'processCallFromPushInBackground', + logger.error( + 'processCallFromPushInBackground: failed to create video client', + e, ); - logger.error('failed to create video client', e); + onIOSActionCanBeFulfilled(true); return; } - await processCallFromPush(videoClient, call_cid, action, pushConfig); -}; -/** - * This function is used process the call from push notifications due to incoming call - * It does the following steps: - * 1. Get the call from the client if present or create a new call - * 2. Fetch the latest state of the call from the server if its not already in ringing state - * 3. Join or leave the call based on the user's action. - */ -export const processCallFromPush = async ( - client: StreamVideoClient, - call_cid: string, - action: 'accept' | 'decline' | 'pressed' | 'backgroundDelivered', - pushConfig: PushConfig, -) => { let callFromPush: Call; try { - callFromPush = await client.onRingingCall(call_cid); + callFromPush = await videoClient.onRingingCall(call_cid); } catch (e) { - const logger = videoLoggerSystem.getLogger('processCallFromPush'); - logger.error('failed to fetch call from push notification', e); + logger.error( + 'processCallFromPushInBackground: failed to fetch call from push notification', + e, + ); + onIOSActionCanBeFulfilled(true); return; } - // note: when action was pressed or delivered, we dont need to do anything as the only thing is to do is to get the call which adds it to the client - try { - if (action === 'accept') { - if (pushConfig.publishOptions) { - callFromPush.updatePublishOptions(pushConfig.publishOptions); - } - videoLoggerSystem - .getLogger('processCallFromPush') - .debug( - `joining call from push notification with callCid: ${callFromPush.cid}`, - ); + if (action === 'accept') { + if (pushConfig.publishOptions) { + callFromPush.updatePublishOptions(pushConfig.publishOptions); + } + logger.debug( + `joining call from push notification with callCid: ${callFromPush.cid}`, + ); + const callingState = callFromPush.state.callingState; + if ( + callingState !== CallingState.RINGING && + callingState !== CallingState.IDLE + ) { + logger.debug( + `skipping join call as it is not in ringing or idle state from push notification. callCid: ${callFromPush.cid}`, + ); + onIOSActionCanBeFulfilled(true); + return; + } + try { + onIOSActionCanBeFulfilled(false); await callFromPush.join(); - } else if (action === 'decline') { - const canReject = - callFromPush.state.callingState === CallingState.RINGING; - videoLoggerSystem - .getLogger('processCallFromPush') - .debug( - `declining call from push notification with callCid: ${callFromPush.cid} reject: ${canReject}`, - ); - await callFromPush.leave({ reject: canReject, reason: 'decline' }); + } catch (e) { + logger.warn( + 'processCallFromPushInBackground: failed to join call from push notification', + e, + ); + } + } else if (action === 'decline') { + const alreadyLeft = callFromPush.state.callingState === CallingState.LEFT; + if (alreadyLeft) { + onIOSActionCanBeFulfilled(false); + return; + } + const canReject = + callFromPush.state.callingState === CallingState.RINGING || + callFromPush.state.callingState === CallingState.IDLE; + const isCurrentUserMember = callFromPush.state.members.some( + (member) => member.user_id === callFromPush.currentUserId, + ); + const reject = canReject && isCurrentUserMember; + logger.debug( + `declining call from push notification with callCid: ${callFromPush.cid} reject: ${reject}`, + ); + try { + await callFromPush.leave({ reject, reason: 'decline' }); + onIOSActionCanBeFulfilled(false); + } catch (e) { + logger.warn( + 'processCallFromPushInBackground: failed to decline call from push notification', + e, + ); + onIOSActionCanBeFulfilled(true); } - } catch (e) { - const logger = videoLoggerSystem.getLogger('processCallFromPush'); - logger.warn(`failed to process ${action} call from push notification`, e); } }; @@ -163,10 +186,13 @@ export const processNonIncomingCallFromPush = async ( await callFromPush.get(); } } catch (e) { - const logger = videoLoggerSystem.getLogger( + const nonRingingCallLogger = videoLoggerSystem.getLogger( 'processNonIncomingCallFromPush', ); - logger.error('failed to fetch call from push notification', e); + nonRingingCallLogger.error( + 'failed to fetch call from push notification', + e, + ); return; } onNewCallNotification(callFromPush, nonRingingNotificationType); @@ -191,3 +217,21 @@ export const clearPushWSEventSubscriptions = (call_cid: string) => { export const canAddPushWSSubscriptionsRef: CanAddPushWSSubscriptionsRef = { current: true, }; + +export const canListenToWS = () => + canAddPushWSSubscriptionsRef.current && AppState.currentState !== 'active'; + +export const shouldCallBeClosed = ( + call: Call, + pushData: { [key: string]: string | object }, +) => { + const created_by_id = pushData?.created_by_id as string; + const receiver_id = pushData?.receiver_id as string; + + const { mustEndCall, endCallReason } = shouldCallBeEnded( + call, + created_by_id, + receiver_id, + ); + return { mustEndCall, endCallReason }; +}; diff --git a/packages/react-native-sdk/src/utils/push/ios.ts b/packages/react-native-sdk/src/utils/push/ios.ts index 077a6c8c58..f78d7a3941 100644 --- a/packages/react-native-sdk/src/utils/push/ios.ts +++ b/packages/react-native-sdk/src/utils/push/ios.ts @@ -62,12 +62,7 @@ export const oniOSExpoNotificationEvent = (event: ExpoNotification) => { } }; -export const oniOSNotifeeEvent = ({ - event, -}: { - event: Event; - isBackground: boolean; -}) => { +export const oniOSNotifeeEvent = ({ event }: { event: Event }) => { if (Platform.OS !== 'ios') return; const pushConfig = StreamVideoRN.getConfig().push; const { type, detail } = event; diff --git a/packages/react-native-sdk/src/utils/push/libs/callingx.ts b/packages/react-native-sdk/src/utils/push/libs/callingx.ts new file mode 100644 index 0000000000..0ab035a65c --- /dev/null +++ b/packages/react-native-sdk/src/utils/push/libs/callingx.ts @@ -0,0 +1,89 @@ +import { StreamVideoConfig } from '../../StreamVideoRN/types'; + +export type RNCallingxType = + import('@stream-io/react-native-callingx').ICallingxModule; +export type EventData = import('@stream-io/react-native-callingx').EventData; +export type EventParams = + import('@stream-io/react-native-callingx').EventParams; +export type CallingExpOptions = + import('@stream-io/react-native-callingx').CallingExpOptions; + +let callingx: RNCallingxType | undefined; + +try { + callingx = require('@stream-io/react-native-callingx').CallingxModule; +} catch {} + +export function getCallingxLib() { + if (!callingx) { + throw Error('react-native-callingx library is not installed.'); + } + return callingx; +} + +export function getCallingxLibIfAvailable() { + return callingx ?? undefined; +} + +export function extractCallingExpOptions( + config: StreamVideoConfig, +): CallingExpOptions { + const { push: pushConfig, foregroundService: foregroundServiceConfig } = + config; + const callingExpOptions: CallingExpOptions = {}; + + if (pushConfig?.ios) { + const iosOptions: CallingExpOptions['ios'] = {}; + if (pushConfig.ios.supportsVideo !== undefined) { + iosOptions.supportsVideo = pushConfig.ios.supportsVideo; + } + if (pushConfig.ios.sound !== undefined) { + iosOptions.sound = pushConfig.ios.sound; + } + if (pushConfig.ios.imageName !== undefined) { + iosOptions.imageName = pushConfig.ios.imageName; + } + if (pushConfig.ios.callsHistory !== undefined) { + iosOptions.callsHistory = pushConfig.ios.callsHistory; + } + if (pushConfig.ios.displayCallTimeout !== undefined) { + iosOptions.displayCallTimeout = pushConfig.ios.displayCallTimeout; + } + + if (Object.keys(iosOptions).length > 0) { + callingExpOptions.ios = iosOptions; + } + } + + const androidOptions: CallingExpOptions['android'] = {}; + if (pushConfig?.android) { + if (pushConfig.android.incomingChannel) { + androidOptions.incomingChannel = pushConfig.android.incomingChannel; + } + if (pushConfig.android.titleTransformer) { + androidOptions.titleTransformer = pushConfig.android.titleTransformer; + } + if (pushConfig.android.notificationTexts) { + androidOptions.notificationTexts = pushConfig.android.notificationTexts; + } + } + + if (foregroundServiceConfig.android.channel) { + androidOptions.ongoingChannel = foregroundServiceConfig.android.channel; + } + + if (Object.keys(androidOptions).length > 0) { + callingExpOptions.android = androidOptions; + } + + if (pushConfig?.shouldRejectCallWhenBusy !== undefined) { + callingExpOptions.shouldRejectCallWhenBusy = + pushConfig.shouldRejectCallWhenBusy; + } + + if (pushConfig?.enableOngoingCalls !== undefined) { + callingExpOptions.enableOngoingCalls = pushConfig?.enableOngoingCalls; + } + + return callingExpOptions; +} diff --git a/packages/react-native-sdk/src/utils/push/libs/callkeep.ts b/packages/react-native-sdk/src/utils/push/libs/callkeep.ts deleted file mode 100644 index fe8a20bf68..0000000000 --- a/packages/react-native-sdk/src/utils/push/libs/callkeep.ts +++ /dev/null @@ -1,16 +0,0 @@ -export type RNCallKeepType = typeof import('react-native-callkeep').default; - -let callkeep: RNCallKeepType | undefined; - -try { - callkeep = require('react-native-callkeep').default; -} catch {} - -export function getCallKeepLib() { - if (!callkeep) { - throw Error( - 'react-native-callkeep library is not installed. Please see https://github.com/react-native-webrtc/react-native-callkeep#Installation for installation instructions', - ); - } - return callkeep; -} diff --git a/packages/react-native-sdk/src/utils/push/libs/index.ts b/packages/react-native-sdk/src/utils/push/libs/index.ts index 73a249953b..ff5c595f95 100644 --- a/packages/react-native-sdk/src/utils/push/libs/index.ts +++ b/packages/react-native-sdk/src/utils/push/libs/index.ts @@ -1,9 +1,8 @@ export * from './expoNotifications'; export * from './firebaseMessaging'; export * from './iosPushNotification'; -export * from './voipPushNotification'; -export * from './callkeep'; export * from './notifee'; +export * from './callingx'; /* NOTE: must keep each libs in different files diff --git a/packages/react-native-sdk/src/utils/push/libs/notifee/index.ts b/packages/react-native-sdk/src/utils/push/libs/notifee/index.ts index e1e45a7994..e867904733 100644 --- a/packages/react-native-sdk/src/utils/push/libs/notifee/index.ts +++ b/packages/react-native-sdk/src/utils/push/libs/notifee/index.ts @@ -1,6 +1,4 @@ -import { PermissionsAndroid } from 'react-native'; import { lib, type Type } from './lib'; -import { videoLoggerSystem } from '@stream-io/video-client'; export type NotifeeLib = Type; @@ -35,35 +33,6 @@ export function getNotifeeLibThrowIfNotInstalledForPush() { return lib; } -export function getNotifeeLibNoThrowForKeepCallAlive() { - if (!lib) { - const logger = videoLoggerSystem.getLogger('getNotifeeLibNoThrow'); - logger.info( - `${'@notifee/react-native library not installed. It is required to keep call alive in the background for Android. '}${INSTALLATION_INSTRUCTION}`, - ); - } - return lib; -} - -export async function getKeepCallAliveForegroundServiceTypes() { - const types: AndroidForegroundServiceType[] = [ - AndroidForegroundServiceType.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK, - ]; - const hasCameraPermission = await PermissionsAndroid.check( - PermissionsAndroid.PERMISSIONS.CAMERA!, - ); - if (hasCameraPermission) { - types.push(AndroidForegroundServiceType.FOREGROUND_SERVICE_TYPE_CAMERA); - } - const hasMicrophonePermission = await PermissionsAndroid.check( - PermissionsAndroid.PERMISSIONS.RECORD_AUDIO!, - ); - if (hasMicrophonePermission) { - types.push(AndroidForegroundServiceType.FOREGROUND_SERVICE_TYPE_MICROPHONE); - } - return types; -} - export function getIncomingCallForegroundServiceTypes() { const types: AndroidForegroundServiceType[] = [ AndroidForegroundServiceType.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE, diff --git a/packages/react-native-sdk/src/utils/push/libs/voipPushNotification.ts b/packages/react-native-sdk/src/utils/push/libs/voipPushNotification.ts deleted file mode 100644 index 8b1f52bd83..0000000000 --- a/packages/react-native-sdk/src/utils/push/libs/voipPushNotification.ts +++ /dev/null @@ -1,17 +0,0 @@ -export type VoipPushNotificationType = - typeof import('react-native-voip-push-notification').default; - -let voipPushNotification: VoipPushNotificationType | undefined; - -try { - voipPushNotification = require('react-native-voip-push-notification').default; -} catch {} - -export function getVoipPushNotificationLib() { - if (!voipPushNotification) { - throw Error( - "react-native-voip-push-notification library is not installed. Please install it using 'yarn add react-native-voip-push-notification' or 'npm i react-native-voip-push-notification --save'", - ); - } - return voipPushNotification; -} diff --git a/packages/react-native-sdk/src/utils/push/setupCallingExpEvents.ts b/packages/react-native-sdk/src/utils/push/setupCallingExpEvents.ts new file mode 100644 index 0000000000..3bf11d6b3e --- /dev/null +++ b/packages/react-native-sdk/src/utils/push/setupCallingExpEvents.ts @@ -0,0 +1,135 @@ +import { videoLoggerSystem } from '@stream-io/video-client'; +import type { StreamVideoConfig } from '../StreamVideoRN/types'; +import { + clearPushWSEventSubscriptions, + processCallFromPushInBackground, +} from './internal/utils'; +import { setPushLogoutCallback } from '../internal/pushLogoutCallback'; +import { resolvePendingAudioSession } from '../internal/callingx/audioSessionPromise'; +import { + getCallingxLib, + type EventData, + type EventParams, +} from './libs/callingx'; +import { Platform } from 'react-native'; + +type PushConfig = NonNullable; + +const logger = videoLoggerSystem.getLogger('callingx'); + +/** + * Sets up callingx event listeners for handling call actions from the native calling UI. + */ +export function setupCallingExpEvents(pushConfig: NonNullable) { + const hasPushProvider = + (Platform.OS === 'android' && pushConfig.android?.pushProviderName) || + (Platform.OS === 'ios' && pushConfig.ios?.pushProviderName); + + if (!hasPushProvider) { + return; + } + + const callingx = getCallingxLib(); + + const { remove: removeAnswerCall } = callingx.addEventListener( + 'answerCall', + (params) => { + onAcceptCall(pushConfig)(params); + }, + ); + + const { remove: removeEndCall } = callingx.addEventListener( + 'endCall', + (params) => { + onEndCall(pushConfig)(params); + }, + ); + + const { remove: removeDidActivateAudioSession } = callingx.addEventListener( + 'didActivateAudioSession', + onDidActivateAudioSession, + ); + const { remove: removeDidDeactivateAudioSession } = callingx.addEventListener( + 'didDeactivateAudioSession', + onDidDeactivateAudioSession, + ); + + //NOTE: until getInitialEvents invocation, events are delayed and won't be sent to event listeners, this is a way to make sure none of required events are missed + //in most cases there will be no delayed answers or ends, but if so we don't want to miss any of them + const events = callingx.getInitialEvents(); + events.forEach((event: EventData) => { + const { eventName, params } = event; + if (eventName === 'answerCall') { + logger.debug(`answerCall delayed event callId: ${params?.callId}`); + onAcceptCall(pushConfig)(params as EventParams['answerCall']); + } else if (eventName === 'endCall') { + logger.debug(`endCall delayed event callId: ${params?.callId}`); + onEndCall(pushConfig)(params as EventParams['endCall']); + } else if (eventName === 'didActivateAudioSession') { + onDidActivateAudioSession(); + } else if (eventName === 'didDeactivateAudioSession') { + onDidDeactivateAudioSession(); + } + }); + + setPushLogoutCallback(async () => { + removeAnswerCall(); + removeEndCall(); + removeDidActivateAudioSession(); + removeDidDeactivateAudioSession(); + }); +} + +const onDidActivateAudioSession = () => { + logger.debug('callingExpDidActivateAudioSession'); + resolvePendingAudioSession(); +}; + +const onDidDeactivateAudioSession = () => { + logger.debug('callingExpDidDeactivateAudioSession'); +}; + +const onAcceptCall = + (pushConfig: PushConfig) => + ({ callId: call_cid, source }: EventParams['answerCall']) => { + logger.debug(`onAcceptCall event call_cid: ${call_cid} source: ${source}`); + + if (source === 'app' || !call_cid) { + // App-initiated actions are fulfilled on the native side immediately — nothing to do here + return; + } + + const callingx = getCallingxLib(); + clearPushWSEventSubscriptions(call_cid); + + processCallFromPushInBackground( + pushConfig, + call_cid, + 'accept', + (didFail) => { + callingx.fulfillAnswerCallAction(call_cid, didFail); + }, + ); + }; + +const onEndCall = + (pushConfig: PushConfig) => + ({ callId: call_cid, source }: EventParams['endCall']) => { + logger.debug(`onEndCall event call_cid: ${call_cid} source: ${source}`); + + if (source === 'app' || !call_cid) { + // App-initiated actions are fulfilled on the native side immediately — nothing to do here + return; + } + + const callingx = getCallingxLib(); + clearPushWSEventSubscriptions(call_cid); + processCallFromPushInBackground( + pushConfig, + call_cid, + 'decline', + (didFail) => { + callingx.fulfillEndCallAction(call_cid, didFail); + }, + ); + }; diff --git a/packages/react-native-sdk/src/utils/push/setupIosCallKeepEvents.ts b/packages/react-native-sdk/src/utils/push/setupIosCallKeepEvents.ts deleted file mode 100644 index afb7468f4e..0000000000 --- a/packages/react-native-sdk/src/utils/push/setupIosCallKeepEvents.ts +++ /dev/null @@ -1,252 +0,0 @@ -import { - pushAcceptedIncomingCallCId$, - voipCallkeepAcceptedCallOnNativeDialerMap$, - voipCallkeepCallOnForegroundMap$, - voipPushNotificationCallCId$, -} from './internal/rxSubjects'; -import { RxUtils, videoLoggerSystem } from '@stream-io/video-client'; -import { getCallKeepLib, getVoipPushNotificationLib } from './libs'; -import type { StreamVideoConfig } from '../StreamVideoRN/types'; -import { - clearPushWSEventSubscriptions, - processCallFromPushInBackground, -} from './internal/utils'; -import { AppState, NativeModules, Platform } from 'react-native'; -import { RTCAudioSession } from '@stream-io/react-native-webrtc'; -import { setPushLogoutCallback } from '../internal/pushLogoutCallback'; - -type PushConfig = NonNullable; - -/** - * This hook is used to listen to callkeep events and do the necessary actions - */ -export function setupIosCallKeepEvents( - pushConfig: NonNullable, -) { - if (Platform.OS !== 'ios' || !pushConfig.ios.pushProviderName) { - return; - } - if (!pushConfig.android.incomingCallChannel) { - // TODO: remove this check and find a better way once we have telecom integration for android - videoLoggerSystem - .getLogger('setupIosCallKeepEvents') - .debug( - 'android incomingCallChannel is not defined, so skipping the setupIosCallKeepEvents', - ); - return; - } - const logger = videoLoggerSystem.getLogger('setupIosCallKeepEvents'); - const callkeep = getCallKeepLib(); - - async function getCallCid(callUUID: string): Promise { - try { - const call_cid = - await NativeModules.StreamVideoReactNative.getIncomingCallCid(callUUID); - // in a case that voipPushNotificationCallCId$ is empty (this should not happen as voipPushNotificationCallCId$ is updated in push reception)] - // update it with this call_cid - const voipPushNotificationCallCId = RxUtils.getCurrentValue( - voipPushNotificationCallCId$, - ); - if (!voipPushNotificationCallCId) { - logger.debug( - `voipPushNotificationCallCId$ is empty, updating it with the call_cid: ${call_cid} for callUUID: ${callUUID}`, - ); - voipPushNotificationCallCId$.next(call_cid); - } - return call_cid; - } catch { - logger.debug( - `Error in getting call cid from native module for callUUID: ${callUUID} - probably the call was already processed, so ignoring this callkeep event`, - ); - } - return undefined; - } - - function answerCall(callUUID: string) { - getCallCid(callUUID).then((call_cid) => { - logger.debug(`answerCall event with call_cid: ${call_cid}`); - iosCallkeepAcceptCall(call_cid, callUUID); - }); - } - - function endCall(callUUID: string) { - getCallCid(callUUID).then((call_cid) => { - logger.debug(`endCall event with call_cid: ${call_cid}`); - iosCallkeepRejectCall(call_cid, callUUID, pushConfig!); - }); - } - - /** - * CallKeep / CallKit audio-session events -> WebRTC (iOS) - * - * iOS CallKit is the authority that *activates* and *deactivates* the underlying `AVAudioSession` - * when a call is answered/ended from the system UI (lock screen, Call UI, Bluetooth, etc). - * - * WebRTC on iOS wraps `AVAudioSession` with `RTCAudioSession` and its AudioDeviceModule relies on - * being notified of those lifecycle transitions to correctly start/stop audio I/O and keep its - * internal activation state consistent (e.g. activation count, playout/recording start). - * - * If these callbacks don’t reach WebRTC, answering via the native dialer UI can result in: - * - no microphone capture / one-way audio - * - silent playout until the app forces an audio reconfiguration - * - flaky audio routing (speaker/earpiece/Bluetooth) across subsequent calls - * - * We forward CallKeep’s `didActivateAudioSession` / `didDeactivateAudioSession` events to WebRTC’s - * `RTCAudioSession.audioSessionDidActivate()` / `audioSessionDidDeactivate()` methods. - */ - function didActivateAudioSession() { - logger.debug('didActivateAudioSession'); - RTCAudioSession.audioSessionDidActivate(); - } - - function didDeactivateAudioSession() { - logger.debug('didDeactivateAudioSession'); - RTCAudioSession.audioSessionDidDeactivate(); - } - - function didDisplayIncomingCall(callUUID: string, payload: object) { - const voipPushNotification = getVoipPushNotificationLib(); - // @ts-expect-error - call_cid is not part of RNCallKeepEventPayload - const call_cid = payload?.call_cid as string | undefined; - logger.debug( - `didDisplayIncomingCall event with callUUID: ${callUUID} call_cid: ${call_cid}`, - ); - if (call_cid) { - if (AppState.currentState === 'background') { - processCallFromPushInBackground( - pushConfig!, - call_cid, - 'backgroundDelivered', - ); - } - voipCallkeepCallOnForegroundMap$.next({ - uuid: callUUID, - cid: call_cid, - }); - } - voipPushNotification.onVoipNotificationCompleted(callUUID); - } - - const { remove: removeAnswerCall } = callkeep.addEventListener( - 'answerCall', - ({ callUUID }) => { - answerCall(callUUID); - }, - ); - const { remove: removeEndCall } = callkeep.addEventListener( - 'endCall', - ({ callUUID }) => { - endCall(callUUID); - }, - ); - - const { remove: removeDisplayIncomingCall } = callkeep.addEventListener( - 'didDisplayIncomingCall', - ({ callUUID, payload }) => { - didDisplayIncomingCall(callUUID, payload); - }, - ); - - const { remove: removeDidActivateAudioSession } = callkeep.addEventListener( - 'didActivateAudioSession', - () => { - didActivateAudioSession(); - }, - ); - - const { remove: removeDidDeactivateAudioSession } = callkeep.addEventListener( - 'didDeactivateAudioSession', - () => { - didDeactivateAudioSession(); - }, - ); - - const { remove: removeDidLoadWithEvents } = callkeep.addEventListener( - 'didLoadWithEvents', - (events) => { - if (!events || !Array.isArray(events) || events.length < 1) { - return; - } - - events.forEach((event) => { - const { name, data } = event; - if (name === 'RNCallKeepDidDisplayIncomingCall') { - didDisplayIncomingCall(data.callUUID, data.payload); - } else if (name === 'RNCallKeepPerformAnswerCallAction') { - answerCall(data.callUUID); - } else if (name === 'RNCallKeepPerformEndCallAction') { - endCall(data.callUUID); - } else if (name === 'RNCallKeepDidActivateAudioSession') { - didActivateAudioSession(); - } else if (name === 'RNCallKeepDidDeactivateAudioSession') { - didDeactivateAudioSession(); - } - }); - }, - ); - - setPushLogoutCallback(async () => { - removeAnswerCall(); - removeEndCall(); - removeDisplayIncomingCall(); - removeDidActivateAudioSession(); - removeDidDeactivateAudioSession(); - removeDidLoadWithEvents(); - }); -} - -const iosCallkeepAcceptCall = ( - call_cid: string | undefined, - callUUIDFromCallkeep: string, -) => { - if (!shouldProcessCallFromCallkeep(call_cid, callUUIDFromCallkeep)) { - return; - } - clearPushWSEventSubscriptions(call_cid); - // to call end callkeep later if ended in app and not through callkeep - voipCallkeepAcceptedCallOnNativeDialerMap$.next({ - uuid: callUUIDFromCallkeep, - cid: call_cid, - }); - // to process the call in the app - pushAcceptedIncomingCallCId$.next(call_cid); - // no need to keep these references anymore - voipCallkeepCallOnForegroundMap$.next(undefined); -}; - -const iosCallkeepRejectCall = async ( - call_cid: string | undefined, - callUUIDFromCallkeep: string, - pushConfig: PushConfig, -) => { - if (!shouldProcessCallFromCallkeep(call_cid, callUUIDFromCallkeep)) { - return; - } - clearPushWSEventSubscriptions(call_cid); - // remove the references if the call_cid matches - const voipPushNotificationCallCId = RxUtils.getCurrentValue( - voipPushNotificationCallCId$, - ); - if (voipPushNotificationCallCId === call_cid) { - voipCallkeepAcceptedCallOnNativeDialerMap$.next(undefined); - voipCallkeepCallOnForegroundMap$.next(undefined); - voipPushNotificationCallCId$.next(undefined); - } - - await processCallFromPushInBackground(pushConfig, call_cid, 'decline'); - await NativeModules.StreamVideoReactNative?.removeIncomingCall(call_cid); -}; - -/** - * Helper function to determine if the answer/end call event from callkeep must be processed - * Just checks if we have a valid call_cid and acts as a type guard for call_cid - */ -const shouldProcessCallFromCallkeep = ( - call_cid: string | undefined, - callUUIDFromCallkeep: string, -): call_cid is string => { - if (!call_cid || !callUUIDFromCallkeep) { - return false; - } - return true; -}; diff --git a/packages/react-native-sdk/src/utils/push/setupIosVoipPushEvents.ts b/packages/react-native-sdk/src/utils/push/setupIosVoipPushEvents.ts index 6646e6498b..7de582f23e 100644 --- a/packages/react-native-sdk/src/utils/push/setupIosVoipPushEvents.ts +++ b/packages/react-native-sdk/src/utils/push/setupIosVoipPushEvents.ts @@ -1,10 +1,11 @@ -import { getVoipPushNotificationLib } from './libs'; +// import { getVoipPushNotificationLib } from './libs'; import { Platform } from 'react-native'; import { onVoipNotificationReceived } from './internal/ios'; import { setPushLogoutCallback } from '../internal/pushLogoutCallback'; import { StreamVideoConfig } from '../StreamVideoRN/types'; import { videoLoggerSystem } from '@stream-io/video-client'; +import { getCallingxLib } from './libs'; export function setupIosVoipPushEvents( pushConfig: NonNullable, @@ -20,16 +21,19 @@ export function setupIosVoipPushEvents( ); return; } - const voipPushNotification = getVoipPushNotificationLib(); - logger.debug('notification event listener added'); - voipPushNotification.addEventListener('notification', (notification) => { - onVoipNotificationReceived(notification, pushConfig); - }); + const callingx = getCallingxLib(); + const voipNotificationReceivedListener = callingx.addEventListener( + 'voipNotificationReceived', + (params) => { + onVoipNotificationReceived(params, pushConfig); + }, + ); + setPushLogoutCallback(async () => { videoLoggerSystem .getLogger('setPushLogoutCallback') .debug('notification event listener removed'); - voipPushNotification.removeEventListener('notification'); + voipNotificationReceivedListener.remove(); }); } diff --git a/packages/video-filters-react-native/package.json b/packages/video-filters-react-native/package.json index fe6389f995..6fccfe37ee 100644 --- a/packages/video-filters-react-native/package.json +++ b/packages/video-filters-react-native/package.json @@ -56,7 +56,7 @@ "typescript": "^5.9.3" }, "peerDependencies": { - "@stream-io/react-native-webrtc": ">=125.2.1", + "@stream-io/react-native-webrtc": ">=137.1.2", "react-native": "*" }, "react-native-builder-bob": { diff --git a/sample-apps/react-native/dogfood/App.tsx b/sample-apps/react-native/dogfood/App.tsx index 3d0d46c9ab..0a5871a36f 100755 --- a/sample-apps/react-native/dogfood/App.tsx +++ b/sample-apps/react-native/dogfood/App.tsx @@ -183,30 +183,17 @@ const StackNavigator = () => { }; /** - * This component is used to watch for incoming calls and set the app mode to 'Call' + * This component is used to watch for ringing calls and set the app mode to 'Call' */ const RingingWatcher = () => { const setState = useAppGlobalStoreSetState(); - const calls = useCalls().filter((c) => c.ringing); + const hasRingingCall = useCalls().some((c) => c.ringing); useEffect(() => { - if (calls.length > 1) { - const lastCallCreatedBy = calls.at(-1)?.state.createdBy; - Alert.alert( - `Incoming call from ${ - lastCallCreatedBy?.name ?? lastCallCreatedBy?.id - }, only 1 call at a time is supported`, - ); - } - }, [calls]); - - const firstCall = calls[0]; - - useEffect(() => { - if (firstCall) { + if (hasRingingCall) { setState({ appMode: 'Call' }); } - }, [firstCall, setState]); + }, [hasRingingCall, setState]); return null; }; diff --git a/sample-apps/react-native/dogfood/Gemfile.lock b/sample-apps/react-native/dogfood/Gemfile.lock index 030db3c4a7..0dfed3130a 100644 --- a/sample-apps/react-native/dogfood/Gemfile.lock +++ b/sample-apps/react-native/dogfood/Gemfile.lock @@ -1,11 +1,9 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.7) - base64 - nkf - rexml - activesupport (7.2.2.1) + CFPropertyList (3.0.8) + abbrev (0.1.2) + activesupport (7.2.3) base64 benchmark (>= 0.3) bigdecimal @@ -17,36 +15,37 @@ GEM minitest (>= 5.1) securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) - addressable (2.8.7) - public_suffix (>= 2.0.2, < 7.0) + addressable (2.8.8) + public_suffix (>= 2.0.2, < 8.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) artifactory (3.0.17) ast (2.4.3) atomos (0.1.3) - aws-eventstream (1.3.2) - aws-partitions (1.1100.0) - aws-sdk-core (3.223.0) + aws-eventstream (1.4.0) + aws-partitions (1.1208.0) + aws-sdk-core (3.241.4) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) base64 + bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.100.0) - aws-sdk-core (~> 3, >= 3.216.0) + aws-sdk-kms (1.121.0) + aws-sdk-core (~> 3, >= 3.241.4) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.185.0) - aws-sdk-core (~> 3, >= 3.216.0) + aws-sdk-s3 (1.212.0) + aws-sdk-core (~> 3, >= 3.241.4) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) - aws-sigv4 (1.11.0) + aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.2.0) - benchmark (0.4.0) - bigdecimal (3.1.9) + benchmark (0.5.0) + bigdecimal (4.0.1) claide (1.1.0) cocoapods (1.15.2) addressable (~> 2.8) @@ -90,16 +89,17 @@ GEM commander (4.6.0) highline (~> 2.0.0) concurrent-ruby (1.3.3) - connection_pool (2.5.3) + connection_pool (3.0.2) + csv (3.3.5) declarative (0.0.20) digest-crc (0.7.0) rake (>= 12.0.0, < 14.0.0) domain_name (0.6.20240107) dotenv (2.8.1) - drb (2.2.1) + drb (2.2.3) emoji_regex (3.2.3) escape (0.0.4) - ethon (0.16.0) + ethon (0.15.0) ffi (>= 1.15.0) excon (0.112.0) faraday (1.10.4) @@ -114,14 +114,14 @@ GEM faraday-rack (~> 1.0) faraday-retry (~> 1.0) ruby2_keywords (>= 0.0.4) - faraday-cookie_jar (0.0.7) + faraday-cookie_jar (0.0.8) faraday (>= 0.8.0) - http-cookie (~> 1.0.0) + http-cookie (>= 1.0.0) faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.0) + faraday-em_synchrony (1.0.1) faraday-excon (1.1.0) faraday-httpclient (1.0.1) - faraday-multipart (1.1.0) + faraday-multipart (1.2.0) multipart-post (~> 2.0) faraday-net_http (1.0.2) faraday-net_http_persistent (1.2.0) @@ -131,15 +131,19 @@ GEM faraday_middleware (1.2.1) faraday (~> 1.0) fastimage (2.4.0) - fastlane (2.227.2) + fastlane (2.231.1) CFPropertyList (>= 2.3, < 4.0.0) + abbrev (~> 0.1.2) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) aws-sdk-s3 (~> 1.0) babosa (>= 1.0.3, < 2.0.0) - bundler (>= 1.12.0, < 3.0.0) + base64 (~> 0.2.0) + benchmark (>= 0.1.0) + bundler (>= 1.17.3, < 5.0.0) colored (~> 1.2) commander (~> 4.6) + csv (~> 3.3) dotenv (>= 2.1.1, < 3.0.0) emoji_regex (>= 0.1, < 4.0) excon (>= 0.71.0, < 1.0.0) @@ -157,10 +161,14 @@ GEM http-cookie (~> 1.0.5) json (< 3.0.0) jwt (>= 2.1.0, < 3) + logger (>= 1.6, < 2.0) mini_magick (>= 4.9.4, < 5.0.0) multipart-post (>= 2.0.0, < 3.0.0) + mutex_m (~> 0.3.0) naturally (~> 2.2) + nkf (~> 0.2.0) optparse (>= 0.1.1, < 1.0.0) + ostruct (>= 0.1.0) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) security (= 0.1.5) @@ -180,7 +188,7 @@ GEM xctest_list (= 1.2.1) fastlane-sirp (1.0.0) sysrandom (~> 1.0) - ffi (1.17.2) + ffi (1.17.3) fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) @@ -225,45 +233,47 @@ GEM domain_name (~> 0.5) httpclient (2.9.0) mutex_m - i18n (1.14.7) + i18n (1.14.8) concurrent-ruby (~> 1.0) jmespath (1.6.2) - json (2.11.3) - jwt (2.10.1) + json (2.18.0) + jwt (2.10.2) base64 - language_server-protocol (3.17.0.4) + language_server-protocol (3.17.0.5) logger (1.7.0) mini_magick (4.13.2) mini_mime (1.1.5) - minitest (5.25.5) + minitest (6.0.1) + prism (~> 1.5) molinillo (0.8.0) - multi_json (1.15.0) + multi_json (1.19.1) multipart-post (2.4.1) mutex_m (0.3.0) nanaimo (0.3.0) nap (1.1.0) - naturally (2.2.1) + naturally (2.3.0) netrc (0.11.0) nkf (0.2.0) - optparse (0.6.0) + optparse (0.8.1) os (1.1.4) + ostruct (0.6.3) parallel (1.27.0) - parser (3.3.8.0) + parser (3.3.10.1) ast (~> 2.4.1) racc plist (3.7.2) - prism (1.4.0) + prism (1.8.0) public_suffix (4.0.7) racc (1.8.1) rainbow (3.1.1) - rake (13.2.1) - regexp_parser (2.10.0) + rake (13.3.1) + regexp_parser (2.11.3) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.4.1) + rexml (3.4.4) rouge (3.28.0) rubocop (1.60.2) json (~> 2.3) @@ -276,9 +286,9 @@ GEM rubocop-ast (>= 1.30.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.44.1) + rubocop-ast (1.49.0) parser (>= 3.3.7.2) - prism (~> 1.4) + prism (~> 1.7) rubocop-performance (1.23.1) rubocop (>= 1.48.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) @@ -290,10 +300,10 @@ GEM rubyzip (2.4.1) securerandom (0.4.1) security (0.1.5) - signet (0.20.0) + signet (0.21.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) - jwt (>= 1.5, < 3.0) + jwt (>= 1.5, < 4.0) multi_json (~> 1.10) simctl (1.6.10) CFPropertyList @@ -307,8 +317,8 @@ GEM tty-screen (0.8.2) tty-spinner (0.9.3) tty-cursor (~> 0.7) - typhoeus (1.4.1) - ethon (>= 0.9.0) + typhoeus (1.5.0) + ethon (>= 0.9.0, < 0.16.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) uber (0.1.0) @@ -349,7 +359,7 @@ DEPENDENCIES xcodeproj (< 1.26.0) RUBY VERSION - ruby 3.2.3p157 + ruby 3.4.3p32 BUNDLED WITH - 2.6.8 + 2.6.7 diff --git a/sample-apps/react-native/dogfood/android/app/build.gradle b/sample-apps/react-native/dogfood/android/app/build.gradle index 708b257ee9..6250da9eca 100644 --- a/sample-apps/react-native/dogfood/android/app/build.gradle +++ b/sample-apps/react-native/dogfood/android/app/build.gradle @@ -117,8 +117,6 @@ android { } dependencies { - implementation (project(':react-native-callkeep')) - // The version of react-native is set by the React Native Gradle Plugin implementation("com.facebook.react:react-android") diff --git a/sample-apps/react-native/dogfood/android/app/src/main/AndroidManifest.xml b/sample-apps/react-native/dogfood/android/app/src/main/AndroidManifest.xml index c6d0daecef..2e69c5f646 100644 --- a/sample-apps/react-native/dogfood/android/app/src/main/AndroidManifest.xml +++ b/sample-apps/react-native/dogfood/android/app/src/main/AndroidManifest.xml @@ -23,7 +23,6 @@ - @@ -31,7 +30,6 @@ - Void ) { - - guard let stream = payload.dictionaryPayload["stream"] as? [String: Any], - let createdCallerName = stream["created_by_display_name"] as? String, - let cid = stream["call_cid"] as? String else { - completion() // Ensure completion handler is called even if parsing fails - return - } - - // Check if user is busy BEFORE registering the call - let shouldReject = StreamVideoReactNative.shouldRejectCallWhenBusy() - let hasAnyActiveCall = StreamVideoReactNative.hasAnyActiveCall() - - if shouldReject && hasAnyActiveCall { - // Complete the VoIP notification without showing CallKit UI - completion() - return - } - - let uuid = UUID().uuidString - let videoIncluded = stream["video"] as? String - let hasVideo = videoIncluded == "false" ? false : true - - StreamVideoReactNative.registerIncomingCall(cid, uuid: uuid) - - // required if you want to call `completion()` on the js side - RNVoipPushNotificationManager.addCompletionHandler(uuid, completionHandler: completion) - - // Process the received push // fire 'notification' event to JS - RNVoipPushNotificationManager.didReceiveIncomingPush(with: payload, forType: type.rawValue) // type is enum, use rawValue - - RNCallKeep.reportNewIncomingCall(uuid, - handle: createdCallerName, - handleType: "generic", - hasVideo: hasVideo, - localizedCallerName: createdCallerName, - supportsHolding: false, - supportsDTMF: false, - supportsGrouping: false, - supportsUngrouping: false, - fromPushKit: true, - payload: stream, - withCompletionHandler: nil) // Completion handler is already handled above + StreamVideoReactNative.didReceiveIncomingPush(payload, forType: type.rawValue, completionHandler: completion) } //Called when a notification is delivered to a foreground app. func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.sound, .alert, .badge]) // Use array literal for options } - - func provider(_ provider: CXProvider, didActivateAudioSession audioSession: AVAudioSession) { - RTCAudioSession.sharedInstance().audioSessionDidActivate(AVAudioSession.sharedInstance()) // Use sharedInstance() - } - - func provider(_ provider: CXProvider, didDeactivateAudioSession audioSession: AVAudioSession) { - RTCAudioSession.sharedInstance().audioSessionDidDeactivate(AVAudioSession.sharedInstance()) // Use sharedInstance() - } func application( _ application: UIApplication, @@ -132,15 +82,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // Uncomment the next line to enable verbose WebRTC logs // WebRTCModuleOptions.sharedInstance().loggingSeverity = .verbose - let localizedAppName = Bundle.main.localizedInfoDictionary?["CFBundleDisplayName"] as? String - let appName = Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String - RNCallKeep.setup([ - "appName": localizedAppName != nil ? localizedAppName! : appName as Any, // Forced unwrap is safe here due to nil check - "supportsVideo": true, - "includesCallsInRecents": false, - ]) - - RNVoipPushNotificationManager.voipRegistration() + StreamVideoReactNative.voipRegistration() let center = UNUserNotificationCenter.current() center.delegate = self diff --git a/sample-apps/react-native/dogfood/ios/Podfile.lock b/sample-apps/react-native/dogfood/ios/Podfile.lock index 2b79ccf8ff..f9049ecf16 100644 --- a/sample-apps/react-native/dogfood/ios/Podfile.lock +++ b/sample-apps/react-native/dogfood/ios/Podfile.lock @@ -1,8 +1,37 @@ PODS: - boost (1.84.0) + - Callingx (0.1.0): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - stream-react-native-webrtc + - Yoga - DoubleConversion (1.1.6) - fast_float (8.0.0) - - FBLazyVector (0.83.2) + - FBLazyVector (0.83.4) - fmt (11.0.2) - glog (0.3.5) - hermes-engine (0.14.1): @@ -27,31 +56,31 @@ PODS: - fast_float (= 8.0.0) - fmt (= 11.0.2) - glog - - RCTDeprecation (0.83.2) - - RCTRequired (0.83.2) - - RCTSwiftUI (0.83.2) - - RCTSwiftUIWrapper (0.83.2): + - RCTDeprecation (0.83.4) + - RCTRequired (0.83.4) + - RCTSwiftUI (0.83.4) + - RCTSwiftUIWrapper (0.83.4): - RCTSwiftUI - - RCTTypeSafety (0.83.2): - - FBLazyVector (= 0.83.2) - - RCTRequired (= 0.83.2) - - React-Core (= 0.83.2) - - React (0.83.2): - - React-Core (= 0.83.2) - - React-Core/DevSupport (= 0.83.2) - - React-Core/RCTWebSocket (= 0.83.2) - - React-RCTActionSheet (= 0.83.2) - - React-RCTAnimation (= 0.83.2) - - React-RCTBlob (= 0.83.2) - - React-RCTImage (= 0.83.2) - - React-RCTLinking (= 0.83.2) - - React-RCTNetwork (= 0.83.2) - - React-RCTSettings (= 0.83.2) - - React-RCTText (= 0.83.2) - - React-RCTVibration (= 0.83.2) - - React-callinvoker (0.83.2) + - RCTTypeSafety (0.83.4): + - FBLazyVector (= 0.83.4) + - RCTRequired (= 0.83.4) + - React-Core (= 0.83.4) + - React (0.83.4): + - React-Core (= 0.83.4) + - React-Core/DevSupport (= 0.83.4) + - React-Core/RCTWebSocket (= 0.83.4) + - React-RCTActionSheet (= 0.83.4) + - React-RCTAnimation (= 0.83.4) + - React-RCTBlob (= 0.83.4) + - React-RCTImage (= 0.83.4) + - React-RCTLinking (= 0.83.4) + - React-RCTNetwork (= 0.83.4) + - React-RCTSettings (= 0.83.4) + - React-RCTText (= 0.83.4) + - React-RCTVibration (= 0.83.4) + - React-callinvoker (0.83.4) - React-Codegen (0.1.0) - - React-Core (0.83.2): + - React-Core (0.83.4): - boost - DoubleConversion - fast_float @@ -61,7 +90,7 @@ PODS: - RCT-Folly - RCT-Folly/Fabric - RCTDeprecation - - React-Core/Default (= 0.83.2) + - React-Core/Default (= 0.83.4) - React-cxxreact - React-featureflags - React-hermes @@ -76,7 +105,7 @@ PODS: - React-utils - SocketRocket - Yoga - - React-Core/CoreModulesHeaders (0.83.2): + - React-Core/CoreModulesHeaders (0.83.4): - boost - DoubleConversion - fast_float @@ -101,7 +130,7 @@ PODS: - React-utils - SocketRocket - Yoga - - React-Core/Default (0.83.2): + - React-Core/Default (0.83.4): - boost - DoubleConversion - fast_float @@ -125,7 +154,7 @@ PODS: - React-utils - SocketRocket - Yoga - - React-Core/DevSupport (0.83.2): + - React-Core/DevSupport (0.83.4): - boost - DoubleConversion - fast_float @@ -135,8 +164,8 @@ PODS: - RCT-Folly - RCT-Folly/Fabric - RCTDeprecation - - React-Core/Default (= 0.83.2) - - React-Core/RCTWebSocket (= 0.83.2) + - React-Core/Default (= 0.83.4) + - React-Core/RCTWebSocket (= 0.83.4) - React-cxxreact - React-featureflags - React-hermes @@ -151,7 +180,7 @@ PODS: - React-utils - SocketRocket - Yoga - - React-Core/RCTActionSheetHeaders (0.83.2): + - React-Core/RCTActionSheetHeaders (0.83.4): - boost - DoubleConversion - fast_float @@ -176,7 +205,7 @@ PODS: - React-utils - SocketRocket - Yoga - - React-Core/RCTAnimationHeaders (0.83.2): + - React-Core/RCTAnimationHeaders (0.83.4): - boost - DoubleConversion - fast_float @@ -201,7 +230,7 @@ PODS: - React-utils - SocketRocket - Yoga - - React-Core/RCTBlobHeaders (0.83.2): + - React-Core/RCTBlobHeaders (0.83.4): - boost - DoubleConversion - fast_float @@ -226,7 +255,7 @@ PODS: - React-utils - SocketRocket - Yoga - - React-Core/RCTImageHeaders (0.83.2): + - React-Core/RCTImageHeaders (0.83.4): - boost - DoubleConversion - fast_float @@ -251,7 +280,7 @@ PODS: - React-utils - SocketRocket - Yoga - - React-Core/RCTLinkingHeaders (0.83.2): + - React-Core/RCTLinkingHeaders (0.83.4): - boost - DoubleConversion - fast_float @@ -276,7 +305,7 @@ PODS: - React-utils - SocketRocket - Yoga - - React-Core/RCTNetworkHeaders (0.83.2): + - React-Core/RCTNetworkHeaders (0.83.4): - boost - DoubleConversion - fast_float @@ -301,7 +330,7 @@ PODS: - React-utils - SocketRocket - Yoga - - React-Core/RCTSettingsHeaders (0.83.2): + - React-Core/RCTSettingsHeaders (0.83.4): - boost - DoubleConversion - fast_float @@ -326,7 +355,7 @@ PODS: - React-utils - SocketRocket - Yoga - - React-Core/RCTTextHeaders (0.83.2): + - React-Core/RCTTextHeaders (0.83.4): - boost - DoubleConversion - fast_float @@ -351,7 +380,7 @@ PODS: - React-utils - SocketRocket - Yoga - - React-Core/RCTVibrationHeaders (0.83.2): + - React-Core/RCTVibrationHeaders (0.83.4): - boost - DoubleConversion - fast_float @@ -376,7 +405,7 @@ PODS: - React-utils - SocketRocket - Yoga - - React-Core/RCTWebSocket (0.83.2): + - React-Core/RCTWebSocket (0.83.4): - boost - DoubleConversion - fast_float @@ -386,7 +415,7 @@ PODS: - RCT-Folly - RCT-Folly/Fabric - RCTDeprecation - - React-Core/Default (= 0.83.2) + - React-Core/Default (= 0.83.4) - React-cxxreact - React-featureflags - React-hermes @@ -401,7 +430,7 @@ PODS: - React-utils - SocketRocket - Yoga - - React-CoreModules (0.83.2): + - React-CoreModules (0.83.4): - boost - DoubleConversion - fast_float @@ -409,22 +438,22 @@ PODS: - glog - RCT-Folly - RCT-Folly/Fabric - - RCTTypeSafety (= 0.83.2) - - React-Core/CoreModulesHeaders (= 0.83.2) + - RCTTypeSafety (= 0.83.4) + - React-Core/CoreModulesHeaders (= 0.83.4) - React-debug - - React-jsi (= 0.83.2) + - React-jsi (= 0.83.4) - React-jsinspector - React-jsinspectorcdp - React-jsinspectortracing - React-NativeModulesApple - React-RCTBlob - React-RCTFBReactNativeSpec - - React-RCTImage (= 0.83.2) + - React-RCTImage (= 0.83.4) - React-runtimeexecutor - React-utils - ReactCommon - SocketRocket - - React-cxxreact (0.83.2): + - React-cxxreact (0.83.4): - boost - DoubleConversion - fast_float @@ -433,20 +462,20 @@ PODS: - hermes-engine - RCT-Folly - RCT-Folly/Fabric - - React-callinvoker (= 0.83.2) - - React-debug (= 0.83.2) - - React-jsi (= 0.83.2) + - React-callinvoker (= 0.83.4) + - React-debug (= 0.83.4) + - React-jsi (= 0.83.4) - React-jsinspector - React-jsinspectorcdp - React-jsinspectortracing - - React-logger (= 0.83.2) - - React-perflogger (= 0.83.2) + - React-logger (= 0.83.4) + - React-perflogger (= 0.83.4) - React-runtimeexecutor - - React-timing (= 0.83.2) + - React-timing (= 0.83.4) - React-utils - SocketRocket - - React-debug (0.83.2) - - React-defaultsnativemodule (0.83.2): + - React-debug (0.83.4) + - React-defaultsnativemodule (0.83.4): - boost - DoubleConversion - fast_float @@ -467,7 +496,7 @@ PODS: - React-webperformancenativemodule - SocketRocket - Yoga - - React-domnativemodule (0.83.2): + - React-domnativemodule (0.83.4): - boost - DoubleConversion - fast_float @@ -487,7 +516,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-Fabric (0.83.2): + - React-Fabric (0.83.4): - boost - DoubleConversion - fast_float @@ -501,25 +530,25 @@ PODS: - React-Core - React-cxxreact - React-debug - - React-Fabric/animated (= 0.83.2) - - React-Fabric/animationbackend (= 0.83.2) - - React-Fabric/animations (= 0.83.2) - - React-Fabric/attributedstring (= 0.83.2) - - React-Fabric/bridging (= 0.83.2) - - React-Fabric/componentregistry (= 0.83.2) - - React-Fabric/componentregistrynative (= 0.83.2) - - React-Fabric/components (= 0.83.2) - - React-Fabric/consistency (= 0.83.2) - - React-Fabric/core (= 0.83.2) - - React-Fabric/dom (= 0.83.2) - - React-Fabric/imagemanager (= 0.83.2) - - React-Fabric/leakchecker (= 0.83.2) - - React-Fabric/mounting (= 0.83.2) - - React-Fabric/observers (= 0.83.2) - - React-Fabric/scheduler (= 0.83.2) - - React-Fabric/telemetry (= 0.83.2) - - React-Fabric/templateprocessor (= 0.83.2) - - React-Fabric/uimanager (= 0.83.2) + - React-Fabric/animated (= 0.83.4) + - React-Fabric/animationbackend (= 0.83.4) + - React-Fabric/animations (= 0.83.4) + - React-Fabric/attributedstring (= 0.83.4) + - React-Fabric/bridging (= 0.83.4) + - React-Fabric/componentregistry (= 0.83.4) + - React-Fabric/componentregistrynative (= 0.83.4) + - React-Fabric/components (= 0.83.4) + - React-Fabric/consistency (= 0.83.4) + - React-Fabric/core (= 0.83.4) + - React-Fabric/dom (= 0.83.4) + - React-Fabric/imagemanager (= 0.83.4) + - React-Fabric/leakchecker (= 0.83.4) + - React-Fabric/mounting (= 0.83.4) + - React-Fabric/observers (= 0.83.4) + - React-Fabric/scheduler (= 0.83.4) + - React-Fabric/telemetry (= 0.83.4) + - React-Fabric/templateprocessor (= 0.83.4) + - React-Fabric/uimanager (= 0.83.4) - React-featureflags - React-graphics - React-jsi @@ -531,7 +560,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/animated (0.83.2): + - React-Fabric/animated (0.83.4): - boost - DoubleConversion - fast_float @@ -556,7 +585,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/animationbackend (0.83.2): + - React-Fabric/animationbackend (0.83.4): - boost - DoubleConversion - fast_float @@ -581,7 +610,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/animations (0.83.2): + - React-Fabric/animations (0.83.4): - boost - DoubleConversion - fast_float @@ -606,7 +635,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/attributedstring (0.83.2): + - React-Fabric/attributedstring (0.83.4): - boost - DoubleConversion - fast_float @@ -631,7 +660,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/bridging (0.83.2): + - React-Fabric/bridging (0.83.4): - boost - DoubleConversion - fast_float @@ -656,7 +685,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/componentregistry (0.83.2): + - React-Fabric/componentregistry (0.83.4): - boost - DoubleConversion - fast_float @@ -681,7 +710,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/componentregistrynative (0.83.2): + - React-Fabric/componentregistrynative (0.83.4): - boost - DoubleConversion - fast_float @@ -706,7 +735,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/components (0.83.2): + - React-Fabric/components (0.83.4): - boost - DoubleConversion - fast_float @@ -720,10 +749,10 @@ PODS: - React-Core - React-cxxreact - React-debug - - React-Fabric/components/legacyviewmanagerinterop (= 0.83.2) - - React-Fabric/components/root (= 0.83.2) - - React-Fabric/components/scrollview (= 0.83.2) - - React-Fabric/components/view (= 0.83.2) + - React-Fabric/components/legacyviewmanagerinterop (= 0.83.4) + - React-Fabric/components/root (= 0.83.4) + - React-Fabric/components/scrollview (= 0.83.4) + - React-Fabric/components/view (= 0.83.4) - React-featureflags - React-graphics - React-jsi @@ -735,7 +764,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/components/legacyviewmanagerinterop (0.83.2): + - React-Fabric/components/legacyviewmanagerinterop (0.83.4): - boost - DoubleConversion - fast_float @@ -760,7 +789,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/components/root (0.83.2): + - React-Fabric/components/root (0.83.4): - boost - DoubleConversion - fast_float @@ -785,7 +814,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/components/scrollview (0.83.2): + - React-Fabric/components/scrollview (0.83.4): - boost - DoubleConversion - fast_float @@ -810,7 +839,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/components/view (0.83.2): + - React-Fabric/components/view (0.83.4): - boost - DoubleConversion - fast_float @@ -837,7 +866,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-Fabric/consistency (0.83.2): + - React-Fabric/consistency (0.83.4): - boost - DoubleConversion - fast_float @@ -862,7 +891,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/core (0.83.2): + - React-Fabric/core (0.83.4): - boost - DoubleConversion - fast_float @@ -887,7 +916,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/dom (0.83.2): + - React-Fabric/dom (0.83.4): - boost - DoubleConversion - fast_float @@ -912,7 +941,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/imagemanager (0.83.2): + - React-Fabric/imagemanager (0.83.4): - boost - DoubleConversion - fast_float @@ -937,7 +966,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/leakchecker (0.83.2): + - React-Fabric/leakchecker (0.83.4): - boost - DoubleConversion - fast_float @@ -962,7 +991,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/mounting (0.83.2): + - React-Fabric/mounting (0.83.4): - boost - DoubleConversion - fast_float @@ -987,7 +1016,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/observers (0.83.2): + - React-Fabric/observers (0.83.4): - boost - DoubleConversion - fast_float @@ -1001,8 +1030,8 @@ PODS: - React-Core - React-cxxreact - React-debug - - React-Fabric/observers/events (= 0.83.2) - - React-Fabric/observers/intersection (= 0.83.2) + - React-Fabric/observers/events (= 0.83.4) + - React-Fabric/observers/intersection (= 0.83.4) - React-featureflags - React-graphics - React-jsi @@ -1014,7 +1043,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/observers/events (0.83.2): + - React-Fabric/observers/events (0.83.4): - boost - DoubleConversion - fast_float @@ -1039,7 +1068,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/observers/intersection (0.83.2): + - React-Fabric/observers/intersection (0.83.4): - boost - DoubleConversion - fast_float @@ -1064,7 +1093,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/scheduler (0.83.2): + - React-Fabric/scheduler (0.83.4): - boost - DoubleConversion - fast_float @@ -1092,7 +1121,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/telemetry (0.83.2): + - React-Fabric/telemetry (0.83.4): - boost - DoubleConversion - fast_float @@ -1117,7 +1146,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/templateprocessor (0.83.2): + - React-Fabric/templateprocessor (0.83.4): - boost - DoubleConversion - fast_float @@ -1142,7 +1171,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/uimanager (0.83.2): + - React-Fabric/uimanager (0.83.4): - boost - DoubleConversion - fast_float @@ -1156,7 +1185,7 @@ PODS: - React-Core - React-cxxreact - React-debug - - React-Fabric/uimanager/consistency (= 0.83.2) + - React-Fabric/uimanager/consistency (= 0.83.4) - React-featureflags - React-graphics - React-jsi @@ -1169,7 +1198,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/uimanager/consistency (0.83.2): + - React-Fabric/uimanager/consistency (0.83.4): - boost - DoubleConversion - fast_float @@ -1195,7 +1224,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-FabricComponents (0.83.2): + - React-FabricComponents (0.83.4): - boost - DoubleConversion - fast_float @@ -1210,8 +1239,8 @@ PODS: - React-cxxreact - React-debug - React-Fabric - - React-FabricComponents/components (= 0.83.2) - - React-FabricComponents/textlayoutmanager (= 0.83.2) + - React-FabricComponents/components (= 0.83.4) + - React-FabricComponents/textlayoutmanager (= 0.83.4) - React-featureflags - React-graphics - React-jsi @@ -1224,7 +1253,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components (0.83.2): + - React-FabricComponents/components (0.83.4): - boost - DoubleConversion - fast_float @@ -1239,18 +1268,18 @@ PODS: - React-cxxreact - React-debug - React-Fabric - - React-FabricComponents/components/inputaccessory (= 0.83.2) - - React-FabricComponents/components/iostextinput (= 0.83.2) - - React-FabricComponents/components/modal (= 0.83.2) - - React-FabricComponents/components/rncore (= 0.83.2) - - React-FabricComponents/components/safeareaview (= 0.83.2) - - React-FabricComponents/components/scrollview (= 0.83.2) - - React-FabricComponents/components/switch (= 0.83.2) - - React-FabricComponents/components/text (= 0.83.2) - - React-FabricComponents/components/textinput (= 0.83.2) - - React-FabricComponents/components/unimplementedview (= 0.83.2) - - React-FabricComponents/components/virtualview (= 0.83.2) - - React-FabricComponents/components/virtualviewexperimental (= 0.83.2) + - React-FabricComponents/components/inputaccessory (= 0.83.4) + - React-FabricComponents/components/iostextinput (= 0.83.4) + - React-FabricComponents/components/modal (= 0.83.4) + - React-FabricComponents/components/rncore (= 0.83.4) + - React-FabricComponents/components/safeareaview (= 0.83.4) + - React-FabricComponents/components/scrollview (= 0.83.4) + - React-FabricComponents/components/switch (= 0.83.4) + - React-FabricComponents/components/text (= 0.83.4) + - React-FabricComponents/components/textinput (= 0.83.4) + - React-FabricComponents/components/unimplementedview (= 0.83.4) + - React-FabricComponents/components/virtualview (= 0.83.4) + - React-FabricComponents/components/virtualviewexperimental (= 0.83.4) - React-featureflags - React-graphics - React-jsi @@ -1263,7 +1292,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/inputaccessory (0.83.2): + - React-FabricComponents/components/inputaccessory (0.83.4): - boost - DoubleConversion - fast_float @@ -1290,7 +1319,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/iostextinput (0.83.2): + - React-FabricComponents/components/iostextinput (0.83.4): - boost - DoubleConversion - fast_float @@ -1317,7 +1346,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/modal (0.83.2): + - React-FabricComponents/components/modal (0.83.4): - boost - DoubleConversion - fast_float @@ -1344,7 +1373,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/rncore (0.83.2): + - React-FabricComponents/components/rncore (0.83.4): - boost - DoubleConversion - fast_float @@ -1371,7 +1400,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/safeareaview (0.83.2): + - React-FabricComponents/components/safeareaview (0.83.4): - boost - DoubleConversion - fast_float @@ -1398,7 +1427,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/scrollview (0.83.2): + - React-FabricComponents/components/scrollview (0.83.4): - boost - DoubleConversion - fast_float @@ -1425,7 +1454,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/switch (0.83.2): + - React-FabricComponents/components/switch (0.83.4): - boost - DoubleConversion - fast_float @@ -1452,7 +1481,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/text (0.83.2): + - React-FabricComponents/components/text (0.83.4): - boost - DoubleConversion - fast_float @@ -1479,7 +1508,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/textinput (0.83.2): + - React-FabricComponents/components/textinput (0.83.4): - boost - DoubleConversion - fast_float @@ -1506,7 +1535,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/unimplementedview (0.83.2): + - React-FabricComponents/components/unimplementedview (0.83.4): - boost - DoubleConversion - fast_float @@ -1533,7 +1562,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/virtualview (0.83.2): + - React-FabricComponents/components/virtualview (0.83.4): - boost - DoubleConversion - fast_float @@ -1560,7 +1589,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/virtualviewexperimental (0.83.2): + - React-FabricComponents/components/virtualviewexperimental (0.83.4): - boost - DoubleConversion - fast_float @@ -1587,7 +1616,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/textlayoutmanager (0.83.2): + - React-FabricComponents/textlayoutmanager (0.83.4): - boost - DoubleConversion - fast_float @@ -1614,7 +1643,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricImage (0.83.2): + - React-FabricImage (0.83.4): - boost - DoubleConversion - fast_float @@ -1623,21 +1652,21 @@ PODS: - hermes-engine - RCT-Folly - RCT-Folly/Fabric - - RCTRequired (= 0.83.2) - - RCTTypeSafety (= 0.83.2) + - RCTRequired (= 0.83.4) + - RCTTypeSafety (= 0.83.4) - React-Fabric - React-featureflags - React-graphics - React-ImageManager - React-jsi - - React-jsiexecutor (= 0.83.2) + - React-jsiexecutor (= 0.83.4) - React-logger - React-rendererdebug - React-utils - ReactCommon - SocketRocket - Yoga - - React-featureflags (0.83.2): + - React-featureflags (0.83.4): - boost - DoubleConversion - fast_float @@ -1646,7 +1675,7 @@ PODS: - RCT-Folly - RCT-Folly/Fabric - SocketRocket - - React-featureflagsnativemodule (0.83.2): + - React-featureflagsnativemodule (0.83.4): - boost - DoubleConversion - fast_float @@ -1661,7 +1690,7 @@ PODS: - React-RCTFBReactNativeSpec - ReactCommon/turbomodule/core - SocketRocket - - React-graphics (0.83.2): + - React-graphics (0.83.4): - boost - DoubleConversion - fast_float @@ -1674,7 +1703,7 @@ PODS: - React-jsiexecutor - React-utils - SocketRocket - - React-hermes (0.83.2): + - React-hermes (0.83.4): - boost - DoubleConversion - fast_float @@ -1683,17 +1712,17 @@ PODS: - hermes-engine - RCT-Folly - RCT-Folly/Fabric - - React-cxxreact (= 0.83.2) + - React-cxxreact (= 0.83.4) - React-jsi - - React-jsiexecutor (= 0.83.2) + - React-jsiexecutor (= 0.83.4) - React-jsinspector - React-jsinspectorcdp - React-jsinspectortracing - React-oscompat - - React-perflogger (= 0.83.2) + - React-perflogger (= 0.83.4) - React-runtimeexecutor - SocketRocket - - React-idlecallbacksnativemodule (0.83.2): + - React-idlecallbacksnativemodule (0.83.4): - boost - DoubleConversion - fast_float @@ -1709,7 +1738,7 @@ PODS: - React-runtimescheduler - ReactCommon/turbomodule/core - SocketRocket - - React-ImageManager (0.83.2): + - React-ImageManager (0.83.4): - boost - DoubleConversion - fast_float @@ -1724,7 +1753,7 @@ PODS: - React-rendererdebug - React-utils - SocketRocket - - React-intersectionobservernativemodule (0.83.2): + - React-intersectionobservernativemodule (0.83.4): - boost - DoubleConversion - fast_float @@ -1745,7 +1774,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-jserrorhandler (0.83.2): + - React-jserrorhandler (0.83.4): - boost - DoubleConversion - fast_float @@ -1760,7 +1789,7 @@ PODS: - React-jsi - ReactCommon/turbomodule/bridging - SocketRocket - - React-jsi (0.83.2): + - React-jsi (0.83.4): - boost - DoubleConversion - fast_float @@ -1770,7 +1799,7 @@ PODS: - RCT-Folly - RCT-Folly/Fabric - SocketRocket - - React-jsiexecutor (0.83.2): + - React-jsiexecutor (0.83.4): - boost - DoubleConversion - fast_float @@ -1789,7 +1818,7 @@ PODS: - React-runtimeexecutor - React-utils - SocketRocket - - React-jsinspector (0.83.2): + - React-jsinspector (0.83.4): - boost - DoubleConversion - fast_float @@ -1804,11 +1833,11 @@ PODS: - React-jsinspectornetwork - React-jsinspectortracing - React-oscompat - - React-perflogger (= 0.83.2) + - React-perflogger (= 0.83.4) - React-runtimeexecutor - React-utils - SocketRocket - - React-jsinspectorcdp (0.83.2): + - React-jsinspectorcdp (0.83.4): - boost - DoubleConversion - fast_float @@ -1817,7 +1846,7 @@ PODS: - RCT-Folly - RCT-Folly/Fabric - SocketRocket - - React-jsinspectornetwork (0.83.2): + - React-jsinspectornetwork (0.83.4): - boost - DoubleConversion - fast_float @@ -1827,7 +1856,7 @@ PODS: - RCT-Folly/Fabric - React-jsinspectorcdp - SocketRocket - - React-jsinspectortracing (0.83.2): + - React-jsinspectortracing (0.83.4): - boost - DoubleConversion - fast_float @@ -1841,7 +1870,7 @@ PODS: - React-oscompat - React-timing - SocketRocket - - React-jsitooling (0.83.2): + - React-jsitooling (0.83.4): - boost - DoubleConversion - fast_float @@ -1849,18 +1878,18 @@ PODS: - glog - RCT-Folly - RCT-Folly/Fabric - - React-cxxreact (= 0.83.2) + - React-cxxreact (= 0.83.4) - React-debug - - React-jsi (= 0.83.2) + - React-jsi (= 0.83.4) - React-jsinspector - React-jsinspectorcdp - React-jsinspectortracing - React-runtimeexecutor - React-utils - SocketRocket - - React-jsitracing (0.83.2): + - React-jsitracing (0.83.4): - React-jsi - - React-logger (0.83.2): + - React-logger (0.83.4): - boost - DoubleConversion - fast_float @@ -1869,7 +1898,7 @@ PODS: - RCT-Folly - RCT-Folly/Fabric - SocketRocket - - React-Mapbuffer (0.83.2): + - React-Mapbuffer (0.83.4): - boost - DoubleConversion - fast_float @@ -1879,7 +1908,7 @@ PODS: - RCT-Folly/Fabric - React-debug - SocketRocket - - React-microtasksnativemodule (0.83.2): + - React-microtasksnativemodule (0.83.4): - boost - DoubleConversion - fast_float @@ -1977,9 +2006,35 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - react-native-netinfo (11.4.1): + - react-native-netinfo (11.5.2): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety - React-Core - - react-native-safe-area-context (5.6.1): + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga + - react-native-safe-area-context (5.6.2): - boost - DoubleConversion - fast_float @@ -1997,8 +2052,8 @@ PODS: - React-graphics - React-ImageManager - React-jsi - - react-native-safe-area-context/common (= 5.6.1) - - react-native-safe-area-context/fabric (= 5.6.1) + - react-native-safe-area-context/common (= 5.6.2) + - react-native-safe-area-context/fabric (= 5.6.2) - React-NativeModulesApple - React-RCTFabric - React-renderercss @@ -2009,7 +2064,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - react-native-safe-area-context/common (5.6.1): + - react-native-safe-area-context/common (5.6.2): - boost - DoubleConversion - fast_float @@ -2037,7 +2092,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - react-native-safe-area-context/fabric (5.6.1): + - react-native-safe-area-context/fabric (5.6.2): - boost - DoubleConversion - fast_float @@ -2152,7 +2207,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-NativeModulesApple (0.83.2): + - React-NativeModulesApple (0.83.4): - boost - DoubleConversion - fast_float @@ -2173,7 +2228,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - SocketRocket - - React-networking (0.83.2): + - React-networking (0.83.4): - boost - DoubleConversion - fast_float @@ -2187,8 +2242,8 @@ PODS: - React-performancetimeline - React-timing - SocketRocket - - React-oscompat (0.83.2) - - React-perflogger (0.83.2): + - React-oscompat (0.83.4) + - React-perflogger (0.83.4): - boost - DoubleConversion - fast_float @@ -2197,7 +2252,7 @@ PODS: - RCT-Folly - RCT-Folly/Fabric - SocketRocket - - React-performancecdpmetrics (0.83.2): + - React-performancecdpmetrics (0.83.4): - boost - DoubleConversion - fast_float @@ -2211,7 +2266,7 @@ PODS: - React-runtimeexecutor - React-timing - SocketRocket - - React-performancetimeline (0.83.2): + - React-performancetimeline (0.83.4): - boost - DoubleConversion - fast_float @@ -2224,9 +2279,9 @@ PODS: - React-perflogger - React-timing - SocketRocket - - React-RCTActionSheet (0.83.2): - - React-Core/RCTActionSheetHeaders (= 0.83.2) - - React-RCTAnimation (0.83.2): + - React-RCTActionSheet (0.83.4): + - React-Core/RCTActionSheetHeaders (= 0.83.4) + - React-RCTAnimation (0.83.4): - boost - DoubleConversion - fast_float @@ -2242,7 +2297,7 @@ PODS: - React-RCTFBReactNativeSpec - ReactCommon - SocketRocket - - React-RCTAppDelegate (0.83.2): + - React-RCTAppDelegate (0.83.4): - boost - DoubleConversion - fast_float @@ -2276,7 +2331,7 @@ PODS: - React-utils - ReactCommon - SocketRocket - - React-RCTBlob (0.83.2): + - React-RCTBlob (0.83.4): - boost - DoubleConversion - fast_float @@ -2295,7 +2350,7 @@ PODS: - React-RCTNetwork - ReactCommon - SocketRocket - - React-RCTFabric (0.83.2): + - React-RCTFabric (0.83.4): - boost - DoubleConversion - fast_float @@ -2332,7 +2387,7 @@ PODS: - React-utils - SocketRocket - Yoga - - React-RCTFBReactNativeSpec (0.83.2): + - React-RCTFBReactNativeSpec (0.83.4): - boost - DoubleConversion - fast_float @@ -2346,10 +2401,10 @@ PODS: - React-Core - React-jsi - React-NativeModulesApple - - React-RCTFBReactNativeSpec/components (= 0.83.2) + - React-RCTFBReactNativeSpec/components (= 0.83.4) - ReactCommon - SocketRocket - - React-RCTFBReactNativeSpec/components (0.83.2): + - React-RCTFBReactNativeSpec/components (0.83.4): - boost - DoubleConversion - fast_float @@ -2372,7 +2427,7 @@ PODS: - ReactCommon - SocketRocket - Yoga - - React-RCTImage (0.83.2): + - React-RCTImage (0.83.4): - boost - DoubleConversion - fast_float @@ -2388,14 +2443,14 @@ PODS: - React-RCTNetwork - ReactCommon - SocketRocket - - React-RCTLinking (0.83.2): - - React-Core/RCTLinkingHeaders (= 0.83.2) - - React-jsi (= 0.83.2) + - React-RCTLinking (0.83.4): + - React-Core/RCTLinkingHeaders (= 0.83.4) + - React-jsi (= 0.83.4) - React-NativeModulesApple - React-RCTFBReactNativeSpec - ReactCommon - - ReactCommon/turbomodule/core (= 0.83.2) - - React-RCTNetwork (0.83.2): + - ReactCommon/turbomodule/core (= 0.83.4) + - React-RCTNetwork (0.83.4): - boost - DoubleConversion - fast_float @@ -2415,7 +2470,7 @@ PODS: - React-RCTFBReactNativeSpec - ReactCommon - SocketRocket - - React-RCTRuntime (0.83.2): + - React-RCTRuntime (0.83.4): - boost - DoubleConversion - fast_float @@ -2437,7 +2492,7 @@ PODS: - React-RuntimeHermes - React-utils - SocketRocket - - React-RCTSettings (0.83.2): + - React-RCTSettings (0.83.4): - boost - DoubleConversion - fast_float @@ -2452,10 +2507,10 @@ PODS: - React-RCTFBReactNativeSpec - ReactCommon - SocketRocket - - React-RCTText (0.83.2): - - React-Core/RCTTextHeaders (= 0.83.2) + - React-RCTText (0.83.4): + - React-Core/RCTTextHeaders (= 0.83.4) - Yoga - - React-RCTVibration (0.83.2): + - React-RCTVibration (0.83.4): - boost - DoubleConversion - fast_float @@ -2469,11 +2524,11 @@ PODS: - React-RCTFBReactNativeSpec - ReactCommon - SocketRocket - - React-rendererconsistency (0.83.2) - - React-renderercss (0.83.2): + - React-rendererconsistency (0.83.4) + - React-renderercss (0.83.4): - React-debug - React-utils - - React-rendererdebug (0.83.2): + - React-rendererdebug (0.83.4): - boost - DoubleConversion - fast_float @@ -2483,7 +2538,7 @@ PODS: - RCT-Folly/Fabric - React-debug - SocketRocket - - React-RuntimeApple (0.83.2): + - React-RuntimeApple (0.83.4): - boost - DoubleConversion - fast_float @@ -2512,7 +2567,7 @@ PODS: - React-runtimescheduler - React-utils - SocketRocket - - React-RuntimeCore (0.83.2): + - React-RuntimeCore (0.83.4): - boost - DoubleConversion - fast_float @@ -2534,7 +2589,7 @@ PODS: - React-runtimescheduler - React-utils - SocketRocket - - React-runtimeexecutor (0.83.2): + - React-runtimeexecutor (0.83.4): - boost - DoubleConversion - fast_float @@ -2544,10 +2599,10 @@ PODS: - RCT-Folly/Fabric - React-debug - React-featureflags - - React-jsi (= 0.83.2) + - React-jsi (= 0.83.4) - React-utils - SocketRocket - - React-RuntimeHermes (0.83.2): + - React-RuntimeHermes (0.83.4): - boost - DoubleConversion - fast_float @@ -2568,7 +2623,7 @@ PODS: - React-runtimeexecutor - React-utils - SocketRocket - - React-runtimescheduler (0.83.2): + - React-runtimescheduler (0.83.4): - boost - DoubleConversion - fast_float @@ -2590,9 +2645,9 @@ PODS: - React-timing - React-utils - SocketRocket - - React-timing (0.83.2): + - React-timing (0.83.4): - React-debug - - React-utils (0.83.2): + - React-utils (0.83.4): - boost - DoubleConversion - fast_float @@ -2602,9 +2657,9 @@ PODS: - RCT-Folly - RCT-Folly/Fabric - React-debug - - React-jsi (= 0.83.2) + - React-jsi (= 0.83.4) - SocketRocket - - React-webperformancenativemodule (0.83.2): + - React-webperformancenativemodule (0.83.4): - boost - DoubleConversion - fast_float @@ -2621,9 +2676,9 @@ PODS: - React-runtimeexecutor - ReactCommon/turbomodule/core - SocketRocket - - ReactAppDependencyProvider (0.83.2): + - ReactAppDependencyProvider (0.83.4): - ReactCodegen - - ReactCodegen (0.83.2): + - ReactCodegen (0.83.4): - boost - DoubleConversion - fast_float @@ -2649,7 +2704,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - SocketRocket - - ReactCommon (0.83.2): + - ReactCommon (0.83.4): - boost - DoubleConversion - fast_float @@ -2657,9 +2712,9 @@ PODS: - glog - RCT-Folly - RCT-Folly/Fabric - - ReactCommon/turbomodule (= 0.83.2) + - ReactCommon/turbomodule (= 0.83.4) - SocketRocket - - ReactCommon/turbomodule (0.83.2): + - ReactCommon/turbomodule (0.83.4): - boost - DoubleConversion - fast_float @@ -2668,15 +2723,15 @@ PODS: - hermes-engine - RCT-Folly - RCT-Folly/Fabric - - React-callinvoker (= 0.83.2) - - React-cxxreact (= 0.83.2) - - React-jsi (= 0.83.2) - - React-logger (= 0.83.2) - - React-perflogger (= 0.83.2) - - ReactCommon/turbomodule/bridging (= 0.83.2) - - ReactCommon/turbomodule/core (= 0.83.2) + - React-callinvoker (= 0.83.4) + - React-cxxreact (= 0.83.4) + - React-jsi (= 0.83.4) + - React-logger (= 0.83.4) + - React-perflogger (= 0.83.4) + - ReactCommon/turbomodule/bridging (= 0.83.4) + - ReactCommon/turbomodule/core (= 0.83.4) - SocketRocket - - ReactCommon/turbomodule/bridging (0.83.2): + - ReactCommon/turbomodule/bridging (0.83.4): - boost - DoubleConversion - fast_float @@ -2685,13 +2740,13 @@ PODS: - hermes-engine - RCT-Folly - RCT-Folly/Fabric - - React-callinvoker (= 0.83.2) - - React-cxxreact (= 0.83.2) - - React-jsi (= 0.83.2) - - React-logger (= 0.83.2) - - React-perflogger (= 0.83.2) + - React-callinvoker (= 0.83.4) + - React-cxxreact (= 0.83.4) + - React-jsi (= 0.83.4) + - React-logger (= 0.83.4) + - React-perflogger (= 0.83.4) - SocketRocket - - ReactCommon/turbomodule/core (0.83.2): + - ReactCommon/turbomodule/core (0.83.4): - boost - DoubleConversion - fast_float @@ -2700,17 +2755,15 @@ PODS: - hermes-engine - RCT-Folly - RCT-Folly/Fabric - - React-callinvoker (= 0.83.2) - - React-cxxreact (= 0.83.2) - - React-debug (= 0.83.2) - - React-featureflags (= 0.83.2) - - React-jsi (= 0.83.2) - - React-logger (= 0.83.2) - - React-perflogger (= 0.83.2) - - React-utils (= 0.83.2) + - React-callinvoker (= 0.83.4) + - React-cxxreact (= 0.83.4) + - React-debug (= 0.83.4) + - React-featureflags (= 0.83.4) + - React-jsi (= 0.83.4) + - React-logger (= 0.83.4) + - React-perflogger (= 0.83.4) + - React-utils (= 0.83.4) - SocketRocket - - RNCallKeep (4.3.16): - - React - RNCClipboard (1.16.3): - boost - DoubleConversion @@ -2739,11 +2792,11 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - RNCPushNotificationIOS (1.11.0): + - RNCPushNotificationIOS (1.12.0): - React-Core - RNDeviceInfo (14.1.1): - React-Core - - RNGestureHandler (2.28.0): + - RNGestureHandler (2.30.1): - boost - DoubleConversion - fast_float @@ -2833,7 +2886,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - RNReanimated (4.2.1): + - RNReanimated (4.2.3): - boost - DoubleConversion - fast_float @@ -2860,11 +2913,11 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/reanimated (= 4.2.1) + - RNReanimated/reanimated (= 4.2.3) - RNWorklets - SocketRocket - Yoga - - RNReanimated/reanimated (4.2.1): + - RNReanimated/reanimated (4.2.3): - boost - DoubleConversion - fast_float @@ -2891,11 +2944,11 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/reanimated/apple (= 4.2.1) + - RNReanimated/reanimated/apple (= 4.2.3) - RNWorklets - SocketRocket - Yoga - - RNReanimated/reanimated/apple (4.2.1): + - RNReanimated/reanimated/apple (4.2.3): - boost - DoubleConversion - fast_float @@ -2925,7 +2978,7 @@ PODS: - RNWorklets - SocketRocket - Yoga - - RNScreens (4.16.0): + - RNScreens (4.23.0): - boost - DoubleConversion - fast_float @@ -2952,10 +3005,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNScreens/common (= 4.16.0) + - RNScreens/common (= 4.23.0) - SocketRocket - Yoga - - RNScreens/common (4.16.0): + - RNScreens/common (4.23.0): - boost - DoubleConversion - fast_float @@ -2984,7 +3037,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - RNSVG (15.14.0): + - RNSVG (15.15.3): - boost - DoubleConversion - fast_float @@ -3010,10 +3063,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNSVG/common (= 15.14.0) + - RNSVG/common (= 15.15.3) - SocketRocket - Yoga - - RNSVG/common (15.14.0): + - RNSVG/common (15.15.3): - boost - DoubleConversion - fast_float @@ -3041,8 +3094,6 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - RNVoipPushNotification (3.3.3): - - React-Core - RNWorklets (0.7.3): - boost - DoubleConversion @@ -3162,7 +3213,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - stream-io-noise-cancellation-react-native (0.5.1): + - stream-io-noise-cancellation-react-native (0.6.0): - boost - DoubleConversion - fast_float @@ -3192,7 +3243,7 @@ PODS: - stream-react-native-webrtc - StreamVideoNoiseCancellation - Yoga - - stream-io-video-filters-react-native (0.10.1): + - stream-io-video-filters-react-native (0.11.0): - boost - DoubleConversion - fast_float @@ -3224,7 +3275,7 @@ PODS: - stream-react-native-webrtc (137.1.3): - React-Core - StreamWebRTC (~> 137.0.54) - - stream-video-react-native (1.30.5): + - stream-video-react-native (1.31.0): - boost - DoubleConversion - fast_float @@ -3265,6 +3316,7 @@ PODS: DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) + - "Callingx (from `../node_modules/@stream-io/react-native-callingx`)" - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) @@ -3347,7 +3399,6 @@ DEPENDENCIES: - ReactAppDependencyProvider (from `build/generated/ios/ReactAppDependencyProvider`) - ReactCodegen (from `build/generated/ios/ReactCodegen`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) - - RNCallKeep (from `../node_modules/react-native-callkeep`) - "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)" - "RNCPushNotificationIOS (from `../node_modules/@react-native-community/push-notification-ios`)" - RNDeviceInfo (from `../node_modules/react-native-device-info`) @@ -3358,7 +3409,6 @@ DEPENDENCIES: - RNReanimated (from `../node_modules/react-native-reanimated`) - RNScreens (from `../node_modules/react-native-screens`) - RNSVG (from `../node_modules/react-native-svg`) - - RNVoipPushNotification (from `../node_modules/react-native-voip-push-notification`) - RNWorklets (from `../node_modules/react-native-worklets`) - SocketRocket (~> 0.7.1) - stream-chat-react-native (from `../node_modules/stream-chat-react-native`) @@ -3379,6 +3429,8 @@ SPEC REPOS: EXTERNAL SOURCES: boost: :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" + Callingx: + :path: "../node_modules/@stream-io/react-native-callingx" DoubleConversion: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" fast_float: @@ -3542,8 +3594,6 @@ EXTERNAL SOURCES: :path: build/generated/ios/ReactCodegen ReactCommon: :path: "../node_modules/react-native/ReactCommon" - RNCallKeep: - :path: "../node_modules/react-native-callkeep" RNCClipboard: :path: "../node_modules/@react-native-clipboard/clipboard" RNCPushNotificationIOS: @@ -3564,8 +3614,6 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-screens" RNSVG: :path: "../node_modules/react-native-svg" - RNVoipPushNotification: - :path: "../node_modules/react-native-voip-push-notification" RNWorklets: :path: "../node_modules/react-native-worklets" stream-chat-react-native: @@ -3585,112 +3633,111 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 + Callingx: 509e3924c64821ef1b42fae50934def0b987b9ff DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6 - FBLazyVector: f1200e6ef6cf24885501668bdbb9eff4cf48843f + FBLazyVector: 82d1d7996af4c5850242966eb81e73f9a6dfab1e fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 - hermes-engine: b9e3cb56773893e8b538b9dad138949344e87269 - RCT-Folly: 59ec0ac1f2f39672a0c6e6cecdd39383b764646f - RCTDeprecation: 3b915a7b166f7d04eaeb4ae30aaf24236a016551 - RCTRequired: fcfec6ba532cfe4e53c49e0a4061b5eff87f8c64 - RCTSwiftUI: b2f0c2f2761631b8cd605767536cbb1cbf8d020f - RCTSwiftUIWrapper: 82b4944db8c3e99e68ff1122e5d39d6c0f4e54de - RCTTypeSafety: ef5deb31526e96bee85936b2f9fa9ccf8f009e46 - React: f4edc7518ccb0b54a6f580d89dd91471844b4990 - React-callinvoker: 55ce59d13846f45dcfb655f03160f54b26b7623e + hermes-engine: 79258df51fb2de8c52574d7678c0aeb338e65c3b + RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 + RCTDeprecation: 9da1d0cf93db23ca8b41e8efe9ae558fd9c0077f + RCTRequired: 92a63c7041031a131fa5206eb082d53f95729b79 + RCTSwiftUI: 395b65655229fa2006415207adcfcb6e35dc78ed + RCTSwiftUIWrapper: 91351441a592e07e09a2f94d2cbdf088fde7e2e1 + RCTTypeSafety: 091ec3b2994c00939652cbe91cfa9ee8a4ae75b5 + React: 3e14066ac707b3e369d09e2e923d8bee7f8c33ff + React-callinvoker: 2d95e8e26fbab01f06fbf006d2c370f834a3537b React-Codegen: 4b8b4817cea7a54b83851d4c1f91f79aa73de30a - React-Core: ca8908221ec94fb099e4aee4b23f12ecb594209f - React-CoreModules: b029cb546324cc89c90668e9269baf15cd581f5a - React-cxxreact: e5d9125d4f584f4a650b80f840911812523921e7 - React-debug: ca259fe5f0bafad00e43708897ffbed5ef02ef9b - React-defaultsnativemodule: 9011e763abba216618561d852ce0ce99768879f0 - React-domnativemodule: 3336edad7e606e811c838c1390d9a28a09e0cdb1 - React-Fabric: 850125d934dbf511b0add788b73d7035b29b9d36 - React-FabricComponents: a55b3050b9baf53a24fed0b4926aeee9d79a6b40 - React-FabricImage: 84906cc0c352d14766d96474b4bf3a790c44debe - React-featureflags: 882e16c0c98f3d6a3fe8e3f9d6d541f64b501d4c - React-featureflagsnativemodule: e348dc2dc62e81236c60fca4d23ee6433c13caa8 - React-graphics: b6c2874488663c37f5398c0b78a45bb8acb056a5 - React-hermes: 43d8fc5a7b2dd22f9f1d420cb724daadcbe1fa29 - React-idlecallbacksnativemodule: def8e780bda8b077f7380da10f9173e0204ef191 - React-ImageManager: 45f7686feee3c93ca325b44a9adfa47cff7a1a73 - React-intersectionobservernativemodule: e0247f165505712c9b169c30e8aaebb83153dde2 - React-jserrorhandler: 687870b003cb2c291213ffa459a42edbe9059acd - React-jsi: 3f643b09237167570ad62e8416f61f225072e9fa - React-jsiexecutor: a66875a34dbe62e7345f7fe3d52bc7e52fc7b129 - React-jsinspector: 9ab790d29b86761f6cd01e7c753be834730bf315 - React-jsinspectorcdp: fd58f196154f51f534ae3e565e891cfc26cb5e91 - React-jsinspectornetwork: ebcbf396f7ca378fdc8e4c9fa3079be9a431cc3d - React-jsinspectortracing: c0a4caf5cf2a9169cabfd7886be86daedc30456a - React-jsitooling: fdbb10f6e0698c1451900a35178affb6d1392684 - React-jsitracing: 3dc369824704b3812759b7bf4fd3b9878d8c196e - React-logger: 041882c33e69659747c29ee47399ef3f68666995 - React-Mapbuffer: b7b3fcfcec1007ef3721f1c3facd9a831fdfc9dd - React-microtasksnativemodule: 274d47d4b947c6c5b2245d9b528b3ca6fd0f9940 - react-native-blob-util: 9027c999d7d2b7d4b087ea40413eddc899af46fe - react-native-image-picker: b99d9c1f2ddbef491eb49e1103e5c2ce78b9ab37 - react-native-mmkv: c70c58437c0f5e5fe6992f1de1e350dcdd236332 - react-native-netinfo: f0a9899081c185db1de5bb2fdc1c88c202a059ac - react-native-safe-area-context: 3e0a25a843c40ad5efdf2bef93fdd95ad7229650 - react-native-video: 201e71d168053580319a4741e1ef44380cdcb06f - React-NativeModulesApple: 161fa0250bd6e806bd6b3dadbb110ead32ba037a - React-networking: 5ad1e3fcbcc82bfeed5019b85c6142049d995c8d - React-oscompat: 173f032a95ee30e92bbd28cd9b4626de0977f828 - React-perflogger: 7fa10f25e97c5d5f49423b9fc9d712332c0b6dac - React-performancecdpmetrics: eeae91cd039d69087ccdcd7a38c852c885811003 - React-performancetimeline: 2de5d800c11ff7d23104914d16581263e80c6df9 - React-RCTActionSheet: 238ce94d76d3f43bf8c758520889f1d8bf85f2be - React-RCTAnimation: 6c1c7720c97e76a06268b30f37f838c3301be864 - React-RCTAppDelegate: 7f077b0e819f3390ebc702237b62c8f9efb7398d - React-RCTBlob: a6edbed5914706a872e351298d9c00bb809e503f - React-RCTFabric: f35f8fd02c9cd9eea597ccc12498b9708a811ed5 - React-RCTFBReactNativeSpec: 7463e6775893a7363e4fab727523cb760f9ab516 - React-RCTImage: 8309fef87ec397a786a019ea28e5a9c3b330292d - React-RCTLinking: 5c54c58cc2093481768d54c08cc47917b0d1839e - React-RCTNetwork: a3dab6f068f647530858d905893b5afb3a9ad8e4 - React-RCTRuntime: 15cc2e323e5888fb83bf5a15cb090bae1e82198a - React-RCTSettings: a3c63913ea41134a2100c49590bdd94758180e37 - React-RCTText: c8627dda631a3beefc9c68453d52145dbb361329 - React-RCTVibration: 902f5fb272c087d4089c0d5947187c835e17f1a9 - React-rendererconsistency: 2c33da8b7ef5fc547a2e6bb6fa8379dfdb961247 - React-renderercss: 831bc3c54a475a8a7dc40aa1214d72066990768d - React-rendererdebug: 2b14ecbabe7050e0dddf35486ccf5a244738fd8b - React-RuntimeApple: 679e1177ee6e811e2fc959a1131db7356aa8ffbe - React-RuntimeCore: bc1f5c87af5c4bee955c12d71146f97b801e8ddb - React-runtimeexecutor: 47c8f4101beafa54b08abd27feb23e8c25886a06 - React-RuntimeHermes: 4787419e6512bda027700a6987c1590ff83124c3 - React-runtimescheduler: 2d3e07d8de2e5242ebb3189bd2edd90320554a33 - React-timing: 8a7692de41c7a1e152d02ca99e7daf38817b1a32 - React-utils: 95eedd35726a7a91feb374ce5df2e9ed2402fa1a - React-webperformancenativemodule: 495d6c52b3deac86b2a074c288290073ef2d90bb - ReactAppDependencyProvider: 1976cdf5076a7e34718a56ead2f2069c7f54ebe9 - ReactCodegen: 5a5391bff894d39f4f110c9d2c4f31a01953581f - ReactCommon: 69b84bf292e29f218e77631304161c0a1841dd82 - RNCallKeep: 94bbe46b807ccf58e9f6ec11bc4d6087323a1954 - RNCClipboard: 4eea71d801c880175c12f6ab14332ca86fed935f - RNCPushNotificationIOS: 64218f3c776c03d7408284a819b2abfda1834bc8 - RNDeviceInfo: 8b6fa8379062949dd79a009cf3d6b02a9c03ca59 - RNGestureHandler: 9f2af339dd736c7b2de7d0df5256af69b5f25cef - RNNotifee: 4a6ee5c7deaf00e005050052d73ee6315dff7ec9 - RNPermissions: f16d5f721280a171831d992c5886761f08075926 - RNReactNativeHapticFeedback: 43c09eb41d8321be2e1375cb87ae734e58f677b0 - RNReanimated: 0a23a0a9721f450580f0fdf04b9ae928cd22915a - RNScreens: 871305075ddf1291411b051d86da9e0d9289de02 - RNSVG: 14a9b979bfb8fc5b90c03de67409f399f847b3b3 - RNVoipPushNotification: a6f7c09e1ca7220e2be1c45e9b6b897c9600024b - RNWorklets: b5f7871d7544b3af3a0bbf2187733e989e426e8b + React-Core: 0e73cf940736e6d32683d1b9e427ca9e92f96e5a + React-CoreModules: a252c33b178381722498afe5fa475bb110cc2943 + React-cxxreact: 271c58e22ece5be60e9a6ee7d3d40474028833fc + React-debug: 00098ab10215a5f349f9e41b8da69b52d7f53b91 + React-defaultsnativemodule: 2182274eeb1b40459a8a7405712d8a6cd6792681 + React-domnativemodule: 7e38c517ab0fb3ea74f57c69e2e2ba730c0109c5 + React-Fabric: b5720298e72677d7b8a8aa89483fdc33dcf33764 + React-FabricComponents: 98ff7d7e35800d3d05bdc1a54f83788025c1a6cd + React-FabricImage: aa4f9826dcced5eee5e12e41e3f491eaa220e91a + React-featureflags: eefbf8f62c6a576602126a0de1de230750058bab + React-featureflagsnativemodule: 0085450d3061db5344c6781099542311b3513aaa + React-graphics: 56492a59f40d36354ec67c0792247ff3503941db + React-hermes: 26feaea19d95e73a794d6f84cfcbce63f85cb9ec + React-idlecallbacksnativemodule: fad601be1d1785e569378c5ac22074474e4cfc26 + React-ImageManager: 543553e70b7bdb9d51acb18787a12296827248d8 + React-intersectionobservernativemodule: b1bd7b872a7afc59f64cba9284e9a06a77dd72b1 + React-jserrorhandler: bf0069cf5482871e740e5e930e94c1efbc1f9b7f + React-jsi: 8442310fcae4f17ed2c2df00cc8a53fb479bef1b + React-jsiexecutor: e73fa2e25be645f8f98f00893adcf24e449de8ce + React-jsinspector: acee2680599a9a1dbe777524019ec5d3331a6fda + React-jsinspectorcdp: 0eae61e61eab2a9879ced678acfa184f55d9c2d1 + React-jsinspectornetwork: 6782891f0fa52e6ae726841b0ed0ce475c581931 + React-jsinspectortracing: 872d5770944d5d2b748b3eaafae66347778d8d38 + React-jsitooling: 44b154b80c0ca99f4944aa736ed15f896c8a3a98 + React-jsitracing: 9f5bf0eae699f0d8d4f82814868969f43523b04b + React-logger: 993e4b9793768764e0fdd379ad1d6582f7905463 + React-Mapbuffer: 0b0d3c3074187e72d8a6e8cacce120cb24581565 + React-microtasksnativemodule: 175741856a8f6a31e20b973cb784db023256b259 + react-native-blob-util: 7f71e0af02279ef38a2ba43e8c2fcb79cf927732 + react-native-image-picker: 6051cfd030121b880a58f1cc0e5e33e9887804e4 + react-native-mmkv: 7b9c7469fa0a7e463f9411ad3e4fe273bd5ff030 + react-native-netinfo: 64f05e94821ee3f3adcd9b67b35c1480e5564915 + react-native-safe-area-context: 0a3b034bb63a5b684dd2f5fffd3c90ef6ed41ee8 + react-native-video: d9d12aa2325ae06222e97e8bd801bbc31df2675d + React-NativeModulesApple: 5f9aa5b3964c22ea10a13fbefc5b0e91285882b9 + React-networking: f134989f25bd7e4c86128f279495b33d6c64b624 + React-oscompat: 854967d380ee2921c848790cdb942b42d22017d8 + React-perflogger: bb302310d56078ced79111225a74815465b5c9f9 + React-performancecdpmetrics: 60f97726acc23e9e339965baf01c4794da339311 + React-performancetimeline: bdedf8a33b075deedf93ef08b4e44fa7e2a63b05 + React-RCTActionSheet: 1182e251a2f93857ab7a4a13732c881449cc225f + React-RCTAnimation: 7fff267277af4af4abcec3b7d8dc4e3956aaf414 + React-RCTAppDelegate: 5e0010863f9a433d724f0811c9a4518a96cec535 + React-RCTBlob: 44ada012ff2dfa9a88f979d9631808138356b1f4 + React-RCTFabric: f14e7548d2a95e336727170b8a6a2ccdb40e1654 + React-RCTFBReactNativeSpec: ffd275ee59ee639832b2682e5841dff0b36ee341 + React-RCTImage: e02f7772bbd165ef13c0051de1b9da6baefd11e6 + React-RCTLinking: 68ffd8feb4f0ea6fe3f10a264568901e17a7575c + React-RCTNetwork: 2f99990cb2ada2f2409b83174a96e6b901d254f8 + React-RCTRuntime: 373e1bd4bfbd400fda6edf545978f37c73d9a792 + React-RCTSettings: a0ccf26bdca389ee6f6d897bf208293f86234814 + React-RCTText: d24b35c913a17b68b6207b0211967587e5c64c81 + React-RCTVibration: 3ab7eb971e4fa0774a3e0a376f3ea14dc6c7f963 + React-rendererconsistency: 6b7dbf477c6b4993e77ec4ba89386168c490259f + React-renderercss: 288970f844805dd3864a9c967c1e035948ac7ef9 + React-rendererdebug: 30c19c8fd4d3ce080efcdab0d693ffda46281316 + React-RuntimeApple: c8f3853dea3e4051379adbdb40621750add60037 + React-RuntimeCore: 8b9f702c2a2c9130b720b34a2231405d44bdf432 + React-runtimeexecutor: 75189b1b32e6d6ffa296cec70f7b867d89082215 + React-RuntimeHermes: 3c5ae1084fca5a50aa61eaa155c1dd83c9035850 + React-runtimescheduler: 05ac9eea4e46178d45dbefb3daef2448b3b0713e + React-timing: 88019680fbc33e5f23e76a0c48db5f754938163a + React-utils: 7e1534af16a2bd9ff8b0a3caf83b68696d3c913f + React-webperformancenativemodule: f61042f7803aedfe91bc688d5e3bb4323d7feb77 + ReactAppDependencyProvider: 2b19d66e5ddfe8dc7afb6338a4626156cbf2bab1 + ReactCodegen: d6c9a8bda218917454530b681464c8556e896a34 + ReactCommon: cbb4e1e2bff77892671eae8eb52154dcebf02280 + RNCClipboard: e560338bf6cc4656a09ff90610b62ddc0dbdad65 + RNCPushNotificationIOS: 85957a0534fd1309b4c028ccbe5b1baf9ab5cf57 + RNDeviceInfo: bcce8752b5043a623fe3c26789679b473f705d3c + RNGestureHandler: 8cf03fa922e1677e7ff1e3d352921ba1a7444ff0 + RNNotifee: 5e3b271e8ea7456a36eec994085543c9adca9168 + RNPermissions: ee88ba7d9fb3f9b3650ff7e5a78e7ee2d1c7e200 + RNReactNativeHapticFeedback: 5f1542065f0b24c9252bd8cf3e83bc9c548182e4 + RNReanimated: a0068c25e0b27d5418d66289a915f53eb97380df + RNScreens: 199799bdab32fa1e17ebf938b06fec52033e81e5 + RNSVG: 282c14a0d61ce7231e2f4aa213c618171f1dced5 + RNWorklets: 944dddd0eef13006b658e653abbb3ee8365c3809 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - stream-chat-react-native: 97c3894622f2da79e82307d0ac85fe38c121c7fc - stream-io-noise-cancellation-react-native: e030ba6739f33114f5efc8d0fe1fa509d9159bc1 - stream-io-video-filters-react-native: 7c4f1651b06adb4089fa421ebf14ba2a9be1545b - stream-react-native-webrtc: 9a5ebaa1175b8d2f10d590009cb168819a448586 - stream-video-react-native: fcbe50aff06854b202cc4fdaa4997d65d89871e2 + stream-chat-react-native: 362e74c743dd34d750e1878f9e479707e9edc794 + stream-io-noise-cancellation-react-native: 3ad4c3df18d6048f5dc107a93e875184959ee59c + stream-io-video-filters-react-native: 9b5f1c93ccbed9b3a8dfec557bfead7282dff949 + stream-react-native-webrtc: 98f68f17acc6bd95b5cc417dfdc0953e0120e696 + stream-video-react-native: f180cac19f111a38d55ef8a42047d2803a37c280 StreamVideoNoiseCancellation: 41f5a712aba288f9636b64b17ebfbdff52c61490 StreamWebRTC: 57bd35729bcc46b008de4e741a5b23ac28b8854d - VisionCamera: 05e4bc4783174689a5878a0797015ab32afae9e4 - Yoga: 89dfd64939cad2d0d2cc2dc48193100cef28bd98 + VisionCamera: 891edb31806dd3a239c8a9d6090d6ec78e11ee80 + Yoga: c7fcecd3a5864321db161a74cf2cd735f800c119 PODFILE CHECKSUM: 40ce52e159cfdb7fd419e1127f09742de1db1cd5 -COCOAPODS: 1.16.2 +COCOAPODS: 1.15.2 diff --git a/sample-apps/react-native/dogfood/metro.config.js b/sample-apps/react-native/dogfood/metro.config.js index a414f24daa..0dc43e348f 100644 --- a/sample-apps/react-native/dogfood/metro.config.js +++ b/sample-apps/react-native/dogfood/metro.config.js @@ -14,6 +14,7 @@ config.watchFolders = [ path.join(workspaceRoot, 'packages/react-native-sdk'), path.join(workspaceRoot, 'packages/video-filters-react-native'), path.join(workspaceRoot, 'packages/noise-cancellation-react-native'), + path.join(workspaceRoot, 'packages/react-native-callingx'), ]; // find what all modules need to be unique for the app diff --git a/sample-apps/react-native/dogfood/package.json b/sample-apps/react-native/dogfood/package.json index 05ab46580a..68fceab679 100644 --- a/sample-apps/react-native/dogfood/package.json +++ b/sample-apps/react-native/dogfood/package.json @@ -16,11 +16,12 @@ "@react-native-clipboard/clipboard": "^1.16.3", "@react-native-community/netinfo": "^11.4.1", "@react-native-community/push-notification-ios": "^1.11.0", - "@react-native-firebase/app": "^23.4.0", - "@react-native-firebase/messaging": "^23.4.0", + "@react-native-firebase/app": "~23.7.0", + "@react-native-firebase/messaging": "~23.7.0", "@react-navigation/native": "^7.1.18", "@react-navigation/native-stack": "^7.3.27", "@stream-io/noise-cancellation-react-native": "workspace:^", + "@stream-io/react-native-callingx": "workspace:^", "@stream-io/react-native-webrtc": "137.1.3", "@stream-io/video-filters-react-native": "workspace:^", "@stream-io/video-react-native-sdk": "workspace:^", @@ -28,7 +29,6 @@ "react": "19.2.0", "react-native": "^0.83.2", "react-native-blob-util": "^0.22.2", - "react-native-callkeep": "^4.3.16", "react-native-device-info": "^14.1.1", "react-native-dotenv": "^3.4.11", "react-native-gesture-handler": "^2.28.0", @@ -43,7 +43,6 @@ "react-native-toast-message": "^2.3.3", "react-native-video": "^6.17.0", "react-native-vision-camera": "^4.7.2", - "react-native-voip-push-notification": "~3.3.3", "react-native-worklets": "^0.7.3", "rxjs": "~7.8.2", "stream-chat": "^9.33.0", diff --git a/sample-apps/react-native/dogfood/src/components/VideoWrapper.tsx b/sample-apps/react-native/dogfood/src/components/VideoWrapper.tsx index 3b34d7ad21..656faf1d04 100644 --- a/sample-apps/react-native/dogfood/src/components/VideoWrapper.tsx +++ b/sample-apps/react-native/dogfood/src/components/VideoWrapper.tsx @@ -59,7 +59,7 @@ export const VideoWrapper = ({ children }: PropsWithChildren<{}>) => { token, tokenProvider, options: { - rejectCallWhenBusy: true, + rejectCallWhenBusy: false, logLevel: 'debug', logger: (level, message, ...args) => { if ( diff --git a/sample-apps/react-native/dogfood/src/constants/KnownUsers.ts b/sample-apps/react-native/dogfood/src/constants/KnownUsers.ts index 45e308091c..9ee0e3a8c2 100644 --- a/sample-apps/react-native/dogfood/src/constants/KnownUsers.ts +++ b/sample-apps/react-native/dogfood/src/constants/KnownUsers.ts @@ -25,8 +25,8 @@ export const KnownUsers = [ image: 'https://ca.slack-edge.com/T02RM6X6B-U02CA8MV9D1-8631020b96bf-512', }, { - id: 'kristian', - name: 'Kristian Martinoski', - image: 'https://ca.slack-edge.com/T02RM6X6B-U07L58DPSHG-0f665ede711c-512', + id: 'artem', + name: 'Artem Grintsevich', + image: 'https://ca.slack-edge.com/T02RM6X6B-U09P8U0KYLB-15f84a2f4367-512', }, ]; diff --git a/sample-apps/react-native/dogfood/src/navigators/Call.tsx b/sample-apps/react-native/dogfood/src/navigators/Call.tsx index ed6c55df77..0565b5b59d 100644 --- a/sample-apps/react-native/dogfood/src/navigators/Call.tsx +++ b/sample-apps/react-native/dogfood/src/navigators/Call.tsx @@ -25,7 +25,7 @@ const Calls = () => { const { top } = useSafeAreaInsets(); const orientation = useOrientation(); - const firstCall = calls[0]; + const firstCall = calls.at(-1); if (!firstCall) { return null; diff --git a/sample-apps/react-native/dogfood/src/screens/Meeting/GuestMeetingScreen.tsx b/sample-apps/react-native/dogfood/src/screens/Meeting/GuestMeetingScreen.tsx index 1721a87016..69e5df7086 100644 --- a/sample-apps/react-native/dogfood/src/screens/Meeting/GuestMeetingScreen.tsx +++ b/sample-apps/react-native/dogfood/src/screens/Meeting/GuestMeetingScreen.tsx @@ -39,7 +39,7 @@ export const GuestMeetingScreen = (props: Props) => { appEnvironment, ); - const options = { logLevel: 'warn', rejectCallWhenBusy: true } as const; + const options = { logLevel: 'warn', rejectCallWhenBusy: false } as const; if (mode === 'guest') { _videoClient = StreamVideoClient.getOrCreateInstance({ apiKey, diff --git a/sample-apps/react-native/dogfood/src/utils/setPushConfig.ts b/sample-apps/react-native/dogfood/src/utils/setPushConfig.ts index 58158bd415..d02486ef5d 100644 --- a/sample-apps/react-native/dogfood/src/utils/setPushConfig.ts +++ b/sample-apps/react-native/dogfood/src/utils/setPushConfig.ts @@ -32,6 +32,7 @@ export function setPushConfig() { StreamVideoRN.setPushConfig({ ios: { pushProviderName: 'rn-apn-video', + callsHistory: true, }, android: { pushProviderName: 'rn-fcm-video', @@ -41,16 +42,6 @@ export function setPushConfig() { importance: AndroidImportance.HIGH, sound: 'default', }, - incomingCallChannel: { - id: 'stream_incoming_call_channel_update2', - name: 'Incoming call notifications', - importance: AndroidImportance.HIGH, - }, - incomingCallNotificationTextGetters: { - getTitle: (createdUserName: string) => - `Incoming call from ${createdUserName}`, - getBody: () => 'Tap to open the call', - }, callNotificationTextGetters: { getTitle(type, createdUserName) { if (type === 'call.live_started') { @@ -70,6 +61,8 @@ export function setPushConfig() { }, }, }, + enableOngoingCalls: true, + shouldRejectCallWhenBusy: false, createStreamVideoClient, onTapNonRingingCallNotification: (call_cid) => { const [callType, callId] = call_cid.split(':'); @@ -89,13 +82,13 @@ export function setPushConfig() { // on press handlers of background notifications notifee.onBackgroundEvent(async (event) => { if (isNotifeeStreamVideoEvent(event)) { - await onAndroidNotifeeEvent({ event, isBackground: true }); + await onAndroidNotifeeEvent({ event }); } }); // on press handlers of foreground notifications notifee.onForegroundEvent((event) => { if (isNotifeeStreamVideoEvent(event)) { - onAndroidNotifeeEvent({ event, isBackground: false }); + onAndroidNotifeeEvent({ event }); } }); } @@ -104,7 +97,7 @@ export function setPushConfig() { // note: used only for non-ringing notifications notifee.onForegroundEvent((event) => { if (isNotifeeStreamVideoEvent(event)) { - oniOSNotifeeEvent({ event, isBackground: false }); + oniOSNotifeeEvent({ event }); } }); } @@ -142,6 +135,6 @@ const createStreamVideoClient = async () => { user, token, tokenProvider, - options: { logLevel: 'warn', rejectCallWhenBusy: true }, + options: { logLevel: 'warn', rejectCallWhenBusy: false }, }); }; diff --git a/sample-apps/react-native/dogfood/tsconfig.json b/sample-apps/react-native/dogfood/tsconfig.json index 8a53bc9c3f..4a97a7e995 100644 --- a/sample-apps/react-native/dogfood/tsconfig.json +++ b/sample-apps/react-native/dogfood/tsconfig.json @@ -2,7 +2,7 @@ "extends": "@react-native/typescript-config", "compilerOptions": { "skipLibCheck": true, - "lib": ["dom"], + "lib": ["esnext", "dom"], "paths": { "react": ["./node_modules/@types/react"], "react-native": ["./node_modules/react-native"] diff --git a/sample-apps/react-native/expo-video-sample/app.json b/sample-apps/react-native/expo-video-sample/app.json index d1988a27b9..2a473b4c21 100644 --- a/sample-apps/react-native/expo-video-sample/app.json +++ b/sample-apps/react-native/expo-video-sample/app.json @@ -65,7 +65,6 @@ } } ], - "@react-native-firebase/app", [ "@stream-io/video-react-native-sdk", { @@ -73,17 +72,12 @@ "addNoiseCancellation": true, "androidKeepCallAlive": true, "appleTeamId": "EHV7XZLAHA", - "ringingPushNotifications": { - "disableVideoIos": false, - "includesCallsInRecentsIos": false, - "showWhenLockedAndroid": true - }, + "ringing": true, "enableNonRingingPushNotifications": true, "androidPictureInPicture": true, "iOSEnableMultitaskingCameraAccess": true } ], - "@config-plugins/react-native-callkeep", [ "@config-plugins/react-native-webrtc", { diff --git a/sample-apps/react-native/expo-video-sample/app/index.tsx b/sample-apps/react-native/expo-video-sample/app/index.tsx index 3f3515d47d..81ec6c238b 100644 --- a/sample-apps/react-native/expo-video-sample/app/index.tsx +++ b/sample-apps/react-native/expo-video-sample/app/index.tsx @@ -6,6 +6,7 @@ import { ScrollView, KeyboardAvoidingView, Alert, + View, } from 'react-native'; import { isExpoNotificationStreamVideoEvent, @@ -75,12 +76,10 @@ export default function CreateCallScreen() { behavior={Platform.OS === 'ios' ? 'padding' : 'height'} > - - - - - - + + + + ); } diff --git a/sample-apps/react-native/expo-video-sample/components/CreateRingingCall.tsx b/sample-apps/react-native/expo-video-sample/components/CreateRingingCall.tsx index 8a3cfd79ed..8d52538f47 100644 --- a/sample-apps/react-native/expo-video-sample/components/CreateRingingCall.tsx +++ b/sample-apps/react-native/expo-video-sample/components/CreateRingingCall.tsx @@ -15,7 +15,6 @@ import { MemberRequest, useStreamVideoClient, } from '@stream-io/video-react-native-sdk'; -import { router } from 'expo-router'; export default function CreateRingingCall() { const [ringingUsers, setRingingUsers] = useState([]); @@ -45,7 +44,6 @@ export default function CreateRingingCall() { }), }, }); - router.push('/ringing'); } catch (error) { if (error instanceof Error) { Alert.alert('Error calling users', error.message); diff --git a/sample-apps/react-native/expo-video-sample/components/NavigationHeader.tsx b/sample-apps/react-native/expo-video-sample/components/NavigationHeader.tsx index 68d4bdc435..b5133fe0c4 100644 --- a/sample-apps/react-native/expo-video-sample/components/NavigationHeader.tsx +++ b/sample-apps/react-native/expo-video-sample/components/NavigationHeader.tsx @@ -1,5 +1,13 @@ import React from 'react'; -import { Alert, Image, Pressable, StyleSheet, View } from 'react-native'; +import { + Alert, + Image, + Pressable, + StyleSheet, + Text, + TouchableOpacity, + View, +} from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useAppContext } from '../context/AppContext'; @@ -28,28 +36,37 @@ export const NavigationHeader = () => { }; return ( - - + + - + {user?.name} + ); }; const styles = StyleSheet.create({ header: { - backgroundColor: 'white', paddingHorizontal: 16, }, + user: { + flexDirection: 'row', + alignItems: 'center', + gap: 10, + }, avatar: { height: 50, width: 50, borderRadius: 50, marginVertical: 10, }, + userName: { + fontSize: 16, + fontWeight: 'bold', + }, }); diff --git a/sample-apps/react-native/expo-video-sample/data/users.ts b/sample-apps/react-native/expo-video-sample/data/users.ts index f4bddbf7fa..285735f0d5 100644 --- a/sample-apps/react-native/expo-video-sample/data/users.ts +++ b/sample-apps/react-native/expo-video-sample/data/users.ts @@ -16,4 +16,10 @@ export const users = [ name: 'Vishal Narkhede', imageUrl: 'https://ca.slack-edge.com/T02RM6X6B-UHGDQJ8A0-b4a6ca584e05-512', }, + { + id: 'artemexpo', + name: 'Artem Grintsevich', + imageUrl: + 'https://ca.slack-edge.com/T02RM6X6B-U09P8U0KYLB-15f84a2f4367-512', + }, ]; diff --git a/sample-apps/react-native/expo-video-sample/metro.config.js b/sample-apps/react-native/expo-video-sample/metro.config.js index b5dd5477f9..0baf9adb42 100644 --- a/sample-apps/react-native/expo-video-sample/metro.config.js +++ b/sample-apps/react-native/expo-video-sample/metro.config.js @@ -49,6 +49,7 @@ config.watchFolders = [ path.join(workspaceRoot, 'packages/client'), path.join(workspaceRoot, 'packages/react-bindings'), path.join(workspaceRoot, 'packages/react-native-sdk'), + path.join(workspaceRoot, 'packages/react-native-callingx'), path.join(workspaceRoot, 'packages/noise-cancellation-react-native'), path.join(workspaceRoot, 'packages/video-filters-react-native'), ]; diff --git a/sample-apps/react-native/expo-video-sample/package.json b/sample-apps/react-native/expo-video-sample/package.json index 82f71cce46..b1ec62af65 100644 --- a/sample-apps/react-native/expo-video-sample/package.json +++ b/sample-apps/react-native/expo-video-sample/package.json @@ -12,14 +12,14 @@ "prebuild": "expo prebuild --clean" }, "dependencies": { - "@config-plugins/react-native-callkeep": "^12.0.0", "@config-plugins/react-native-webrtc": "^13.0.0", "@notifee/react-native": "9.1.8", "@react-native-async-storage/async-storage": "2.2.0", "@react-native-community/netinfo": "11.4.1", - "@react-native-firebase/app": "^23.4.0", - "@react-native-firebase/messaging": "^23.4.0", + "@react-native-firebase/app": "~23.7.0", + "@react-native-firebase/messaging": "~23.7.0", "@stream-io/noise-cancellation-react-native": "workspace:^", + "@stream-io/react-native-callingx": "workspace:^", "@stream-io/react-native-webrtc": "137.1.3", "@stream-io/video-filters-react-native": "workspace:^", "@stream-io/video-react-native-sdk": "workspace:^", @@ -36,13 +36,11 @@ "react": "19.1.0", "react-dom": "19.1.0", "react-native": "^0.81.5", - "react-native-callkeep": "^4.3.16", "react-native-gesture-handler": "^2.28.0", "react-native-reanimated": "~4.1.2", "react-native-safe-area-context": "~5.6.1", "react-native-screens": "~4.16.0", "react-native-svg": "^15.14.0", - "react-native-voip-push-notification": "^3.3.3", "react-native-worklets": "^0.5.0" }, "devDependencies": { diff --git a/sample-apps/react-native/expo-video-sample/utils/setPushConfig.ts b/sample-apps/react-native/expo-video-sample/utils/setPushConfig.ts index 3a8b651dd3..1af69b383e 100644 --- a/sample-apps/react-native/expo-video-sample/utils/setPushConfig.ts +++ b/sample-apps/react-native/expo-video-sample/utils/setPushConfig.ts @@ -27,16 +27,6 @@ export function setPushConfig() { importance: AndroidImportance.HIGH, sound: 'default', }, - incomingCallChannel: { - id: 'stream_incoming_call_update1', - name: 'Incoming call notifications', - importance: AndroidImportance.HIGH, - }, - incomingCallNotificationTextGetters: { - getTitle: (createdUserName: string) => - `Incoming call from ${createdUserName}`, - getBody: () => 'Tap to open the call', - }, callNotificationTextGetters: { getTitle(type, createdUserName) { if (type === 'call.live_started') { @@ -68,13 +58,13 @@ export function setPushConfig() { // on press handlers of background notifications notifee.onBackgroundEvent(async (event) => { if (isNotifeeStreamVideoEvent(event)) { - await onAndroidNotifeeEvent({ event, isBackground: true }); + await onAndroidNotifeeEvent({ event }); } }); // on press handlers of foreground notifications notifee.onForegroundEvent((event) => { if (isNotifeeStreamVideoEvent(event)) { - onAndroidNotifeeEvent({ event, isBackground: false }); + onAndroidNotifeeEvent({ event }); } }); } @@ -96,7 +86,7 @@ export function setPushConfig() { // note: used only for non-ringing notifications notifee.onForegroundEvent((event) => { if (isNotifeeStreamVideoEvent(event)) { - oniOSNotifeeEvent({ event, isBackground: false }); + oniOSNotifeeEvent({ event }); } }); } diff --git a/sample-apps/react-native/ringing-tutorial/app.json b/sample-apps/react-native/ringing-tutorial/app.json index aeadf0e2ad..ac8a966d80 100644 --- a/sample-apps/react-native/ringing-tutorial/app.json +++ b/sample-apps/react-native/ringing-tutorial/app.json @@ -24,38 +24,12 @@ "backgroundColor": "#ffffff" }, "googleServicesFile": "./google-services.json", - "package": "io.getstream.reactnative.ringingtutorial", - "permissions": [ - "android.permission.BLUETOOTH", - "android.permission.BLUETOOTH_CONNECT", - "android.permission.BLUETOOTH_ADMIN", - "android.permission.WAKE_LOCK", - "android.permission.POST_NOTIFICATIONS", - "android.permission.FOREGROUND_SERVICE", - "android.permission.FOREGROUND_SERVICE_CAMERA", - "android.permission.FOREGROUND_SERVICE_MICROPHONE", - "android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK", - "android.permission.USE_FULL_SCREEN_INTENT", - "android.permission.ACCESS_NETWORK_STATE", - "android.permission.CAMERA", - "android.permission.INTERNET", - "android.permission.MODIFY_AUDIO_SETTINGS", - "android.permission.RECORD_AUDIO", - "android.permission.SYSTEM_ALERT_WINDOW", - "android.permission.BIND_TELECOM_CONNECTION_SERVICE", - "android.permission.READ_PHONE_STATE", - "android.permission.CALL_PHONE" - ] + "package": "io.getstream.reactnative.ringingtutorial" }, "plugins": [ [ "expo-build-properties", { - "android": { - "extraMavenRepos": [ - "$rootDir/../../../node_modules/@notifee/react-native/android/libs" - ] - }, "ios": { "useFrameworks": "static", "forceStaticLinking": [ @@ -79,12 +53,7 @@ [ "@stream-io/video-react-native-sdk", { - "ringingPushNotifications": { - "disableVideoIos": false, - "includesCallsInRecentsIos": false, - "showWhenLockedAndroid": true - }, - "androidKeepCallAlive": true + "ringing": true } ], [ @@ -94,7 +63,6 @@ "microphonePermission": "$(PRODUCT_NAME) requires microphone access in order to capture and transmit audio" } ], - "@config-plugins/react-native-callkeep", "@react-native-firebase/app", "@react-native-firebase/messaging", "expo-font", diff --git a/sample-apps/react-native/ringing-tutorial/app/(app)/_layout.tsx b/sample-apps/react-native/ringing-tutorial/app/(app)/_layout.tsx index 1a7987cd47..95e23c6998 100644 --- a/sample-apps/react-native/ringing-tutorial/app/(app)/_layout.tsx +++ b/sample-apps/react-native/ringing-tutorial/app/(app)/_layout.tsx @@ -1,7 +1,6 @@ import { Text } from 'react-native'; -import { Redirect, Stack } from 'expo-router'; +import { Redirect, Slot } from 'expo-router'; import { useAuthentication } from '../../contexts/authentication-provider'; -import { GestureHandlerRootView } from 'react-native-gesture-handler'; export default function AppLayout() { const { userWithToken, isLoading } = useAuthentication(); @@ -14,9 +13,5 @@ export default function AppLayout() { return ; } - return ( - - - - ); + return ; } diff --git a/sample-apps/react-native/ringing-tutorial/app/(app)/index.tsx b/sample-apps/react-native/ringing-tutorial/app/(app)/index.tsx index dde7a5a976..dced398428 100644 --- a/sample-apps/react-native/ringing-tutorial/app/(app)/index.tsx +++ b/sample-apps/react-native/ringing-tutorial/app/(app)/index.tsx @@ -38,6 +38,7 @@ export default function Index() { const myCall = client!.call('oliver', callId); myCall.getOrCreate({ ring: true, + video: true, data: { members: [ // include self @@ -105,6 +106,7 @@ const styles = StyleSheet.create({ container: { flex: 1, paddingHorizontal: 24, + backgroundColor: 'white', }, header: { flexDirection: 'row', diff --git a/sample-apps/react-native/ringing-tutorial/app/_layout.tsx b/sample-apps/react-native/ringing-tutorial/app/_layout.tsx index c735ac42eb..1ebe79f3c8 100644 --- a/sample-apps/react-native/ringing-tutorial/app/_layout.tsx +++ b/sample-apps/react-native/ringing-tutorial/app/_layout.tsx @@ -1,18 +1,14 @@ import React from 'react'; import { Slot } from 'expo-router'; import { AuthenticationProvider } from '../contexts/authentication-provider'; -// import { setPushConfig } from '../utils/setPushConfig'; -// import { setFirebaseListeners } from '../utils/setFirebaseListeners'; - -// // Set push config -// setPushConfig(); -// // Set the firebase listeners -// setFirebaseListeners(); +import { GestureHandlerRootView } from 'react-native-gesture-handler'; export default function Root() { return ( - + + + ); } diff --git a/sample-apps/react-native/ringing-tutorial/app/index.tsx b/sample-apps/react-native/ringing-tutorial/app/index.tsx new file mode 100644 index 0000000000..b9cc0b1617 --- /dev/null +++ b/sample-apps/react-native/ringing-tutorial/app/index.tsx @@ -0,0 +1,5 @@ +import { Redirect } from 'expo-router'; + +export default function RootIndex() { + return ; +} diff --git a/sample-apps/react-native/ringing-tutorial/app/login.tsx b/sample-apps/react-native/ringing-tutorial/app/login.tsx index 2b7e4b9231..4ccbce08c1 100644 --- a/sample-apps/react-native/ringing-tutorial/app/login.tsx +++ b/sample-apps/react-native/ringing-tutorial/app/login.tsx @@ -49,6 +49,7 @@ const styles = StyleSheet.create({ flex: 1, justifyContent: 'space-around', alignItems: 'center', + backgroundColor: 'white', }, title: { fontSize: 28, diff --git a/sample-apps/react-native/ringing-tutorial/index.js b/sample-apps/react-native/ringing-tutorial/index.js index 72c5a73395..26d7a50cc2 100644 --- a/sample-apps/react-native/ringing-tutorial/index.js +++ b/sample-apps/react-native/ringing-tutorial/index.js @@ -1,6 +1,8 @@ -import 'expo-router/entry'; import { setPushConfig } from './utils/setPushConfig'; import { setFirebaseListeners } from './utils/setFirebaseListeners'; setPushConfig(); setFirebaseListeners(); + +// always import expo-router/entry at the end of the file +import 'expo-router/entry'; diff --git a/sample-apps/react-native/ringing-tutorial/metro.config.js b/sample-apps/react-native/ringing-tutorial/metro.config.js index c5c333918c..3811847baf 100644 --- a/sample-apps/react-native/ringing-tutorial/metro.config.js +++ b/sample-apps/react-native/ringing-tutorial/metro.config.js @@ -25,14 +25,20 @@ const uniqueModules = dependencyPackageNames.map((packageName) => { }; }); +// Filter out expo from unique modules to avoid blocking Expo virtual files +// Expo virtual files are resolved from workspace root's node_modules. +const uniqueModulesFiltered = uniqueModules.filter( + ({ packageName }) => packageName !== 'expo', +); + // provide the path for the unique modules -const extraNodeModules = uniqueModules.reduce((acc, item) => { +const extraNodeModules = uniqueModulesFiltered.reduce((acc, item) => { acc[item.packageName] = item.modulePath; return acc; }, {}); // block the other paths for unique modules from being resolved -const blockList = uniqueModules.map(({ blockPattern }) => blockPattern); +const blockList = uniqueModulesFiltered.map(({ blockPattern }) => blockPattern); const workspaceRoot = path.resolve(projectRoot, '../../..'); @@ -41,6 +47,7 @@ config.watchFolders = [ path.join(workspaceRoot, 'packages/client'), path.join(workspaceRoot, 'packages/react-bindings'), path.join(workspaceRoot, 'packages/react-native-sdk'), + path.join(workspaceRoot, 'packages/react-native-callingx'), ]; // using rnx-kit symlinks resolver to solve https://github.com/react-native-webrtc/react-native-webrtc/issues/1503 diff --git a/sample-apps/react-native/ringing-tutorial/package.json b/sample-apps/react-native/ringing-tutorial/package.json index 7e041a2ca3..752934dfb9 100644 --- a/sample-apps/react-native/ringing-tutorial/package.json +++ b/sample-apps/react-native/ringing-tutorial/package.json @@ -13,45 +13,42 @@ "prebuild": "expo prebuild --clean" }, "dependencies": { - "@config-plugins/react-native-callkeep": "^12.0.0", "@config-plugins/react-native-webrtc": "^13.0.0", "@expo/vector-icons": "^15.0.2", - "@notifee/react-native": "9.1.8", "@react-native-async-storage/async-storage": "2.2.0", - "@react-native-community/netinfo": "11.4.1", - "@react-native-firebase/app": "^23.4.0", - "@react-native-firebase/messaging": "^23.4.0", + "@react-native-community/netinfo": "11.5.2", + "@react-native-firebase/app": "~23.7.0", + "@react-native-firebase/messaging": "~23.7.0", "@react-navigation/bottom-tabs": "^7.4.8", "@react-navigation/native": "^7.1.18", + "@stream-io/react-native-callingx": "workspace:^", "@stream-io/react-native-webrtc": "137.1.3", "@stream-io/video-react-native-sdk": "workspace:^", - "expo": "^54.0.12", - "expo-blur": "~15.0.7", - "expo-build-properties": "~1.0.9", - "expo-constants": "~18.0.9", - "expo-dev-client": "~6.0.13", - "expo-font": "~14.0.8", - "expo-haptics": "~15.0.7", - "expo-linking": "~8.0.8", - "expo-router": "~6.0.10", - "expo-splash-screen": "~31.0.10", - "expo-status-bar": "~3.0.8", - "expo-symbols": "~1.0.7", - "expo-system-ui": "~6.0.7", - "expo-web-browser": "~15.0.8", - "react": "19.1.0", - "react-dom": "19.1.0", - "react-native": "^0.81.5", - "react-native-callkeep": "^4.3.16", - "react-native-gesture-handler": "^2.28.0", - "react-native-reanimated": "~4.1.2", - "react-native-safe-area-context": "~5.6.1", - "react-native-screens": "~4.16.0", - "react-native-svg": "^15.14.0", - "react-native-voip-push-notification": "^3.3.3", + "expo": "^55.0.0", + "expo-blur": "~55.0.10", + "expo-build-properties": "~55.0.10", + "expo-constants": "~55.0.9", + "expo-dev-client": "~55.0.19", + "expo-font": "~55.0.4", + "expo-haptics": "~55.0.9", + "expo-linking": "~55.0.9", + "expo-router": "~55.0.8", + "expo-splash-screen": "~55.0.13", + "expo-status-bar": "~55.0.4", + "expo-symbols": "~55.0.5", + "expo-system-ui": "~55.0.11", + "expo-web-browser": "~55.0.10", + "react": "19.2.0", + "react-dom": "19.2.0", + "react-native": "0.83.4", + "react-native-gesture-handler": "~2.30.0", + "react-native-reanimated": "4.2.1", + "react-native-safe-area-context": "~5.6.2", + "react-native-screens": "~4.23.0", + "react-native-svg": "15.15.3", "react-native-web": "^0.21.1", "react-native-webview": "13.16.0", - "react-native-worklets": "^0.5.0" + "react-native-worklets": "0.7.2" }, "devDependencies": { "@babel/core": "^7.28.4", @@ -59,7 +56,7 @@ "@babel/runtime": "^7.28.4", "@rnx-kit/metro-config": "^2.1.2", "@rnx-kit/metro-resolver-symlinks": "^0.2.6", - "@types/react": "~19.1.17", + "@types/react": "~19.2.10", "typescript": "~5.9.3" }, "installConfig": { diff --git a/sample-apps/react-native/ringing-tutorial/utils/setFirebaseListeners.android.ts b/sample-apps/react-native/ringing-tutorial/utils/setFirebaseListeners.android.ts index 44486ccde0..1f027b7cd7 100644 --- a/sample-apps/react-native/ringing-tutorial/utils/setFirebaseListeners.android.ts +++ b/sample-apps/react-native/ringing-tutorial/utils/setFirebaseListeners.android.ts @@ -2,10 +2,7 @@ import messaging from '@react-native-firebase/messaging'; import { firebaseDataHandler, isFirebaseStreamVideoMessage, - isNotifeeStreamVideoEvent, - onAndroidNotifeeEvent, } from '@stream-io/video-react-native-sdk'; -import notifee from '@notifee/react-native'; export const setFirebaseListeners = () => { // Set up the background message handlers @@ -14,20 +11,10 @@ export const setFirebaseListeners = () => { await firebaseDataHandler(msg.data); } }); - notifee.onBackgroundEvent(async (event) => { - if (isNotifeeStreamVideoEvent(event)) { - await onAndroidNotifeeEvent({ event, isBackground: true }); - } - }); // Set up the foreground message handlers messaging().onMessage((msg) => { if (isFirebaseStreamVideoMessage(msg)) { firebaseDataHandler(msg.data); } }); - notifee.onForegroundEvent((event) => { - if (isNotifeeStreamVideoEvent(event)) { - onAndroidNotifeeEvent({ event, isBackground: false }); - } - }); }; diff --git a/sample-apps/react-native/ringing-tutorial/utils/setPushConfig.ts b/sample-apps/react-native/ringing-tutorial/utils/setPushConfig.ts index acb500acd0..f8ca23511a 100644 --- a/sample-apps/react-native/ringing-tutorial/utils/setPushConfig.ts +++ b/sample-apps/react-native/ringing-tutorial/utils/setPushConfig.ts @@ -2,12 +2,10 @@ import { StreamVideoClient, StreamVideoRN, } from '@stream-io/video-react-native-sdk'; -import { AndroidImportance } from '@notifee/react-native'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { Users } from '../constants/Users'; const API_KEY = 'par8f5s3gn2j'; - export function setPushConfig() { StreamVideoRN.setPushConfig({ isExpo: true, @@ -16,22 +14,6 @@ export function setPushConfig() { }, android: { pushProviderName: 'expo-fcm-video', - callChannel: { - id: 'stream_call_notifications', - name: 'Call notifications', - importance: AndroidImportance.HIGH, - sound: 'default', - }, - incomingCallChannel: { - id: 'stream_incoming_call', - name: 'Incoming call notifications', - importance: AndroidImportance.HIGH, - }, - incomingCallNotificationTextGetters: { - getTitle: (createdUserName: string) => - `Incoming call from ${createdUserName}`, - getBody: () => 'Tap to open the call', - }, }, createStreamVideoClient, }); diff --git a/sample-apps/react/react-dogfood/components/ActiveCallHeader.tsx b/sample-apps/react/react-dogfood/components/ActiveCallHeader.tsx index f1ab99c925..f27a08b09d 100644 --- a/sample-apps/react/react-dogfood/components/ActiveCallHeader.tsx +++ b/sample-apps/react/react-dogfood/components/ActiveCallHeader.tsx @@ -40,6 +40,7 @@ const LatencyIndicator = () => { const Elapsed = ({ startedAt }: { startedAt: string | undefined }) => { const [elapsed, setElapsed] = useState(); const startedAtDate = useMemo( + // eslint-disable-next-line react-hooks/purity () => (startedAt ? new Date(startedAt).getTime() : Date.now()), [startedAt], ); diff --git a/scripts/release-rn-sdk-beta.mjs b/scripts/release-rn-sdk-beta.mjs new file mode 100755 index 0000000000..fc424378ae --- /dev/null +++ b/scripts/release-rn-sdk-beta.mjs @@ -0,0 +1,821 @@ +#!/usr/bin/env node + +/** + * Prerelease the React Native SDK and its workspace dependencies as beta. + * + * Flow: + * 1. Detect which in-scope packages changed vs the base ref. + * 2. Cascade: if a dep is prereleased, any sibling with a peerDep on it + * is also included (one-level propagation). + * 3. For each package: patch-bump + prerelease suffix + * (e.g. 1.30.0 → 1.30.1-beta.0), build, publish, verify on npm. + * 4. Pin the RN SDK's internal deps to the just-published versions, then + * build + publish the RN SDK itself. + * 5. Restore all mutated package.json files from backup. + */ + +import { execFileSync } from 'node:child_process'; +import { createInterface } from 'node:readline'; +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import process from 'node:process'; + +const RN_SDK_NAME = '@stream-io/video-react-native-sdk'; +const DEPENDENCY_FIELDS = [ + 'dependencies', + 'devDependencies', + 'peerDependencies', + 'optionalDependencies', +]; +const RN_PIN_FIELDS = ['dependencies', 'devDependencies']; +const DEFAULT_BASE_REF = 'origin/main'; +const DEFAULT_TAG = 'beta'; +const VERIFY_ATTEMPTS = 15; +const VERIFY_DELAY_MS = 4000; + +// Read a required value for a flag and validate shape. +function readFlagValue(argv, index, flagName) { + const value = argv[index + 1]; + if (!value || value.startsWith('-')) { + printHelpAndExit(1, `Missing or invalid value for ${flagName}.`); + } + return value; +} + +// Parse CLI flags and apply defaults. +function parseArgs(argv) { + const options = { + baseRef: DEFAULT_BASE_REF, + tag: DEFAULT_TAG, + dryRun: false, + allowEmpty: false, + keepChanges: false, + }; + + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i]; + if (arg === '--base-ref') { + options.baseRef = readFlagValue(argv, i, '--base-ref'); + i += 1; + } else if (arg === '--tag') { + options.tag = readFlagValue(argv, i, '--tag'); + i += 1; + } else if (arg === '--dry-run') { + options.dryRun = true; + } else if (arg === '--allow-empty') { + options.allowEmpty = true; + } else if (arg === '--keep-changes') { + options.keepChanges = true; + } else if (arg === '--help' || arg === '-h') { + printHelpAndExit(0); + } else { + printHelpAndExit(1, `Unknown argument: ${arg}`); + } + } + + if (!options.baseRef || !options.tag) { + printHelpAndExit(1, 'Both --base-ref and --tag must have values.'); + } + + return options; +} + +// Print usage text and terminate the process. +function printHelpAndExit(code, errorMessage) { + if (errorMessage) { + console.error(errorMessage); + console.error(''); + } + + console.log(`Release React Native SDK beta with dependency orchestration. + +Usage: + node scripts/release-rn-sdk-beta.mjs [options] + +Options: + --base-ref Base ref for change detection (default: ${DEFAULT_BASE_REF}) + --tag Prerelease tag name (default: ${DEFAULT_TAG}) + --dry-run Print release plan without publishing + --allow-empty Release RN SDK even if no package changed + --keep-changes Keep mutated package.json files after script exits + --help, -h Show this help +`); + process.exit(code); +} + +// Execute a shell command and normalize failure output. +// Use silent: true to suppress all console output (stdout + stderr are +// piped and only surfaced in the thrown Error on failure). +function run(command, args, { capture = true, silent = false } = {}) { + try { + if (capture || silent) { + return execFileSync(command, args, { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'], + }).trim(); + } + + execFileSync(command, args, { stdio: 'inherit' }); + return ''; + } catch (error) { + const stdout = error.stdout ? error.stdout.toString() : ''; + const stderr = error.stderr ? error.stderr.toString() : ''; + const details = [stdout, stderr].filter(Boolean).join('\n'); + const commandText = [command, ...args].join(' '); + throw new Error( + `Command failed: ${commandText}${details ? `\n${details}` : ''}`, + ); + } +} + +// Read and parse a JSON file. +function readJson(path) { + return JSON.parse(readFileSync(path, 'utf8')); +} + +// Write a JSON file with stable formatting. +function writeJson(path, value) { + writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`); +} + +// Prevent accidental releases from a dirty workspace. +function ensureCleanWorkingTree() { + const status = run('git', ['status', '--porcelain']); + if (status) { + // throw new Error( + // 'Working tree is not clean. Commit or stash local changes before running the release script.', + // ); + console.log(status); + } +} + +// Ensure the user-provided base ref exists. +function ensureGitRefExists(ref) { + run('git', ['rev-parse', '--verify', ref]); +} + +// Prompt the user for a line of input from stdin. +function prompt(question) { + return new Promise((done) => { + const rl = createInterface({ + input: process.stdin, + output: process.stdout, + }); + rl.question(question, (answer) => { + rl.close(); + done(answer.trim()); + }); + }); +} + +// Fail early when npm auth is missing. +// Also bridge the npm auth token to Yarn 4 if needed — Yarn does not +// read ~/.npmrc so we surface the token via YARN_NPM_AUTH_TOKEN. +// When running outside CI with 2FA enabled, prompts for an OTP once +// and reuses it for all publish calls. +async function ensureNpmAuth(options) { + run('npm', ['whoami']); + + if (!process.env.YARN_NPM_AUTH_TOKEN) { + const token = readNpmTokenFromNpmrc(); + if (token) { + process.env.YARN_NPM_AUTH_TOKEN = token; + } + } + + if (!options.dryRun && !process.env.CI && !options.otp) { + const otp = await prompt('npm OTP (leave empty to skip): '); + if (otp) { + options.otp = otp; + } + } +} + +// Parse ~/.npmrc for the registry auth token. +function readNpmTokenFromNpmrc() { + const npmrcPath = resolve( + process.env.HOME || process.env.USERPROFILE || '', + '.npmrc', + ); + if (!existsSync(npmrcPath)) { + return null; + } + const contents = readFileSync(npmrcPath, 'utf8'); + for (const line of contents.split('\n')) { + const match = line.match(/^\s*\/\/registry\.npmjs\.org\/:_authToken=(.+)$/); + if (match) { + return match[1].trim(); + } + } + return null; +} + +// Load all workspace manifests keyed by package name. +function getWorkspaceInfo() { + const raw = run('yarn', ['workspaces', 'list', '--json']); + const lines = raw ? raw.split('\n').filter(Boolean) : []; + const workspaces = lines.map((line) => JSON.parse(line)); + + const infoByName = new Map(); + for (const workspace of workspaces) { + const packageJsonPath = resolve( + process.cwd(), + workspace.location, + 'package.json', + ); + infoByName.set(workspace.name, { + name: workspace.name, + location: workspace.location, + packageJsonPath, + manifest: readJson(packageJsonPath), + }); + } + + if (!infoByName.has(RN_SDK_NAME)) { + throw new Error(`Required workspace package not found: ${RN_SDK_NAME}`); + } + + return infoByName; +} + +// Resolve direct internal dependencies for a workspace package. +function getDirectWorkspaceDependencies(workspaceInfoByName, packageName) { + const workspaceInfo = workspaceInfoByName.get(packageName); + if (!workspaceInfo) { + throw new Error(`Workspace package not found: ${packageName}`); + } + + const deps = new Set(); + for (const field of DEPENDENCY_FIELDS) { + const entries = workspaceInfo.manifest[field] ?? {}; + for (const depName of Object.keys(entries)) { + if (workspaceInfoByName.has(depName)) { + deps.add(depName); + } + } + } + + return [...deps].sort(); +} + +// Detect which release-scope packages changed since the base ref. +function getChangedPackages(baseRef, workspaceInfoByName, releaseScope) { + const diffRaw = run('git', ['diff', '--name-only', `${baseRef}...HEAD`]); + const changedFiles = diffRaw ? diffRaw.split('\n').filter(Boolean) : []; + const changedPackages = new Set(); + + for (const packageName of releaseScope) { + const workspaceInfo = workspaceInfoByName.get(packageName); + if (!workspaceInfo) { + throw new Error(`Release package not found in workspace: ${packageName}`); + } + + const prefix = `${workspaceInfo.location}/`; + const changed = changedFiles.some( + (path) => path === workspaceInfo.location || path.startsWith(prefix), + ); + if (changed) { + changedPackages.add(workspaceInfo.name); + } + } + + return changedPackages; +} + +// Build dependency edges within the selected release scope. +function buildDependencyGraph(workspaceInfoByName, releaseScope) { + const graph = new Map(); + for (const name of releaseScope) { + if (!workspaceInfoByName.has(name)) { + throw new Error(`Release package not found in workspace: ${name}`); + } + graph.set(name, new Set()); + } + + for (const packageName of releaseScope) { + const workspaceInfo = workspaceInfoByName.get(packageName); + for (const field of DEPENDENCY_FIELDS) { + const deps = workspaceInfo.manifest[field] ?? {}; + for (const depName of Object.keys(deps)) { + if (!graph.has(depName) || depName === workspaceInfo.name) { + continue; + } + graph.get(depName).add(workspaceInfo.name); + } + } + } + + return graph; +} + +// Topologically order dependencies before dependents. +function topoSort(nodes, graph) { + const indegree = new Map(nodes.map((node) => [node, 0])); + + for (const from of nodes) { + const dependents = graph.get(from) ?? new Set(); + for (const to of dependents) { + if (!indegree.has(to)) { + continue; + } + indegree.set(to, indegree.get(to) + 1); + } + } + + const queue = nodes.filter((node) => indegree.get(node) === 0).sort(); + const sorted = []; + + while (queue.length > 0) { + const current = queue.shift(); + sorted.push(current); + const dependents = graph.get(current) ?? new Set(); + for (const next of dependents) { + if (!indegree.has(next)) { + continue; + } + const nextInDegree = indegree.get(next) - 1; + indegree.set(next, nextInDegree); + if (nextInDegree === 0) { + queue.push(next); + queue.sort(); + } + } + } + + if (sorted.length !== nodes.length) { + throw new Error('Dependency cycle detected in selected release packages.'); + } + + return sorted; +} + +// Escape user values used inside regex patterns. +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +// Strip prerelease suffix from a semver string. +function stableBaseVersion(version) { + return version.split('-')[0]; +} + +// Bump the patch segment of a semver base version (e.g. 1.30.0 -> 1.30.1). +// Expects a stable semver string with no prerelease suffix; +// callers always run stableBaseVersion() first to guarantee this. +function bumpPatch(version) { + const parts = version.split('.'); + if (parts.length !== 3) { + throw new Error(`Invalid semver base version: ${version}`); + } + parts[2] = String(Number(parts[2]) + 1); + return parts.join('.'); +} + +// Compute the next prerelease number for a package/tag pair. +function nextPrereleaseVersion(packageName, currentVersion, tag) { + const baseVersion = bumpPatch(stableBaseVersion(currentVersion)); + const raw = run('npm', ['view', packageName, 'versions', '--json']); + const parsed = raw ? JSON.parse(raw) : []; + const publishedVersions = Array.isArray(parsed) ? parsed : [parsed]; + const pattern = new RegExp( + `^${escapeRegExp(baseVersion)}-${escapeRegExp(tag)}\\.(\\d+)$`, + ); + + let max = -1; + for (const version of publishedVersions) { + const match = pattern.exec(version); + if (match) { + max = Math.max(max, Number(match[1])); + } + } + + return `${baseVersion}-${tag}.${max + 1}`; +} + +// Read a dependency version from released map or local manifest. +function getLiveVersion(workspaceInfoByName, releasedVersions, packageName) { + return ( + releasedVersions.get(packageName) ?? + workspaceInfoByName.get(packageName).manifest.version + ); +} + +// Convert workspace: specifiers to concrete publishable versions. +function resolveWorkspaceSpec(currentSpec, targetVersion) { + if (typeof currentSpec !== 'string') { + return currentSpec; + } + if (!currentSpec.startsWith('workspace:')) { + return currentSpec; + } + + const suffix = currentSpec.slice('workspace:'.length); + if (suffix === '' || suffix === '*') { + return targetVersion; + } + if (suffix === '^' || suffix.startsWith('^')) { + return `^${targetVersion}`; + } + if (suffix === '~' || suffix.startsWith('~')) { + return `~${targetVersion}`; + } + return targetVersion; +} + +// Persist a manifest update with rollback backup. +function backupAndWriteManifest(workspaceInfo, nextManifest, backups) { + backupFile(workspaceInfo.packageJsonPath, backups); + workspaceInfo.manifest = nextManifest; + writeJson(workspaceInfo.packageJsonPath, nextManifest); +} + +// Snapshot a file once so we can restore it later. +function backupFile(path, backups) { + if (!backups.has(path) && existsSync(path)) { + backups.set(path, readFileSync(path, 'utf8')); + } +} + +// Pin internal workspace dependencies for publish-time manifest. +function pinWorkspaceInternalDeps( + workspaceInfo, + workspaceInfoByName, + releasedVersions, +) { + const nextManifest = structuredClone(workspaceInfo.manifest); + for (const field of DEPENDENCY_FIELDS) { + const deps = nextManifest[field]; + if (!deps) { + continue; + } + + for (const [depName, depSpec] of Object.entries(deps)) { + if (!workspaceInfoByName.has(depName)) { + continue; + } + + const depVersion = getLiveVersion( + workspaceInfoByName, + releasedVersions, + depName, + ); + deps[depName] = resolveWorkspaceSpec(depSpec, depVersion); + } + } + return nextManifest; +} + +// Pin selected RN SDK dependency versions for publish. +function pinReactNativeSdkDeps(workspaceInfo, packagesToPin, pinnedVersions) { + const nextManifest = structuredClone(workspaceInfo.manifest); + for (const field of RN_PIN_FIELDS) { + const deps = nextManifest[field]; + if (!deps) { + continue; + } + + for (const packageName of packagesToPin) { + if (Object.prototype.hasOwnProperty.call(deps, packageName)) { + deps[packageName] = pinnedVersions.get(packageName); + } + } + } + return nextManifest; +} + +// Build a package through its workspace build script. +function runBuild(packageName, dryRun) { + const bin = 'yarn'; + const args = ['workspace', packageName, 'run', 'build']; + if (dryRun) { + console.log(`[dry-run] ${[bin, ...args].join(' ')}`); + return; + } + run(bin, args, { capture: false }); +} + +// Publish a workspace package to npm under a specific dist-tag. +// Provenance is disabled when running outside CI because Yarn's +// npmPublishProvenance setting only works in GitHub Actions / GitLab CI. +function publishWorkspace(packageName, tag, { dryRun, otp } = {}) { + const command = [ + 'workspace', + packageName, + 'npm', + 'publish', + '--access=public', + '--tag', + tag, + ...(otp ? ['--otp', otp] : []), + ]; + + if (dryRun) { + console.log(`[dry-run] yarn ${command.join(' ')}`); + return; + } + + // Ensure we publish to the npmjs registry (Yarn defaults to the + // read-only yarnpkg.com mirror). CI sets this via setup-node's .npmrc + // but local runs need the explicit override. + const envOverrides = { + YARN_NPM_PUBLISH_REGISTRY: 'https://registry.npmjs.org', + }; + + // Provenance only works in GitHub Actions / GitLab CI; disable it + // locally to avoid the YN0091 error. + const inCI = Boolean(process.env.CI); + if (!inCI) { + envOverrides.YARN_NPM_PUBLISH_PROVENANCE = 'false'; + } + + const savedEnv = {}; + for (const [key, value] of Object.entries(envOverrides)) { + savedEnv[key] = process.env[key]; + process.env[key] = value; + } + try { + run('yarn', command, { capture: false }); + } finally { + for (const [key] of Object.entries(envOverrides)) { + if (savedEnv[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = savedEnv[key]; + } + } + } +} + +// Confirm npm registry sees the expected published version. +function verifyPublishedVersion(packageName, version, dryRun) { + if (dryRun) { + return; + } + + let lastError = null; + for (let attempt = 1; attempt <= VERIFY_ATTEMPTS; attempt += 1) { + try { + const published = run( + 'npm', + ['view', `${packageName}@${version}`, 'version'], + { silent: true }, + ); + if (published === version) { + return; + } + + lastError = new Error( + `Expected ${packageName}@${version}, but npm returned version "${published}".`, + ); + } catch (error) { + lastError = error; + } + + if (attempt < VERIFY_ATTEMPTS) { + console.log( + `Waiting for npm metadata propagation (${attempt}/${VERIFY_ATTEMPTS}) for ${packageName}@${version}...`, + ); + sleep(VERIFY_DELAY_MS); + } + } + + throw new Error( + `Publish verification failed for ${packageName}@${version} after ${VERIFY_ATTEMPTS} attempts.\n${ + lastError instanceof Error ? lastError.message : String(lastError) + }`, + ); +} + +// Block synchronously for simple retry backoff. +function sleep(ms) { + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); +} + +// Restore all files modified by this script. +function restoreBackups(backups) { + for (const [path, contents] of backups.entries()) { + writeFileSync(path, contents); + } +} + +// Print the release execution plan before running. +function printPlan(depReleaseOrder, depVersionPlan, releaseRnSdk, rnVersion) { + console.log('Release plan'); + console.log('------------'); + if (depReleaseOrder.length === 0) { + console.log('- No dependency prereleases needed.'); + } else { + for (const depName of depReleaseOrder) { + console.log(`- ${depName} -> ${depVersionPlan.get(depName)}`); + } + } + + if (releaseRnSdk) { + console.log(`- ${RN_SDK_NAME} -> ${rnVersion}`); + } else { + console.log(`- ${RN_SDK_NAME} -> skipped (no relevant changes)`); + } + console.log(''); +} + +// Print a short result summary for operators. +function printSummary( + publishedVersions, + pinnedRnVersions, + options, + workspaceInfoByName, + releaseScope, +) { + console.log('Release summary'); + console.log('---------------'); + const action = options.dryRun ? 'Planned' : 'Published'; + const preposition = options.dryRun ? 'with' : 'using'; + + if (publishedVersions.size === 0) { + console.log(`- No packages ${options.dryRun ? 'planned' : 'published'}.`); + } else { + for (const [packageName, version] of publishedVersions.entries()) { + console.log( + `- ${action} ${packageName}@${version} ${preposition} --tag ${options.tag}`, + ); + } + } + + if (pinnedRnVersions) { + console.log('- RN SDK dependency pins used for publish:'); + for (const [packageName, version] of pinnedRnVersions.entries()) { + console.log(` - ${packageName}: ${version}`); + } + } + + // Warn when a published package is a peerDep of an unpublished sibling — + // consumers may hit peer dependency warnings. + for (const publishedName of publishedVersions.keys()) { + for (const siblingName of releaseScope) { + if (publishedVersions.has(siblingName)) continue; + const siblingManifest = + workspaceInfoByName.get(siblingName)?.manifest ?? {}; + const peerDeps = siblingManifest.peerDependencies ?? {}; + if (Object.prototype.hasOwnProperty.call(peerDeps, publishedName)) { + console.log( + `- Warning: ${publishedName} was prereleased without prereleasing ${siblingName}; peer dependency warnings are possible for beta consumers.`, + ); + } + } + } +} + +// Orchestrate dependency and RN SDK prereleases. +async function main() { + const options = parseArgs(process.argv.slice(2)); + const backups = new Map(); + + let pinnedRnVersions = null; + const publishedVersions = new Map(); + + try { + if (!options.dryRun) { + ensureCleanWorkingTree(); + await ensureNpmAuth(options); + } + ensureGitRefExists(options.baseRef); + + const workspaceInfoByName = getWorkspaceInfo(); + const rnWorkspaceDeps = getDirectWorkspaceDependencies( + workspaceInfoByName, + RN_SDK_NAME, + ); + const releaseScope = [...rnWorkspaceDeps, RN_SDK_NAME]; + const changedPackages = getChangedPackages( + options.baseRef, + workspaceInfoByName, + releaseScope, + ); + + const changedDeps = rnWorkspaceDeps.filter((name) => + changedPackages.has(name), + ); + + // When a dependency is prereleased, any sibling in the release scope that + // declares it as a peerDependency must also be prereleased. Otherwise + // consumers would hit peer-dependency warnings because the stable + // peerDependency range won't match the beta version. + // + // Note: propagation is one level deep (snapshot-based iteration over the + // initial changedDeps list). This is sufficient for the current dependency + // graph. If transitive peer dep chains are ever introduced, this would + // need a fixed-point loop. + for (const changedDep of [...changedDeps]) { + for (const siblingName of rnWorkspaceDeps) { + if (changedDeps.includes(siblingName)) continue; + const siblingManifest = + workspaceInfoByName.get(siblingName)?.manifest ?? {}; + const peerDeps = siblingManifest.peerDependencies ?? {}; + if (Object.prototype.hasOwnProperty.call(peerDeps, changedDep)) { + changedDeps.push(siblingName); + } + } + } + + const rnChanged = changedPackages.has(RN_SDK_NAME); + const releaseRnSdk = + rnChanged || changedDeps.length > 0 || options.allowEmpty; + + if (!releaseRnSdk) { + console.log( + `No changes detected in ${RN_SDK_NAME} or its workspace dependencies since ${options.baseRef}.`, + ); + console.log( + 'Nothing to release. Use --allow-empty to force a beta publish.', + ); + return; + } + + const graph = buildDependencyGraph(workspaceInfoByName, releaseScope); + const depReleaseOrder = topoSort(changedDeps, graph); + + const depVersionPlan = new Map(); + for (const depName of depReleaseOrder) { + const currentVersion = workspaceInfoByName.get(depName).manifest.version; + depVersionPlan.set( + depName, + nextPrereleaseVersion(depName, currentVersion, options.tag), + ); + } + + const rnCurrentVersion = + workspaceInfoByName.get(RN_SDK_NAME).manifest.version; + const rnNextVersion = nextPrereleaseVersion( + RN_SDK_NAME, + rnCurrentVersion, + options.tag, + ); + + printPlan(depReleaseOrder, depVersionPlan, releaseRnSdk, rnNextVersion); + + for (const depName of depReleaseOrder) { + const workspaceInfo = workspaceInfoByName.get(depName); + const depNextVersion = depVersionPlan.get(depName); + + if (!options.dryRun) { + const nextManifest = pinWorkspaceInternalDeps( + workspaceInfo, + workspaceInfoByName, + publishedVersions, + ); + nextManifest.version = depNextVersion; + backupAndWriteManifest(workspaceInfo, nextManifest, backups); + } + runBuild(depName, options.dryRun); + publishWorkspace(depName, options.tag, options); + verifyPublishedVersion(depName, depNextVersion, options.dryRun); + + publishedVersions.set(depName, depNextVersion); + } + + if (releaseRnSdk) { + const rnInfo = workspaceInfoByName.get(RN_SDK_NAME); + pinnedRnVersions = new Map(); + for (const depName of rnWorkspaceDeps) { + pinnedRnVersions.set( + depName, + getLiveVersion(workspaceInfoByName, publishedVersions, depName), + ); + } + + if (!options.dryRun) { + const rnManifest = pinReactNativeSdkDeps( + rnInfo, + rnWorkspaceDeps, + pinnedRnVersions, + ); + rnManifest.version = rnNextVersion; + backupAndWriteManifest(rnInfo, rnManifest, backups); + backupFile( + resolve(process.cwd(), 'packages/react-native-sdk/src/version.ts'), + backups, + ); + } + runBuild(RN_SDK_NAME, options.dryRun); + publishWorkspace(RN_SDK_NAME, options.tag, options); + verifyPublishedVersion(RN_SDK_NAME, rnNextVersion, options.dryRun); + + publishedVersions.set(RN_SDK_NAME, rnNextVersion); + } + + printSummary( + publishedVersions, + pinnedRnVersions, + options, + workspaceInfoByName, + releaseScope, + ); + } finally { + if (!options.keepChanges && backups.size > 0) { + restoreBackups(backups); + console.log('\nRestored local package.json changes.'); + } + } +} + +main(); diff --git a/yarn.lock b/yarn.lock index 9c06275581..398d44d05f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,15 +5,15 @@ __metadata: version: 8 cacheKey: 10 -"@0no-co/graphql.web@npm:^1.0.5, @0no-co/graphql.web@npm:^1.0.8": - version: 1.0.12 - resolution: "@0no-co/graphql.web@npm:1.0.12" +"@0no-co/graphql.web@npm:^1.0.13, @0no-co/graphql.web@npm:^1.0.8": + version: 1.2.0 + resolution: "@0no-co/graphql.web@npm:1.2.0" peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 peerDependenciesMeta: graphql: optional: true - checksum: 10/153327b2315d5f46504888888dc67b23995f5f38ab8e56abdcdb203fedac1cf4600828004ea59c9694e4a3fa7d96e4cf8c857a3850e81af841d17b9499469e96 + checksum: 10/bb53b2e013686df0c8ca518430e9371bd14bd26910c1ab5b7bebd76cea1867ba6160d7e01924a04af846e90d99cb8f101f35960f89a76a8a91ce1d70f74d321d languageName: node linkType: hard @@ -59,6 +59,22 @@ __metadata: languageName: node linkType: hard +"@ark/schema@npm:0.56.0": + version: 0.56.0 + resolution: "@ark/schema@npm:0.56.0" + dependencies: + "@ark/util": "npm:0.56.0" + checksum: 10/59d9007ab43ee41814bf3c91acf44fbcff82086eb2b8aa7715ec388f6897ebb6f3ef56ba6b7815fae8d4db702e6a6a3e5897cd48a50112caa3784df0ed4fd7e4 + languageName: node + linkType: hard + +"@ark/util@npm:0.56.0": + version: 0.56.0 + resolution: "@ark/util@npm:0.56.0" + checksum: 10/eb3477c6c1c126e708885721dd518d55dac133d642a916535ef0d75403182407cb36729d954181db82a6388b5e71cdeb486ea72dbfa3a58542f8d0435b79fb35 + languageName: node + linkType: hard + "@babel/cli@npm:^7.23.4": version: 7.26.4 resolution: "@babel/cli@npm:7.26.4" @@ -86,27 +102,7 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:7.10.4, @babel/code-frame@npm:~7.10.4": - version: 7.10.4 - resolution: "@babel/code-frame@npm:7.10.4" - dependencies: - "@babel/highlight": "npm:^7.10.4" - checksum: 10/4ef9c679515be9cb8eab519fcded953f86226155a599cf7ea209e40e088bb9a51bb5893d3307eae510b07bb3e359d64f2620957a00c27825dbe26ac62aca81f5 - languageName: node - linkType: hard - -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.20.0, @babel/code-frame@npm:^7.21.4, @babel/code-frame@npm:^7.24.7, @babel/code-frame@npm:^7.25.7, @babel/code-frame@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/code-frame@npm:7.27.1" - dependencies: - "@babel/helper-validator-identifier": "npm:^7.27.1" - js-tokens: "npm:^4.0.0" - picocolors: "npm:^1.1.1" - checksum: 10/721b8a6e360a1fa0f1c9fe7351ae6c874828e119183688b533c477aa378f1010f37cc9afbfc4722c686d1f5cdd00da02eab4ba7278a0c504fa0d7a321dcd4fdf - languageName: node - linkType: hard - -"@babel/code-frame@npm:^7.28.6, @babel/code-frame@npm:^7.29.0": +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.20.0, @babel/code-frame@npm:^7.21.4, @babel/code-frame@npm:^7.24.7, @babel/code-frame@npm:^7.27.1, @babel/code-frame@npm:^7.28.6, @babel/code-frame@npm:^7.29.0": version: 7.29.0 resolution: "@babel/code-frame@npm:7.29.0" dependencies: @@ -117,10 +113,19 @@ __metadata: languageName: node linkType: hard -"@babel/compat-data@npm:^7.27.2, @babel/compat-data@npm:^7.27.7, @babel/compat-data@npm:^7.28.0": - version: 7.28.4 - resolution: "@babel/compat-data@npm:7.28.4" - checksum: 10/95b7864e6b210c84c069743966da448c0cb50015a4de5e18dd755776a0b5e53c4653e74f26700aed8de922eaa3b8844fc5fc5b29bc64830249d2abe914aec832 +"@babel/code-frame@npm:~7.10.4": + version: 7.10.4 + resolution: "@babel/code-frame@npm:7.10.4" + dependencies: + "@babel/highlight": "npm:^7.10.4" + checksum: 10/4ef9c679515be9cb8eab519fcded953f86226155a599cf7ea209e40e088bb9a51bb5893d3307eae510b07bb3e359d64f2620957a00c27825dbe26ac62aca81f5 + languageName: node + linkType: hard + +"@babel/compat-data@npm:^7.28.0, @babel/compat-data@npm:^7.28.6": + version: 7.29.0 + resolution: "@babel/compat-data@npm:7.29.0" + checksum: 10/7f21beedb930ed8fbf7eabafc60e6e6521c1d905646bf1317a61b2163339157fe797efeb85962bf55136e166b01fd1a6b526a15974b92a8b877d564dcb6c9580 languageName: node linkType: hard @@ -147,20 +152,7 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.20.0, @babel/generator@npm:^7.20.5, @babel/generator@npm:^7.25.0, @babel/generator@npm:^7.25.7, @babel/generator@npm:^7.28.5, @babel/generator@npm:^7.7.2": - version: 7.28.5 - resolution: "@babel/generator@npm:7.28.5" - dependencies: - "@babel/parser": "npm:^7.28.5" - "@babel/types": "npm:^7.28.5" - "@jridgewell/gen-mapping": "npm:^0.3.12" - "@jridgewell/trace-mapping": "npm:^0.3.28" - jsesc: "npm:^3.0.2" - checksum: 10/ae618f0a17a6d76c3983e1fd5d9c2f5fdc07703a119efdb813a7d9b8ad4be0a07d4c6f0d718440d2de01a68e321f64e2d63c77fc5d43ae47ae143746ef28ac1f - languageName: node - linkType: hard - -"@babel/generator@npm:^7.29.0": +"@babel/generator@npm:^7.20.0, @babel/generator@npm:^7.20.5, @babel/generator@npm:^7.25.0, @babel/generator@npm:^7.28.5, @babel/generator@npm:^7.29.0, @babel/generator@npm:^7.29.1, @babel/generator@npm:^7.7.2": version: 7.29.1 resolution: "@babel/generator@npm:7.29.1" dependencies: @@ -182,37 +174,20 @@ __metadata: languageName: node linkType: hard -"@babel/helper-compilation-targets@npm:^7.27.1, @babel/helper-compilation-targets@npm:^7.27.2": - version: 7.27.2 - resolution: "@babel/helper-compilation-targets@npm:7.27.2" +"@babel/helper-compilation-targets@npm:^7.27.1, @babel/helper-compilation-targets@npm:^7.27.2, @babel/helper-compilation-targets@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/helper-compilation-targets@npm:7.28.6" dependencies: - "@babel/compat-data": "npm:^7.27.2" + "@babel/compat-data": "npm:^7.28.6" "@babel/helper-validator-option": "npm:^7.27.1" browserslist: "npm:^4.24.0" lru-cache: "npm:^5.1.1" semver: "npm:^6.3.1" - checksum: 10/bd53c30a7477049db04b655d11f4c3500aea3bcbc2497cf02161de2ecf994fec7c098aabbcebe210ffabc2ecbdb1e3ffad23fb4d3f18723b814f423ea1749fe8 + checksum: 10/f512a5aeee4dfc6ea8807f521d085fdca8d66a7d068a6dd5e5b37da10a6081d648c0bbf66791a081e4e8e6556758da44831b331540965dfbf4f5275f3d0a8788 languageName: node linkType: hard -"@babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.22.10, @babel/helper-create-class-features-plugin@npm:^7.25.9, @babel/helper-create-class-features-plugin@npm:^7.27.1, @babel/helper-create-class-features-plugin@npm:^7.28.3": - version: 7.28.3 - resolution: "@babel/helper-create-class-features-plugin@npm:7.28.3" - dependencies: - "@babel/helper-annotate-as-pure": "npm:^7.27.3" - "@babel/helper-member-expression-to-functions": "npm:^7.27.1" - "@babel/helper-optimise-call-expression": "npm:^7.27.1" - "@babel/helper-replace-supers": "npm:^7.27.1" - "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.27.1" - "@babel/traverse": "npm:^7.28.3" - semver: "npm:^6.3.1" - peerDependencies: - "@babel/core": ^7.0.0 - checksum: 10/32d01bdd601b4d129b1d510058a19644abc764badcc543adaec9e71443e874ef252783cceb2809645bdf0e92b07f206fd439c75a2a48cf702c627aba7f3ee34a - languageName: node - linkType: hard - -"@babel/helper-create-class-features-plugin@npm:^7.28.6": +"@babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.22.10, @babel/helper-create-class-features-plugin@npm:^7.27.1, @babel/helper-create-class-features-plugin@npm:^7.28.6": version: 7.28.6 resolution: "@babel/helper-create-class-features-plugin@npm:7.28.6" dependencies: @@ -229,31 +204,31 @@ __metadata: languageName: node linkType: hard -"@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/helper-create-regexp-features-plugin@npm:7.27.1" +"@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.27.1, @babel/helper-create-regexp-features-plugin@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/helper-create-regexp-features-plugin@npm:7.28.5" dependencies: - "@babel/helper-annotate-as-pure": "npm:^7.27.1" - regexpu-core: "npm:^6.2.0" + "@babel/helper-annotate-as-pure": "npm:^7.27.3" + regexpu-core: "npm:^6.3.1" semver: "npm:^6.3.1" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/dea272628cd8874f127ab7b2ee468620aabc1383d38bb40c49a9c7667db2258cdfe6620a1d1412f5f0706583f6301b4b7ad3d5932f24df7fe72e66bf9bc0be45 + checksum: 10/d8791350fe0479af0909aa5efb6dfd3bacda743c7c3f8fa1b0bb18fe014c206505834102ee24382df1cfe5a83b4e4083220e97f420a48b2cec15bb1ad6c7c9d3 languageName: node linkType: hard -"@babel/helper-define-polyfill-provider@npm:^0.6.2, @babel/helper-define-polyfill-provider@npm:^0.6.5": - version: 0.6.5 - resolution: "@babel/helper-define-polyfill-provider@npm:0.6.5" +"@babel/helper-define-polyfill-provider@npm:^0.6.2, @babel/helper-define-polyfill-provider@npm:^0.6.5, @babel/helper-define-polyfill-provider@npm:^0.6.8": + version: 0.6.8 + resolution: "@babel/helper-define-polyfill-provider@npm:0.6.8" dependencies: - "@babel/helper-compilation-targets": "npm:^7.27.2" - "@babel/helper-plugin-utils": "npm:^7.27.1" - debug: "npm:^4.4.1" + "@babel/helper-compilation-targets": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" + debug: "npm:^4.4.3" lodash.debounce: "npm:^4.0.8" - resolve: "npm:^1.22.10" + resolve: "npm:^1.22.11" peerDependencies: "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 10/0bdd2d9654d2f650c33976caa1a2afac2c23cf07e83856acdb482423c7bf4542c499ca0bdc723f2961bb36883501f09e9f4fe061ba81c07996daacfba82a6f62 + checksum: 10/a6f9fbb82578464da35eec88c7f3e70bdd95237bfc1d3ebb9cf4536a86a577b7c6e587f9a6797b01ee08629599ee2bc6fdab39e99de505751a30d9b4877202ab languageName: node linkType: hard @@ -264,16 +239,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-member-expression-to-functions@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/helper-member-expression-to-functions@npm:7.27.1" - dependencies: - "@babel/traverse": "npm:^7.27.1" - "@babel/types": "npm:^7.27.1" - checksum: 10/533a5a2cf1c9a8770d241b86d5f124c88e953c831a359faf1ac7ba1e632749c1748281b83295d227fe6035b202d81f3d3a1ea13891f150c6538e040668d6126a - languageName: node - linkType: hard - "@babel/helper-member-expression-to-functions@npm:^7.28.5": version: 7.28.5 resolution: "@babel/helper-member-expression-to-functions@npm:7.28.5" @@ -284,26 +249,26 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.25.9, @babel/helper-module-imports@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/helper-module-imports@npm:7.27.1" +"@babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.25.9, @babel/helper-module-imports@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/helper-module-imports@npm:7.28.6" dependencies: - "@babel/traverse": "npm:^7.27.1" - "@babel/types": "npm:^7.27.1" - checksum: 10/58e792ea5d4ae71676e0d03d9fef33e886a09602addc3bd01388a98d87df9fcfd192968feb40ac4aedb7e287ec3d0c17b33e3ecefe002592041a91d8a1998a8d + "@babel/traverse": "npm:^7.28.6" + "@babel/types": "npm:^7.28.6" + checksum: 10/64b1380d74425566a3c288074d7ce4dea56d775d2d3325a3d4a6df1dca702916c1d268133b6f385de9ba5b822b3c6e2af5d3b11ac88e5453d5698d77264f0ec0 languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.27.1, @babel/helper-module-transforms@npm:^7.28.3": - version: 7.28.3 - resolution: "@babel/helper-module-transforms@npm:7.28.3" +"@babel/helper-module-transforms@npm:^7.27.1, @babel/helper-module-transforms@npm:^7.28.3, @babel/helper-module-transforms@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/helper-module-transforms@npm:7.28.6" dependencies: - "@babel/helper-module-imports": "npm:^7.27.1" - "@babel/helper-validator-identifier": "npm:^7.27.1" - "@babel/traverse": "npm:^7.28.3" + "@babel/helper-module-imports": "npm:^7.28.6" + "@babel/helper-validator-identifier": "npm:^7.28.5" + "@babel/traverse": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/598fdd8aa5b91f08542d0ba62a737847d0e752c8b95ae2566bc9d11d371856d6867d93e50db870fb836a6c44cfe481c189d8a2b35ca025a224f070624be9fa87 + checksum: 10/2e421c7db743249819ee51e83054952709dc2e197c7d5d415b4bdddc718580195704bfcdf38544b3f674efc2eccd4d29a65d38678fc827ed3934a7690984cd8b languageName: node linkType: hard @@ -316,14 +281,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.25.9, @babel/helper-plugin-utils@npm:^7.26.5, @babel/helper-plugin-utils@npm:^7.27.1, @babel/helper-plugin-utils@npm:^7.8.0": - version: 7.27.1 - resolution: "@babel/helper-plugin-utils@npm:7.27.1" - checksum: 10/96136c2428888e620e2ec493c25888f9ceb4a21099dcf3dd4508ea64b58cdedbd5a9fb6c7b352546de84d6c24edafe482318646932a22c449ebd16d16c22d864 - languageName: node - linkType: hard - -"@babel/helper-plugin-utils@npm:^7.28.6": +"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.25.9, @babel/helper-plugin-utils@npm:^7.27.1, @babel/helper-plugin-utils@npm:^7.28.6, @babel/helper-plugin-utils@npm:^7.8.0": version: 7.28.6 resolution: "@babel/helper-plugin-utils@npm:7.28.6" checksum: 10/21c853bbc13dbdddf03309c9a0477270124ad48989e1ad6524b83e83a77524b333f92edd2caae645c5a7ecf264ec6d04a9ebe15aeb54c7f33c037b71ec521e4a @@ -343,20 +301,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-replace-supers@npm:^7.22.9, @babel/helper-replace-supers@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/helper-replace-supers@npm:7.27.1" - dependencies: - "@babel/helper-member-expression-to-functions": "npm:^7.27.1" - "@babel/helper-optimise-call-expression": "npm:^7.27.1" - "@babel/traverse": "npm:^7.27.1" - peerDependencies: - "@babel/core": ^7.0.0 - checksum: 10/72e3f8bef744c06874206bf0d80a0abbedbda269586966511c2491df4f6bf6d47a94700810c7a6737345a545dfb8295222e1e72f506bcd0b40edb3f594f739ea - languageName: node - linkType: hard - -"@babel/helper-replace-supers@npm:^7.28.6": +"@babel/helper-replace-supers@npm:^7.22.9, @babel/helper-replace-supers@npm:^7.27.1, @babel/helper-replace-supers@npm:^7.28.6": version: 7.28.6 resolution: "@babel/helper-replace-supers@npm:7.28.6" dependencies: @@ -369,7 +314,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-skip-transparent-expression-wrappers@npm:^7.25.9, @babel/helper-skip-transparent-expression-wrappers@npm:^7.27.1": +"@babel/helper-skip-transparent-expression-wrappers@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.27.1" dependencies: @@ -442,18 +387,7 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.0, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.24.4, @babel/parser@npm:^7.24.7, @babel/parser@npm:^7.25.3, @babel/parser@npm:^7.25.4, @babel/parser@npm:^7.25.7, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.5": - version: 7.28.5 - resolution: "@babel/parser@npm:7.28.5" - dependencies: - "@babel/types": "npm:^7.28.5" - bin: - parser: ./bin/babel-parser.js - checksum: 10/8d9bfb437af6c97a7f6351840b9ac06b4529ba79d6d3def24d6c2996ab38ff7f1f9d301e868ca84a93a3050fadb3d09dbc5105b24634cd281671ac11eebe8df7 - languageName: node - linkType: hard - -"@babel/parser@npm:^7.28.6, @babel/parser@npm:^7.29.0": +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.0, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.24.4, @babel/parser@npm:^7.24.7, @babel/parser@npm:^7.25.3, @babel/parser@npm:^7.25.4, @babel/parser@npm:^7.28.5, @babel/parser@npm:^7.28.6, @babel/parser@npm:^7.29.0": version: 7.29.0 resolution: "@babel/parser@npm:7.29.0" dependencies: @@ -648,14 +582,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-flow@npm:^7.12.1, @babel/plugin-syntax-flow@npm:^7.26.0": - version: 7.26.0 - resolution: "@babel/plugin-syntax-flow@npm:7.26.0" +"@babel/plugin-syntax-flow@npm:^7.12.1, @babel/plugin-syntax-flow@npm:^7.27.1": + version: 7.28.6 + resolution: "@babel/plugin-syntax-flow@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/fdc0d0a7b512e00d933e12cf93c785ea4645a193f4b539230b7601cfaa8c704410199318ce9ea14e5fca7d13e9027822f7d81a7871d3e854df26b6af04cc3c6c + checksum: 10/3dfe5d8168e400376e16937c92648142771b9ba0d9937b04ccdaacd06bf9d854170021b466106d4aa39ba6062b8b5b9b53efddae2c64ca133d4d6fafaa472909 languageName: node linkType: hard @@ -703,18 +637,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-jsx@npm:^7.25.9, @babel/plugin-syntax-jsx@npm:^7.7.2": - version: 7.25.9 - resolution: "@babel/plugin-syntax-jsx@npm:7.25.9" - dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 10/bb609d1ffb50b58f0c1bac8810d0e46a4f6c922aa171c458f3a19d66ee545d36e782d3bffbbc1fed0dc65a558bdce1caf5279316583c0fff5a2c1658982a8563 - languageName: node - linkType: hard - -"@babel/plugin-syntax-jsx@npm:^7.27.1": +"@babel/plugin-syntax-jsx@npm:^7.27.1, @babel/plugin-syntax-jsx@npm:^7.28.6, @babel/plugin-syntax-jsx@npm:^7.7.2": version: 7.28.6 resolution: "@babel/plugin-syntax-jsx@npm:7.28.6" dependencies: @@ -802,18 +725,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-typescript@npm:^7.25.9, @babel/plugin-syntax-typescript@npm:^7.3.3, @babel/plugin-syntax-typescript@npm:^7.7.2": - version: 7.25.9 - resolution: "@babel/plugin-syntax-typescript@npm:7.25.9" - dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 10/0e9821e8ba7d660c36c919654e4144a70546942ae184e85b8102f2322451eae102cbfadbcadd52ce077a2b44b400ee52394c616feab7b5b9f791b910e933fd33 - languageName: node - linkType: hard - -"@babel/plugin-syntax-typescript@npm:^7.28.6": +"@babel/plugin-syntax-typescript@npm:^7.28.6, @babel/plugin-syntax-typescript@npm:^7.3.3, @babel/plugin-syntax-typescript@npm:^7.7.2": version: 7.28.6 resolution: "@babel/plugin-syntax-typescript@npm:7.28.6" dependencies: @@ -848,28 +760,28 @@ __metadata: linkType: hard "@babel/plugin-transform-async-generator-functions@npm:^7.25.4, @babel/plugin-transform-async-generator-functions@npm:^7.28.0": - version: 7.28.0 - resolution: "@babel/plugin-transform-async-generator-functions@npm:7.28.0" + version: 7.29.0 + resolution: "@babel/plugin-transform-async-generator-functions@npm:7.29.0" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" "@babel/helper-remap-async-to-generator": "npm:^7.27.1" - "@babel/traverse": "npm:^7.28.0" + "@babel/traverse": "npm:^7.29.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/8ad31b9969b203dec572738a872e17b14ef76ca45b4ef5ffa76f3514be417ca233d1a0978e5f8de166412a8a745619eb22b07cc5df96f5ebad8ca500f920f61b + checksum: 10/e2c064a5eb212cbdf14f7c0113e069b845ca0f0ba431c1cc04607d3fc4f3bf1ed70f5c375fe7c61338a45db88bc1a79d270c8d633ce12256e1fce3666c1e6b93 languageName: node linkType: hard "@babel/plugin-transform-async-to-generator@npm:^7.24.7, @babel/plugin-transform-async-to-generator@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-async-to-generator@npm:7.27.1" + version: 7.28.6 + resolution: "@babel/plugin-transform-async-to-generator@npm:7.28.6" dependencies: - "@babel/helper-module-imports": "npm:^7.27.1" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-module-imports": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" "@babel/helper-remap-async-to-generator": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/d79d7a7ae7d416f6a48200017d027a6ba94c09c7617eea8b4e9c803630f00094c1a4fc32bf20ce3282567824ce3fcbda51653aac4003c71ea4e681b331338979 + checksum: 10/bca5774263ec01dd2bf71c74bbaf7baa183bf03576636b7826c3346be70c8c8cb15cff549112f2983c36885131a0afde6c443591278c281f733ee17f455aa9b1 languageName: node linkType: hard @@ -885,17 +797,17 @@ __metadata: linkType: hard "@babel/plugin-transform-block-scoping@npm:^7.25.0, @babel/plugin-transform-block-scoping@npm:^7.28.0": - version: 7.28.4 - resolution: "@babel/plugin-transform-block-scoping@npm:7.28.4" + version: 7.28.6 + resolution: "@babel/plugin-transform-block-scoping@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/0848c681b0229ebb98da8a1fab53a29a94f79c4b80e536cb00dcedc08ca29341a48ebdf34d846f4d738376aa8e36830fa7f444bae3e85c8761cab96e9ad72a0f + checksum: 10/7ab8a0856024a5360ba16c3569b739385e939bc5a15ad7d811bec8459361a9aa5ee7c5f154a4e2ce79f5d66779c19464e7532600c31a1b6f681db4eb7e1c7bde languageName: node linkType: hard -"@babel/plugin-transform-class-properties@npm:7.27.1, @babel/plugin-transform-class-properties@npm:^7.0.0-0, @babel/plugin-transform-class-properties@npm:^7.24.7, @babel/plugin-transform-class-properties@npm:^7.25.4, @babel/plugin-transform-class-properties@npm:^7.27.1": +"@babel/plugin-transform-class-properties@npm:7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-class-properties@npm:7.27.1" dependencies: @@ -907,19 +819,31 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-class-properties@npm:^7.0.0-0, @babel/plugin-transform-class-properties@npm:^7.24.7, @babel/plugin-transform-class-properties@npm:^7.25.4, @babel/plugin-transform-class-properties@npm:^7.27.1": + version: 7.28.6 + resolution: "@babel/plugin-transform-class-properties@npm:7.28.6" + dependencies: + "@babel/helper-create-class-features-plugin": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/200f30d44b36a768fa3a8cf690db9e333996af2ad14d9fa1b4c91a427ed9302907873b219b4ce87517ca1014a810eb2e929a6a66be68473f72b546fc64d04fbc + languageName: node + linkType: hard + "@babel/plugin-transform-class-static-block@npm:^7.27.1, @babel/plugin-transform-class-static-block@npm:^7.28.3": - version: 7.28.3 - resolution: "@babel/plugin-transform-class-static-block@npm:7.28.3" + version: 7.28.6 + resolution: "@babel/plugin-transform-class-static-block@npm:7.28.6" dependencies: - "@babel/helper-create-class-features-plugin": "npm:^7.28.3" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-create-class-features-plugin": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.12.0 - checksum: 10/c0ba8f0cbf3699287e5a711907dab3b29f346d9c107faa4e424aa26252e45845d74ca08ee6245bfccf32a8c04bc1d07a89b635e51522592c6044b810a48d3f58 + checksum: 10/bea7836846deefd02d9976ad1b30b5ade0d6329ecd92866db789dcf6aacfaf900b7a77031e25680f8de5ad636a771a5bdca8961361e6218d45d538ec5d9b71cc languageName: node linkType: hard -"@babel/plugin-transform-classes@npm:7.28.4, @babel/plugin-transform-classes@npm:^7.0.0-0, @babel/plugin-transform-classes@npm:^7.25.4, @babel/plugin-transform-classes@npm:^7.28.3": +"@babel/plugin-transform-classes@npm:7.28.4": version: 7.28.4 resolution: "@babel/plugin-transform-classes@npm:7.28.4" dependencies: @@ -935,27 +859,43 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-classes@npm:^7.0.0-0, @babel/plugin-transform-classes@npm:^7.25.4, @babel/plugin-transform-classes@npm:^7.28.3": + version: 7.28.6 + resolution: "@babel/plugin-transform-classes@npm:7.28.6" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.27.3" + "@babel/helper-compilation-targets": "npm:^7.28.6" + "@babel/helper-globals": "npm:^7.28.0" + "@babel/helper-plugin-utils": "npm:^7.28.6" + "@babel/helper-replace-supers": "npm:^7.28.6" + "@babel/traverse": "npm:^7.28.6" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/9c3278a314d1c4bcda792bb22aced20e30c735557daf9bcc56397c0f3eb54761b21c770219e4581036a10dabda3e597321ed093bc245d5f4d561e19ceff66a6d + languageName: node + linkType: hard + "@babel/plugin-transform-computed-properties@npm:^7.24.7, @babel/plugin-transform-computed-properties@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-computed-properties@npm:7.27.1" + version: 7.28.6 + resolution: "@babel/plugin-transform-computed-properties@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" - "@babel/template": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" + "@babel/template": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/101f6d4575447070943d5a9efaa5bea8c552ea3083d73a9612f1a16d38b0a0a7b79a5feb65c6cc4e4fcabf28e85a570b97ccd3294da966e8fbbb6dfb97220eda + checksum: 10/4a5e270f7e1f1e9787cf7cf133d48e3c1e38eb935d29a90331a1324d7c720f589b7b626b2e6485cd5521a7a13f2dbdc89a3e46ecbe7213d5bbb631175267c4aa languageName: node linkType: hard -"@babel/plugin-transform-destructuring@npm:^7.24.8, @babel/plugin-transform-destructuring@npm:^7.28.0": - version: 7.28.0 - resolution: "@babel/plugin-transform-destructuring@npm:7.28.0" +"@babel/plugin-transform-destructuring@npm:^7.24.8, @babel/plugin-transform-destructuring@npm:^7.28.0, @babel/plugin-transform-destructuring@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/plugin-transform-destructuring@npm:7.28.5" dependencies: "@babel/helper-plugin-utils": "npm:^7.27.1" - "@babel/traverse": "npm:^7.28.0" + "@babel/traverse": "npm:^7.28.5" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/cddab2520ff32d18005670fc6646396a253d3811d1ccc49f6f858469f05985ee896c346a0cb34d1cf25155c9be76d1068ff878cf8e8459bd3fa27513ec5a6802 + checksum: 10/9cc67d3377bc5d8063599f2eb4588f5f9a8ab3abc9b64a40c24501fb3c1f91f4d5cf281ea9f208fd6b2ef8d9d8b018dacf1bed9493334577c966cd32370a7036 languageName: node linkType: hard @@ -1039,15 +979,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-flow-strip-types@npm:^7.25.2, @babel/plugin-transform-flow-strip-types@npm:^7.25.9": - version: 7.26.5 - resolution: "@babel/plugin-transform-flow-strip-types@npm:7.26.5" +"@babel/plugin-transform-flow-strip-types@npm:^7.25.2, @babel/plugin-transform-flow-strip-types@npm:^7.25.9, @babel/plugin-transform-flow-strip-types@npm:^7.26.5": + version: 7.27.1 + resolution: "@babel/plugin-transform-flow-strip-types@npm:7.27.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.26.5" - "@babel/plugin-syntax-flow": "npm:^7.26.0" + "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/plugin-syntax-flow": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/01ffdf56f0cbf26d222311cd69be4e5997182dbe6fee217f241c8d67f5e5b115b70efa4acd27d850f0a242b0d36b062d255d763984416155d0237c3ee9e9b8ea + checksum: 10/22e260866b122b7d0c35f2c55b2d422b175606b4d14c9ba116b1fbe88e08cc8b024c1c41bb62527cfc5f7ccc0ed06c752e5945cb1ee22465a30aa5623e617940 languageName: node linkType: hard @@ -1099,13 +1039,13 @@ __metadata: linkType: hard "@babel/plugin-transform-logical-assignment-operators@npm:^7.24.7, @babel/plugin-transform-logical-assignment-operators@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.27.1" + version: 7.28.6 + resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/2757955d81d65cc4701c17b83720745f6858f7a1d1d58117e379c204f47adbeb066b778596b6168bdbf4a22c229aab595d79a9abc261d0c6bfd62d4419466e73 + checksum: 10/36095d5d1cfc680e95298b5389a16016da800ae3379b130dabf557e94652c47b06610407e9fa44aaa03e9b0a5aa7b4b93348123985d44a45e369bf5f3497d149 languageName: node linkType: hard @@ -1132,15 +1072,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-commonjs@npm:^7.24.7, @babel/plugin-transform-modules-commonjs@npm:^7.24.8, @babel/plugin-transform-modules-commonjs@npm:^7.25.9, @babel/plugin-transform-modules-commonjs@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-modules-commonjs@npm:7.27.1" +"@babel/plugin-transform-modules-commonjs@npm:^7.24.7, @babel/plugin-transform-modules-commonjs@npm:^7.24.8, @babel/plugin-transform-modules-commonjs@npm:^7.27.1": + version: 7.28.6 + resolution: "@babel/plugin-transform-modules-commonjs@npm:7.28.6" dependencies: - "@babel/helper-module-transforms": "npm:^7.27.1" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-module-transforms": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/9059243a977bc1f13e3dccfc6feb6508890e7c7bb191f7eb56626b20672b4b12338051ca835ab55426875a473181502c8f35b4df58ba251bef63b25866d995fe + checksum: 10/ec6ea2958e778a7e0220f4a75cb5816cecddc6bd98efa10499fff7baabaa29a594d50d787a4ebf8a8ba66fefcf76ca2ded602be0b4554ae3317e53b3b3375b37 languageName: node linkType: hard @@ -1171,14 +1111,14 @@ __metadata: linkType: hard "@babel/plugin-transform-named-capturing-groups-regex@npm:^7.24.7, @babel/plugin-transform-named-capturing-groups-regex@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-named-capturing-groups-regex@npm:7.27.1" + version: 7.29.0 + resolution: "@babel/plugin-transform-named-capturing-groups-regex@npm:7.29.0" dependencies: - "@babel/helper-create-regexp-features-plugin": "npm:^7.27.1" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-create-regexp-features-plugin": "npm:^7.28.5" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/a711c92d9753df26cefc1792481e5cbff4fe4f32b383d76b25e36fa865d8023b1b9aa6338cf18f5c0e864c71a7fbe8115e840872ccd61a914d9953849c68de7d + checksum: 10/ed8c27699ca82a6c01cbfd39f3de16b90cfea4f8146a358057f76df290d308a66a8bd2e6734e6a87f68c18576e15d2d70548a84cd474d26fdf256c3f5ae44d8c languageName: node linkType: hard @@ -1193,7 +1133,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-nullish-coalescing-operator@npm:7.27.1, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.0.0-0, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.24.7, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.27.1": +"@babel/plugin-transform-nullish-coalescing-operator@npm:7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.27.1" dependencies: @@ -1204,29 +1144,40 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.0.0-0, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.24.7, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.27.1": + version: 7.28.6 + resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.28.6" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.28.6" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/88106952ca4f4fea8f97222a25f9595c6859d458d76905845dfa54f54e7d345e3dc338932e8c84a9c57a6c88b2f6d9ebff47130ce508a49c2b6e6a9f03858750 + languageName: node + linkType: hard + "@babel/plugin-transform-numeric-separator@npm:^7.24.7, @babel/plugin-transform-numeric-separator@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-numeric-separator@npm:7.27.1" + version: 7.28.6 + resolution: "@babel/plugin-transform-numeric-separator@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/049b958911de86d32408cd78017940a207e49c054ae9534ab53a32a57122cc592c0aae3c166d6f29bd1a7d75cc779d71883582dd76cb28b2fbb493e842d8ffca + checksum: 10/4b5ca60e481e22f0842761a3badca17376a230b5a7e5482338604eb95836c2d0c9c9bde53bdc5c2de1c6a12ae6c12de7464d098bf74b0943f85905ca358f0b68 languageName: node linkType: hard "@babel/plugin-transform-object-rest-spread@npm:^7.24.7, @babel/plugin-transform-object-rest-spread@npm:^7.28.0": - version: 7.28.4 - resolution: "@babel/plugin-transform-object-rest-spread@npm:7.28.4" + version: 7.28.6 + resolution: "@babel/plugin-transform-object-rest-spread@npm:7.28.6" dependencies: - "@babel/helper-compilation-targets": "npm:^7.27.2" - "@babel/helper-plugin-utils": "npm:^7.27.1" - "@babel/plugin-transform-destructuring": "npm:^7.28.0" + "@babel/helper-compilation-targets": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" + "@babel/plugin-transform-destructuring": "npm:^7.28.5" "@babel/plugin-transform-parameters": "npm:^7.27.7" - "@babel/traverse": "npm:^7.28.4" + "@babel/traverse": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/aebe464e368cefa5c3ba40316c47b61eb25f891d436b2241021efef5bd0b473c4aa5ba4b9fa0f4b4d5ce4f6bc6b727628d1ca79d54e7b8deebb5369f7dff2984 + checksum: 10/9c8c51a515a5ec98a33a715e82d49f873e58b04b53fa1e826f3c2009f7133cd396d6730553a53d265e096dbfbea17dd100ae38815d0b506c094cb316a7a5519e languageName: node linkType: hard @@ -1243,13 +1194,13 @@ __metadata: linkType: hard "@babel/plugin-transform-optional-catch-binding@npm:^7.24.7, @babel/plugin-transform-optional-catch-binding@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.27.1" + version: 7.28.6 + resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/f4356b04cf21a98480f9788ea50f1f13ee88e89bb6393ba4b84d1f39a4a84c7928c9a4328e8f4c5b6deb218da68a8fd17bf4f46faec7653ddc20ffaaa5ba49f4 + checksum: 10/ee24a17defec056eb9ef01824d7e4a1f65d531af6b4b79acfd0bcb95ce0b47926e80c61897f36f8c01ce733b069c9acdb1c9ce5ec07a729d0dbf9e8d859fe992 languageName: node linkType: hard @@ -1266,14 +1217,14 @@ __metadata: linkType: hard "@babel/plugin-transform-optional-chaining@npm:^7.0.0-0, @babel/plugin-transform-optional-chaining@npm:^7.24.7, @babel/plugin-transform-optional-chaining@npm:^7.24.8, @babel/plugin-transform-optional-chaining@npm:^7.27.1": - version: 7.28.5 - resolution: "@babel/plugin-transform-optional-chaining@npm:7.28.5" + version: 7.28.6 + resolution: "@babel/plugin-transform-optional-chaining@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/0bc900bff66d5acc13b057107eaeb6084b4cb0b124654d35b103f71f292d33dba5beac444ab4f92528583585b6e0cf34d64ce9cbb473b15d22375a4a6ed3cbac + checksum: 10/c7cf29f99384a9a98748f04489a122c0106e0316aa64a2e61ef8af74c1057b587b96d9a08eb4e33d2ac17d1aaff1f0a86fae658d429fa7bcce4ef977e0ad684b languageName: node linkType: hard @@ -1289,27 +1240,27 @@ __metadata: linkType: hard "@babel/plugin-transform-private-methods@npm:^7.24.7, @babel/plugin-transform-private-methods@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-private-methods@npm:7.27.1" + version: 7.28.6 + resolution: "@babel/plugin-transform-private-methods@npm:7.28.6" dependencies: - "@babel/helper-create-class-features-plugin": "npm:^7.27.1" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-create-class-features-plugin": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/c76f8f6056946466116e67eb9d8014a2d748ade2062636ab82045c1dac9c233aff10e597777bc5af6f26428beb845ceb41b95007abef7d0484da95789da56662 + checksum: 10/b80179b28f6a165674d0b0d6c6349b13a01dd282b18f56933423c0a33c23fc0626c8f011f859fc20737d021fe966eb8474a5233e4596401482e9ee7fb00e2aa2 languageName: node linkType: hard "@babel/plugin-transform-private-property-in-object@npm:^7.24.7, @babel/plugin-transform-private-property-in-object@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-private-property-in-object@npm:7.27.1" + version: 7.28.6 + resolution: "@babel/plugin-transform-private-property-in-object@npm:7.28.6" dependencies: - "@babel/helper-annotate-as-pure": "npm:^7.27.1" - "@babel/helper-create-class-features-plugin": "npm:^7.27.1" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-annotate-as-pure": "npm:^7.27.3" + "@babel/helper-create-class-features-plugin": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/d4466d42a02c5a318d9d7b8102969fd032b17ff044918dfd462d5cc49bd11f5773ee0794781702afdf4727ba11e9be6cbea1e396bc0a7307761bb9a56399012a + checksum: 10/d02008c62fd32ff747b850b8581ab5076b717320e1cb01c7fc66ebf5169095bd922e18cfb269992f85bc7fbd2cc61e5b5af25e2b54aad67411474b789ea94d5f languageName: node linkType: hard @@ -1325,13 +1276,13 @@ __metadata: linkType: hard "@babel/plugin-transform-react-display-name@npm:^7.24.7, @babel/plugin-transform-react-display-name@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-react-display-name@npm:7.25.9" + version: 7.28.0 + resolution: "@babel/plugin-transform-react-display-name@npm:7.28.0" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/dc7affde0ed98e40f629ee92a2fc44fbd8008aabda1ddb3f5bd2632699d3289b08dff65b26cf3b89dab46397ec440f453d19856bbb3a9a83df5b4ac6157c5c39 + checksum: 10/d623644a078086f410b1952429d82c10e2833ebffb97800b25f55ab7f3ffafde34e57a4a71958da73f4abfcef39b598e2ca172f2b43531f98b3f12e0de17c219 languageName: node linkType: hard @@ -1369,17 +1320,17 @@ __metadata: linkType: hard "@babel/plugin-transform-react-jsx@npm:^7.25.2, @babel/plugin-transform-react-jsx@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-react-jsx@npm:7.25.9" + version: 7.28.6 + resolution: "@babel/plugin-transform-react-jsx@npm:7.28.6" dependencies: - "@babel/helper-annotate-as-pure": "npm:^7.25.9" - "@babel/helper-module-imports": "npm:^7.25.9" - "@babel/helper-plugin-utils": "npm:^7.25.9" - "@babel/plugin-syntax-jsx": "npm:^7.25.9" - "@babel/types": "npm:^7.25.9" + "@babel/helper-annotate-as-pure": "npm:^7.27.3" + "@babel/helper-module-imports": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" + "@babel/plugin-syntax-jsx": "npm:^7.28.6" + "@babel/types": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/eb179ecdf0ae19aed254105cf78fbac35f9983f51ed04b7b67c863a4820a70a879bd5da250ac518321f86df20eac010e53e3411c8750c386d51da30e4814bfb6 + checksum: 10/c6eade7309f0710b6aac9e747f8c3305633801c035a35efc5e2436742cc466e457ed5848d3dd5dade36e34332cfc50ac92d69a33f7803d66ae2d72f13a76c3bc languageName: node linkType: hard @@ -1396,13 +1347,13 @@ __metadata: linkType: hard "@babel/plugin-transform-regenerator@npm:^7.24.7, @babel/plugin-transform-regenerator@npm:^7.28.3": - version: 7.28.4 - resolution: "@babel/plugin-transform-regenerator@npm:7.28.4" + version: 7.29.0 + resolution: "@babel/plugin-transform-regenerator@npm:7.29.0" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/24da51a659d882e02bd4353da9d8e045e58d967c1cddaf985ad699a9fc9f920a45eff421c4283a248d83dc16590b8956e66fd710be5db8723b274cfea0b51b2f + checksum: 10/c8fa9da74371568c5d34fd7d53de018752550cb10334040ca59e41f34b27f127974bdc5b4d1a1a8e8f3ebcf3cb7f650aa3f2df3b7bf1b7edf67c04493b9e3cb8 languageName: node linkType: hard @@ -1457,14 +1408,14 @@ __metadata: linkType: hard "@babel/plugin-transform-spread@npm:^7.24.7, @babel/plugin-transform-spread@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-spread@npm:7.27.1" + version: 7.28.6 + resolution: "@babel/plugin-transform-spread@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/3edd28b07e1951f32aa2d380d9a0e0ed408c64a5cea2921d02308541042aca18f146b3a61e82e534d4d61cb3225dbc847f4f063aedfff6230b1a41282e95e8a2 + checksum: 10/1fa02ac60ae5e49d46fa2966aaf3f7578cf37255534c2ecf379d65855088a1623c3eea28b9ee6a0b1413b0199b51f9019d0da3fe9da89986bc47e07242415f60 languageName: node linkType: hard @@ -1512,22 +1463,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-typescript@npm:^7.25.2, @babel/plugin-transform-typescript@npm:^7.25.9": - version: 7.26.8 - resolution: "@babel/plugin-transform-typescript@npm:7.26.8" - dependencies: - "@babel/helper-annotate-as-pure": "npm:^7.25.9" - "@babel/helper-create-class-features-plugin": "npm:^7.25.9" - "@babel/helper-plugin-utils": "npm:^7.26.5" - "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.25.9" - "@babel/plugin-syntax-typescript": "npm:^7.25.9" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 10/42741f21aad5b9182f9d05bdef4a04e422f4dbff1c9f9cd16e3d07de985510da024b58d86d2de88d9c3534bc4f1404a288f02d4f7b8e720e757664846a88a83b - languageName: node - linkType: hard - -"@babel/plugin-transform-typescript@npm:^7.27.1": +"@babel/plugin-transform-typescript@npm:^7.25.2, @babel/plugin-transform-typescript@npm:^7.27.1": version: 7.28.6 resolution: "@babel/plugin-transform-typescript@npm:7.28.6" dependencies: @@ -1711,7 +1647,7 @@ __metadata: languageName: node linkType: hard -"@babel/preset-typescript@npm:7.27.1": +"@babel/preset-typescript@npm:7.27.1, @babel/preset-typescript@npm:^7.16.7, @babel/preset-typescript@npm:^7.17.12, @babel/preset-typescript@npm:^7.23.0, @babel/preset-typescript@npm:^7.23.3, @babel/preset-typescript@npm:^7.24.7": version: 7.27.1 resolution: "@babel/preset-typescript@npm:7.27.1" dependencies: @@ -1726,21 +1662,6 @@ __metadata: languageName: node linkType: hard -"@babel/preset-typescript@npm:^7.16.7, @babel/preset-typescript@npm:^7.17.12, @babel/preset-typescript@npm:^7.23.0, @babel/preset-typescript@npm:^7.23.3, @babel/preset-typescript@npm:^7.24.7": - version: 7.26.0 - resolution: "@babel/preset-typescript@npm:7.26.0" - dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" - "@babel/helper-validator-option": "npm:^7.25.9" - "@babel/plugin-syntax-jsx": "npm:^7.25.9" - "@babel/plugin-transform-modules-commonjs": "npm:^7.25.9" - "@babel/plugin-transform-typescript": "npm:^7.25.9" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 10/81a60826160163a3daae017709f42147744757b725b50c9024ef3ee5a402ee45fd2e93eaecdaaa22c81be91f7940916249cfb7711366431cfcacc69c95878c03 - languageName: node - linkType: hard - "@babel/register@npm:^7.24.6": version: 7.28.3 resolution: "@babel/register@npm:7.28.3" @@ -1757,24 +1678,13 @@ __metadata: linkType: hard "@babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.25.0, @babel/runtime@npm:^7.26.0, @babel/runtime@npm:^7.27.6, @babel/runtime@npm:^7.28.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.7": - version: 7.28.4 - resolution: "@babel/runtime@npm:7.28.4" - checksum: 10/6c9a70452322ea80b3c9b2a412bcf60771819213a67576c8cec41e88a95bb7bf01fc983754cda35dc19603eef52df22203ccbf7777b9d6316932f9fb77c25163 + version: 7.29.2 + resolution: "@babel/runtime@npm:7.29.2" + checksum: 10/f55ba4052aa0255055b34371a145fbe69c29b37b49eaea14805b095bfb4153701486416e89392fd27ec8abafa53868be86e960b9f8f959fff91f2c8ac2a14b02 languageName: node linkType: hard -"@babel/template@npm:^7.0.0, @babel/template@npm:^7.25.0, @babel/template@npm:^7.25.7, @babel/template@npm:^7.27.1, @babel/template@npm:^7.27.2, @babel/template@npm:^7.3.3": - version: 7.27.2 - resolution: "@babel/template@npm:7.27.2" - dependencies: - "@babel/code-frame": "npm:^7.27.1" - "@babel/parser": "npm:^7.27.2" - "@babel/types": "npm:^7.27.1" - checksum: 10/fed15a84beb0b9340e5f81566600dbee5eccd92e4b9cc42a944359b1aa1082373391d9d5fc3656981dff27233ec935d0bc96453cf507f60a4b079463999244d8 - languageName: node - linkType: hard - -"@babel/template@npm:^7.28.6": +"@babel/template@npm:^7.0.0, @babel/template@npm:^7.25.0, @babel/template@npm:^7.27.1, @babel/template@npm:^7.27.2, @babel/template@npm:^7.28.6, @babel/template@npm:^7.3.3": version: 7.28.6 resolution: "@babel/template@npm:7.28.6" dependencies: @@ -1785,37 +1695,7 @@ __metadata: languageName: node linkType: hard -"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3": - version: 7.25.7 - resolution: "@babel/traverse@npm:7.25.7" - dependencies: - "@babel/code-frame": "npm:^7.25.7" - "@babel/generator": "npm:^7.25.7" - "@babel/parser": "npm:^7.25.7" - "@babel/template": "npm:^7.25.7" - "@babel/types": "npm:^7.25.7" - debug: "npm:^4.3.1" - globals: "npm:^11.1.0" - checksum: 10/5b2d332fcd6bc78e6500c997e79f7e2a54dfb357e06f0908cb7f0cdd9bb54e7fd3c5673f45993849d433d01ea6076a6d04b825958f0cfa01288ad55ffa5c286f - languageName: node - linkType: hard - -"@babel/traverse@npm:^7.16.0, @babel/traverse@npm:^7.20.0, @babel/traverse@npm:^7.25.3, @babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.28.0, @babel/traverse@npm:^7.28.3, @babel/traverse@npm:^7.28.4, @babel/traverse@npm:^7.28.5": - version: 7.28.5 - resolution: "@babel/traverse@npm:7.28.5" - dependencies: - "@babel/code-frame": "npm:^7.27.1" - "@babel/generator": "npm:^7.28.5" - "@babel/helper-globals": "npm:^7.28.0" - "@babel/parser": "npm:^7.28.5" - "@babel/template": "npm:^7.27.2" - "@babel/types": "npm:^7.28.5" - debug: "npm:^4.3.1" - checksum: 10/1fce426f5ea494913c40f33298ce219708e703f71cac7ac045ebde64b5a7b17b9275dfa4e05fb92c3f123136913dff62c8113172f4a5de66dab566123dbe7437 - languageName: node - linkType: hard - -"@babel/traverse@npm:^7.28.6": +"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3, @babel/traverse@npm:^7.16.0, @babel/traverse@npm:^7.20.0, @babel/traverse@npm:^7.25.3, @babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.28.3, @babel/traverse@npm:^7.28.4, @babel/traverse@npm:^7.28.5, @babel/traverse@npm:^7.28.6, @babel/traverse@npm:^7.29.0": version: 7.29.0 resolution: "@babel/traverse@npm:7.29.0" dependencies: @@ -1830,17 +1710,7 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.24.7, @babel/types@npm:^7.25.2, @babel/types@npm:^7.25.4, @babel/types@npm:^7.25.7, @babel/types@npm:^7.25.9, @babel/types@npm:^7.26.0, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.28.4, @babel/types@npm:^7.28.5, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": - version: 7.28.5 - resolution: "@babel/types@npm:7.28.5" - dependencies: - "@babel/helper-string-parser": "npm:^7.27.1" - "@babel/helper-validator-identifier": "npm:^7.28.5" - checksum: 10/4256bb9fb2298c4f9b320bde56e625b7091ea8d2433d98dcf524d4086150da0b6555aabd7d0725162670614a9ac5bf036d1134ca13dedc9707f988670f1362d7 - languageName: node - linkType: hard - -"@babel/types@npm:^7.28.6, @babel/types@npm:^7.29.0": +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.24.7, @babel/types@npm:^7.25.2, @babel/types@npm:^7.25.4, @babel/types@npm:^7.26.0, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.28.4, @babel/types@npm:^7.28.5, @babel/types@npm:^7.28.6, @babel/types@npm:^7.29.0, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": version: 7.29.0 resolution: "@babel/types@npm:7.29.0" dependencies: @@ -1878,15 +1748,6 @@ __metadata: languageName: node linkType: hard -"@config-plugins/react-native-callkeep@npm:^12.0.0": - version: 12.0.0 - resolution: "@config-plugins/react-native-callkeep@npm:12.0.0" - peerDependencies: - expo: ^54 - checksum: 10/4f5980f515aed00f8be6350aceb891bd16caac5d0c4be253aec776d3c01351751ae12537c41fbf591dd646ce98ce9b6a55d0181e8f7ca626d7d7c0c169952527 - languageName: node - linkType: hard - "@config-plugins/react-native-webrtc@npm:^13.0.0": version: 13.0.0 resolution: "@config-plugins/react-native-webrtc@npm:13.0.0" @@ -2274,21 +2135,21 @@ __metadata: languageName: node linkType: hard -"@eslint-community/eslint-utils@npm:^4.1.2, @eslint-community/eslint-utils@npm:^4.5.0, @eslint-community/eslint-utils@npm:^4.7.0, @eslint-community/eslint-utils@npm:^4.8.0": - version: 4.9.0 - resolution: "@eslint-community/eslint-utils@npm:4.9.0" +"@eslint-community/eslint-utils@npm:^4.1.2, @eslint-community/eslint-utils@npm:^4.5.0, @eslint-community/eslint-utils@npm:^4.7.0, @eslint-community/eslint-utils@npm:^4.8.0, @eslint-community/eslint-utils@npm:^4.9.1": + version: 4.9.1 + resolution: "@eslint-community/eslint-utils@npm:4.9.1" dependencies: eslint-visitor-keys: "npm:^3.4.3" peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - checksum: 10/89b1eb3137e14c379865e60573f524fcc0ee5c4b0c7cd21090673e75e5a720f14b92f05ab2d02704c2314b67e67b6f96f3bb209ded6b890ced7b667aa4bf1fa2 + checksum: 10/863b5467868551c9ae34d03eefe634633d08f623fc7b19d860f8f26eb6f303c1a5934253124163bee96181e45ed22bf27473dccc295937c3078493a4a8c9eddd languageName: node linkType: hard -"@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.11.0, @eslint-community/regexpp@npm:^4.12.1": - version: 4.12.1 - resolution: "@eslint-community/regexpp@npm:4.12.1" - checksum: 10/c08f1dd7dd18fbb60bdd0d85820656d1374dd898af9be7f82cb00451313402a22d5e30569c150315b4385907cdbca78c22389b2a72ab78883b3173be317620cc +"@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.11.0, @eslint-community/regexpp@npm:^4.12.1, @eslint-community/regexpp@npm:^4.12.2": + version: 4.12.2 + resolution: "@eslint-community/regexpp@npm:4.12.2" + checksum: 10/049b280fddf71dd325514e0a520024969431dc3a8b02fa77476e6820e9122f28ab4c9168c11821f91a27982d2453bcd7a66193356ea84e84fb7c8d793be1ba0c languageName: node linkType: hard @@ -2338,13 +2199,20 @@ __metadata: languageName: node linkType: hard -"@eslint/js@npm:9.37.0, @eslint/js@npm:^9.37.0": +"@eslint/js@npm:9.37.0": version: 9.37.0 resolution: "@eslint/js@npm:9.37.0" checksum: 10/2ead426ed47af0b914c7d7064eb59fede858483cf9511f78ded840708aca578138f2a6c375916d520f4f2ecf25945f4bd47b8a84e42106b4eb46f7708a36db1d languageName: node linkType: hard +"@eslint/js@npm:^9.37.0": + version: 9.39.4 + resolution: "@eslint/js@npm:9.39.4" + checksum: 10/0a7ab4c4108cf2cadf66849ebd20f5957cc53052b88d8807d0b54e489dbf6ffcaf741e144e7f9b187c395499ce2e6ddc565dbfa4f60c6df455cf2b30bcbdc5a3 + languageName: node + linkType: hard + "@eslint/object-schema@npm:^2.1.6": version: 2.1.6 resolution: "@eslint/object-schema@npm:2.1.6" @@ -2362,6 +2230,13 @@ __metadata: languageName: node linkType: hard +"@expo-google-fonts/material-symbols@npm:^0.4.1": + version: 0.4.27 + resolution: "@expo-google-fonts/material-symbols@npm:0.4.27" + checksum: 10/842b249ff3c128eca356916a908dd813b235d23262e95b555e1ced47dfa1cd027d45f210ad01cd4f67e1defd5294dff5ff8ad9dba7eaa7dadfa69cbbcb788571 + languageName: node + linkType: hard + "@expo/cli@npm:54.0.10": version: 54.0.10 resolution: "@expo/cli@npm:54.0.10" @@ -2445,6 +2320,82 @@ __metadata: languageName: node linkType: hard +"@expo/cli@npm:55.0.19": + version: 55.0.19 + resolution: "@expo/cli@npm:55.0.19" + dependencies: + "@expo/code-signing-certificates": "npm:^0.0.6" + "@expo/config": "npm:~55.0.11" + "@expo/config-plugins": "npm:~55.0.7" + "@expo/devcert": "npm:^1.2.1" + "@expo/env": "npm:~2.1.1" + "@expo/image-utils": "npm:^0.8.12" + "@expo/json-file": "npm:^10.0.12" + "@expo/log-box": "npm:55.0.8" + "@expo/metro": "npm:~54.2.0" + "@expo/metro-config": "npm:~55.0.11" + "@expo/osascript": "npm:^2.4.2" + "@expo/package-manager": "npm:^1.10.3" + "@expo/plist": "npm:^0.5.2" + "@expo/prebuild-config": "npm:^55.0.11" + "@expo/require-utils": "npm:^55.0.3" + "@expo/router-server": "npm:^55.0.11" + "@expo/schema-utils": "npm:^55.0.2" + "@expo/spawn-async": "npm:^1.7.2" + "@expo/ws-tunnel": "npm:^1.0.1" + "@expo/xcpretty": "npm:^4.4.0" + "@react-native/dev-middleware": "npm:0.83.4" + accepts: "npm:^1.3.8" + arg: "npm:^5.0.2" + better-opn: "npm:~3.0.2" + bplist-creator: "npm:0.1.0" + bplist-parser: "npm:^0.3.1" + chalk: "npm:^4.0.0" + ci-info: "npm:^3.3.0" + compression: "npm:^1.7.4" + connect: "npm:^3.7.0" + debug: "npm:^4.3.4" + dnssd-advertise: "npm:^1.1.3" + expo-server: "npm:^55.0.6" + fetch-nodeshim: "npm:^0.4.6" + getenv: "npm:^2.0.0" + glob: "npm:^13.0.0" + lan-network: "npm:^0.2.0" + multitars: "npm:^0.2.3" + node-forge: "npm:^1.3.3" + npm-package-arg: "npm:^11.0.0" + ora: "npm:^3.4.0" + picomatch: "npm:^4.0.3" + pretty-format: "npm:^29.7.0" + progress: "npm:^2.0.3" + prompts: "npm:^2.3.2" + resolve-from: "npm:^5.0.0" + semver: "npm:^7.6.0" + send: "npm:^0.19.0" + slugify: "npm:^1.3.4" + source-map-support: "npm:~0.5.21" + stacktrace-parser: "npm:^0.1.10" + structured-headers: "npm:^0.4.1" + terminal-link: "npm:^2.1.1" + toqr: "npm:^0.1.1" + wrap-ansi: "npm:^7.0.0" + ws: "npm:^8.12.1" + zod: "npm:^3.25.76" + peerDependencies: + expo: "*" + expo-router: "*" + react-native: "*" + peerDependenciesMeta: + expo-router: + optional: true + react-native: + optional: true + bin: + expo-internal: build/bin/cli + checksum: 10/c986116834b32b6d0208a59080e768ce58ea3159955aeece6ed3d2930b724ea35a8da2570647402406e4816eaeb92722ff5ca42189a9a14daedcb6fc6bb3624b + languageName: node + linkType: hard + "@expo/code-signing-certificates@npm:^0.0.5": version: 0.0.5 resolution: "@expo/code-signing-certificates@npm:0.0.5" @@ -2455,6 +2406,15 @@ __metadata: languageName: node linkType: hard +"@expo/code-signing-certificates@npm:^0.0.6": + version: 0.0.6 + resolution: "@expo/code-signing-certificates@npm:0.0.6" + dependencies: + node-forge: "npm:^1.3.3" + checksum: 10/4446cca45e8b48b90ba728e39aab6b1195ede730d7aba7d9830f635aa16a52634e6eba9dc510f83cc6ff6fb6b0e3077bc6021098f0157f6dba96f8494685c388 + languageName: node + linkType: hard + "@expo/config-plugins@npm:54.0.2, @expo/config-plugins@npm:~54.0.2": version: 54.0.2 resolution: "@expo/config-plugins@npm:54.0.2" @@ -2477,6 +2437,27 @@ __metadata: languageName: node linkType: hard +"@expo/config-plugins@npm:~55.0.7": + version: 55.0.7 + resolution: "@expo/config-plugins@npm:55.0.7" + dependencies: + "@expo/config-types": "npm:^55.0.5" + "@expo/json-file": "npm:~10.0.12" + "@expo/plist": "npm:^0.5.2" + "@expo/sdk-runtime-versions": "npm:^1.0.0" + chalk: "npm:^4.1.2" + debug: "npm:^4.3.5" + getenv: "npm:^2.0.0" + glob: "npm:^13.0.0" + resolve-from: "npm:^5.0.0" + semver: "npm:^7.5.4" + slugify: "npm:^1.6.6" + xcode: "npm:^3.0.1" + xml2js: "npm:0.6.0" + checksum: 10/c76b03c65f37cffac9e93826937f329d4f59a6da3680c25daeb4fc55fe62d70304036e652d470e0b0c234f47934306d08705ce7f18259eac8972fa20ec5f882e + languageName: node + linkType: hard + "@expo/config-types@npm:^54.0.8": version: 54.0.8 resolution: "@expo/config-types@npm:54.0.8" @@ -2484,6 +2465,13 @@ __metadata: languageName: node linkType: hard +"@expo/config-types@npm:^55.0.5": + version: 55.0.5 + resolution: "@expo/config-types@npm:55.0.5" + checksum: 10/9a7b5a025218618b6810d720663ef973b5497baedb194ed29ed60f4aa3d4b012676e57c71807a96aa78f099d562030b3246ae403776b46e0db56db68c6f3ac82 + languageName: node + linkType: hard + "@expo/config@npm:~12.0.10, @expo/config@npm:~12.0.8, @expo/config@npm:~12.0.9": version: 12.0.10 resolution: "@expo/config@npm:12.0.10" @@ -2505,23 +2493,32 @@ __metadata: languageName: node linkType: hard -"@expo/devcert@npm:^1.1.2": - version: 1.1.4 - resolution: "@expo/devcert@npm:1.1.4" +"@expo/config@npm:~55.0.10, @expo/config@npm:~55.0.11": + version: 55.0.11 + resolution: "@expo/config@npm:55.0.11" dependencies: - application-config-path: "npm:^0.1.0" - command-exists: "npm:^1.2.4" + "@expo/config-plugins": "npm:~55.0.7" + "@expo/config-types": "npm:^55.0.5" + "@expo/json-file": "npm:^10.0.12" + "@expo/require-utils": "npm:^55.0.3" + deepmerge: "npm:^4.3.1" + getenv: "npm:^2.0.0" + glob: "npm:^13.0.0" + resolve-from: "npm:^5.0.0" + resolve-workspace-root: "npm:^2.0.0" + semver: "npm:^7.6.0" + slugify: "npm:^1.3.4" + checksum: 10/03120edb2a10e71f5dca166ada6abae03d44f9a057c300edf06be789937a9eeec1c7f0c693c440a52f177f51eb1e8f712c51b86aad3fad88b082979793915f17 + languageName: node + linkType: hard + +"@expo/devcert@npm:^1.1.2, @expo/devcert@npm:^1.2.1": + version: 1.2.1 + resolution: "@expo/devcert@npm:1.2.1" + dependencies: + "@expo/sudo-prompt": "npm:^9.3.1" debug: "npm:^3.1.0" - eol: "npm:^0.9.1" - get-port: "npm:^3.2.0" - glob: "npm:^10.4.2" - lodash: "npm:^4.17.21" - mkdirp: "npm:^0.5.1" - password-prompt: "npm:^1.0.4" - sudo-prompt: "npm:^8.2.0" - tmp: "npm:^0.0.33" - tslib: "npm:^2.4.0" - checksum: 10/da897fad243ff74c5c70486aa020b6ed691c3a68a2bed5758e76245d493cee0499d3c1efbc9fa8993e5addc0cf73de5eff77211780669ae122b802327cefacee + checksum: 10/39ac1ea49fd6c95eee78a3ca712647aa546a08cf1133d2586a7abd23bf4aa2222f618002e8e8b54c26b576cae8a6ed4cdb3ba77d04aa46f147c4cf59d27bd1fc languageName: node linkType: hard @@ -2542,6 +2539,45 @@ __metadata: languageName: node linkType: hard +"@expo/devtools@npm:55.0.2": + version: 55.0.2 + resolution: "@expo/devtools@npm:55.0.2" + dependencies: + chalk: "npm:^4.1.2" + peerDependencies: + react: "*" + react-native: "*" + peerDependenciesMeta: + react: + optional: true + react-native: + optional: true + checksum: 10/0a43121fb5a7993dfe0c112e287e292358c099c4f02dbd1f80e67fe8bb7cff21be77cf389fefcc84f86e2955066e4b0e70e447cf48ca8772de47c6eef114ecdd + languageName: node + linkType: hard + +"@expo/dom-webview@npm:^55.0.3": + version: 55.0.3 + resolution: "@expo/dom-webview@npm:55.0.3" + peerDependencies: + expo: "*" + react: "*" + react-native: "*" + checksum: 10/e93ec71dc764b57fb109ed97794b8b033a88ab9656bee875853f838777590ff85bc7614f1af95e9ea528a3424e18fa27be80fe252565f0dff980e8766a56d7f9 + languageName: node + linkType: hard + +"@expo/env@npm:^2.0.11, @expo/env@npm:~2.1.1": + version: 2.1.1 + resolution: "@expo/env@npm:2.1.1" + dependencies: + chalk: "npm:^4.0.0" + debug: "npm:^4.3.4" + getenv: "npm:^2.0.0" + checksum: 10/19be4c7131b1d718a456018dfe3133b6c021b71b8689b11b208d03aae947c0f0848ce21996adf9010c1b87d765b46b14484f1d1f30f73db466b9500024bfac53 + languageName: node + linkType: hard + "@expo/env@npm:~2.0.7": version: 2.0.7 resolution: "@expo/env@npm:2.0.7" @@ -2576,9 +2612,30 @@ __metadata: languageName: node linkType: hard -"@expo/image-utils@npm:^0.8.7": - version: 0.8.7 - resolution: "@expo/image-utils@npm:0.8.7" +"@expo/fingerprint@npm:0.16.6": + version: 0.16.6 + resolution: "@expo/fingerprint@npm:0.16.6" + dependencies: + "@expo/env": "npm:^2.0.11" + "@expo/spawn-async": "npm:^1.7.2" + arg: "npm:^5.0.2" + chalk: "npm:^4.1.2" + debug: "npm:^4.3.4" + getenv: "npm:^2.0.0" + glob: "npm:^13.0.0" + ignore: "npm:^5.3.1" + minimatch: "npm:^10.2.2" + resolve-from: "npm:^5.0.0" + semver: "npm:^7.6.0" + bin: + fingerprint: bin/cli.js + checksum: 10/2bf59bd6964c17c7bbcf66f963684909345f7b0b5da459be5cc34f27eae4860cd18e8644db30a2cec1553f67b81dce26983d00e34dbafa59f2e0bfa5783fc787 + languageName: node + linkType: hard + +"@expo/image-utils@npm:^0.8.12, @expo/image-utils@npm:^0.8.7": + version: 0.8.12 + resolution: "@expo/image-utils@npm:0.8.12" dependencies: "@expo/spawn-async": "npm:^1.7.2" chalk: "npm:^4.0.0" @@ -2586,21 +2643,44 @@ __metadata: jimp-compact: "npm:0.16.1" parse-png: "npm:^2.1.0" resolve-from: "npm:^5.0.0" - resolve-global: "npm:^1.0.0" semver: "npm:^7.6.0" - temp-dir: "npm:~2.0.0" - unique-string: "npm:~2.0.0" - checksum: 10/cd21880fd27ac249d3a2271b6fc7beb70f3504adbd3663e885daff9baf817f5225cec8a4dda5b5f1790fea66f6d6103d2bd9c7451c6610bef1f9b42b745fbeec + checksum: 10/fb474558bb4009f39c640fb028a57cfae721e52dae0085bb2505390c6968d30cdc82eb195c15de82f30879c710104c08e60120de8f49613183437701f19dd363 languageName: node linkType: hard -"@expo/json-file@npm:^10.0.7, @expo/json-file@npm:~10.0.7": - version: 10.0.7 - resolution: "@expo/json-file@npm:10.0.7" +"@expo/json-file@npm:^10.0.12, @expo/json-file@npm:^10.0.7, @expo/json-file@npm:~10.0.12, @expo/json-file@npm:~10.0.7": + version: 10.0.12 + resolution: "@expo/json-file@npm:10.0.12" dependencies: - "@babel/code-frame": "npm:~7.10.4" + "@babel/code-frame": "npm:^7.20.0" json5: "npm:^2.2.3" - checksum: 10/aadf85187e8f56b2183b24d13757657bc87b6ea8ddc77cca88d8421a4842cffdc2e38e197bde132c4abd0cfa44dc18bea5d16db7fd835a1df1a65063336d6984 + checksum: 10/547f5b9d1c5b10147ef0780d079d853e3b2e8ec0b09080420cb48592060a4399308622fd205aaec5e157c41d37c5b69dffa9aaa96c01fe444b0258f78c3bb85f + languageName: node + linkType: hard + +"@expo/local-build-cache-provider@npm:55.0.7": + version: 55.0.7 + resolution: "@expo/local-build-cache-provider@npm:55.0.7" + dependencies: + "@expo/config": "npm:~55.0.10" + chalk: "npm:^4.1.2" + checksum: 10/d25e83ec263e6dfa22e64b09470a8952e4811a8bf068c7fbbe06ab597d5345119dc130717860ad12a4855766986966635aa3bff3032005b3010de08b5fb53a37 + languageName: node + linkType: hard + +"@expo/log-box@npm:55.0.8": + version: 55.0.8 + resolution: "@expo/log-box@npm:55.0.8" + dependencies: + "@expo/dom-webview": "npm:^55.0.3" + anser: "npm:^1.4.9" + stacktrace-parser: "npm:^0.1.10" + peerDependencies: + "@expo/dom-webview": ^55.0.3 + expo: "*" + react: "*" + react-native: "*" + checksum: 10/15803918b2e0a0d86e06df8f479e1eb24519d13caa21f17ce02ea657f02bbf41b84234158b589c61dc8783bfd3bb4f8df073bd5058c980489dd72eafc31680e8 languageName: node linkType: hard @@ -2648,9 +2728,62 @@ __metadata: peerDependencies: expo: "*" peerDependenciesMeta: - expo: + expo: + optional: true + checksum: 10/82633cd5f56969872cecd776a4c5c2337192c0c383a4ee73892dc07cc1635e2d1284ef53c781eb76bee664c023595434da6637f591649c602826fa959d15fbbf + languageName: node + linkType: hard + +"@expo/metro-config@npm:55.0.11, @expo/metro-config@npm:~55.0.11": + version: 55.0.11 + resolution: "@expo/metro-config@npm:55.0.11" + dependencies: + "@babel/code-frame": "npm:^7.20.0" + "@babel/core": "npm:^7.20.0" + "@babel/generator": "npm:^7.20.5" + "@expo/config": "npm:~55.0.10" + "@expo/env": "npm:~2.1.1" + "@expo/json-file": "npm:~10.0.12" + "@expo/metro": "npm:~54.2.0" + "@expo/spawn-async": "npm:^1.7.2" + browserslist: "npm:^4.25.0" + chalk: "npm:^4.1.0" + debug: "npm:^4.3.2" + getenv: "npm:^2.0.0" + glob: "npm:^13.0.0" + hermes-parser: "npm:^0.32.0" + jsc-safe-url: "npm:^0.2.4" + lightningcss: "npm:^1.30.1" + picomatch: "npm:^4.0.3" + postcss: "npm:~8.4.32" + resolve-from: "npm:^5.0.0" + peerDependencies: + expo: "*" + peerDependenciesMeta: + expo: + optional: true + checksum: 10/df25f9454f773d99371bf90a00a7574d722ccc832baa0e63b06bd037e7bccd63aee626f9725fe0e343d280658996f2a288326dfd13d5fb3c11455cbd7cf7039d + languageName: node + linkType: hard + +"@expo/metro-runtime@npm:^55.0.7": + version: 55.0.7 + resolution: "@expo/metro-runtime@npm:55.0.7" + dependencies: + "@expo/log-box": "npm:55.0.8" + anser: "npm:^1.4.9" + pretty-format: "npm:^29.7.0" + stacktrace-parser: "npm:^0.1.10" + whatwg-fetch: "npm:^3.0.0" + peerDependencies: + expo: "*" + react: "*" + react-dom: "*" + react-native: "*" + peerDependenciesMeta: + react-dom: optional: true - checksum: 10/82633cd5f56969872cecd776a4c5c2337192c0c383a4ee73892dc07cc1635e2d1284ef53c781eb76bee664c023595434da6637f591649c602826fa959d15fbbf + checksum: 10/2f3faa4195578012ac75cfd925690869005e5cbe982a8dda73c2b796fa1006d4fecd12a947b85057e887e44d426185e97c5c58611956e3e1fa634489b58cb64c languageName: node linkType: hard @@ -2694,6 +2827,28 @@ __metadata: languageName: node linkType: hard +"@expo/metro@npm:~54.2.0": + version: 54.2.0 + resolution: "@expo/metro@npm:54.2.0" + dependencies: + metro: "npm:0.83.3" + metro-babel-transformer: "npm:0.83.3" + metro-cache: "npm:0.83.3" + metro-cache-key: "npm:0.83.3" + metro-config: "npm:0.83.3" + metro-core: "npm:0.83.3" + metro-file-map: "npm:0.83.3" + metro-minify-terser: "npm:0.83.3" + metro-resolver: "npm:0.83.3" + metro-runtime: "npm:0.83.3" + metro-source-map: "npm:0.83.3" + metro-symbolicate: "npm:0.83.3" + metro-transform-plugins: "npm:0.83.3" + metro-transform-worker: "npm:0.83.3" + checksum: 10/36087cec4cb1788f6c8f6148f9dcd30e8d3693fbf8a14f8b0a3c9575895bd6b1847690c958181d7e92718d49ab66df285a79d64ff3c13e4168bbfee26b670d7f + languageName: node + linkType: hard + "@expo/npm-proofread@npm:^1.0.1": version: 1.0.1 resolution: "@expo/npm-proofread@npm:1.0.1" @@ -2705,38 +2860,48 @@ __metadata: languageName: node linkType: hard -"@expo/osascript@npm:^2.3.7": - version: 2.3.7 - resolution: "@expo/osascript@npm:2.3.7" +"@expo/osascript@npm:^2.3.7, @expo/osascript@npm:^2.4.2": + version: 2.4.2 + resolution: "@expo/osascript@npm:2.4.2" dependencies: "@expo/spawn-async": "npm:^1.7.2" - exec-async: "npm:^2.2.0" - checksum: 10/e87f195ee73c4adb72e546d59557fbcb8aa33e80521b1856e42341a2b583faa360aba2eb74c8deb3a8b277ede04a495d62bb8c562b682105ff7357b37e92369c + checksum: 10/5609b926bd68120b6a01edea0c7b14d4fa9fcd454bbcb49b89988f7acdb540f3b9c1c133acbbd3f9cd6a6937ce2a950c9cdde2a98ec8769d8a8b1481666a67d9 languageName: node linkType: hard -"@expo/package-manager@npm:^1.9.8": - version: 1.9.8 - resolution: "@expo/package-manager@npm:1.9.8" +"@expo/package-manager@npm:^1.10.3, @expo/package-manager@npm:^1.9.8": + version: 1.10.3 + resolution: "@expo/package-manager@npm:1.10.3" dependencies: - "@expo/json-file": "npm:^10.0.7" + "@expo/json-file": "npm:^10.0.12" "@expo/spawn-async": "npm:^1.7.2" chalk: "npm:^4.0.0" npm-package-arg: "npm:^11.0.0" ora: "npm:^3.4.0" resolve-workspace-root: "npm:^2.0.0" - checksum: 10/2e4db0e08803875e54ab1061bc864b57d8383c21b76c8c4e2adaf92d8f696680834fa08ce241325aa7fe28d7040d76d4e257d6cf1b86138a5853f4eaf7f8a8d0 + checksum: 10/cac9008ec362af0b54ebf55cb64514e3f4258423f0be9a0d1adb2815380e912783be78750c898e393f7bebe7a1b8288d449052b0ce9f790400d185a29b8274bd languageName: node linkType: hard "@expo/plist@npm:^0.4.7": - version: 0.4.7 - resolution: "@expo/plist@npm:0.4.7" + version: 0.4.8 + resolution: "@expo/plist@npm:0.4.8" dependencies: "@xmldom/xmldom": "npm:^0.8.8" base64-js: "npm:^1.2.3" xmlbuilder: "npm:^15.1.1" - checksum: 10/770a316d2231fb9f53f2f65612c12a9178687a4e63e65d792a82788658fd0781e3c45d69e4750bce6ed2dd655c59e2c8583d05bc5cd32cb7260b57f41d8a77e3 + checksum: 10/48ba4ad5cc3668e8c26c5197bf7915a29745d0ae1cba1c38aad0d797ee1835ac74fb577a9e810594063e5984d9e52b367f4069d0ef1d906ba3013fce1c01a19c + languageName: node + linkType: hard + +"@expo/plist@npm:^0.5.2": + version: 0.5.2 + resolution: "@expo/plist@npm:0.5.2" + dependencies: + "@xmldom/xmldom": "npm:^0.8.8" + base64-js: "npm:^1.5.1" + xmlbuilder: "npm:^15.1.1" + checksum: 10/ab9350226a2f651c030f9704a0c66474b616b9772e7c6209d2d8271a6e5cc5d713b3b755c2c790a3b96d6f29af35b5ef18353611dc9e6f58d1827b207036ec81 languageName: node linkType: hard @@ -2760,6 +2925,70 @@ __metadata: languageName: node linkType: hard +"@expo/prebuild-config@npm:^55.0.11": + version: 55.0.11 + resolution: "@expo/prebuild-config@npm:55.0.11" + dependencies: + "@expo/config": "npm:~55.0.11" + "@expo/config-plugins": "npm:~55.0.7" + "@expo/config-types": "npm:^55.0.5" + "@expo/image-utils": "npm:^0.8.12" + "@expo/json-file": "npm:^10.0.12" + "@react-native/normalize-colors": "npm:0.83.4" + debug: "npm:^4.3.1" + resolve-from: "npm:^5.0.0" + semver: "npm:^7.6.0" + xml2js: "npm:0.6.0" + peerDependencies: + expo: "*" + checksum: 10/8151b815d7641e4f80b354ce514f6877eaefbda33b0dca673fd633743664b473188ff1f878f8d2725a2495ceb2f156fd55e9ec72b874f47bc1f570d9d5fe37a1 + languageName: node + linkType: hard + +"@expo/require-utils@npm:^55.0.3": + version: 55.0.3 + resolution: "@expo/require-utils@npm:55.0.3" + dependencies: + "@babel/code-frame": "npm:^7.20.0" + "@babel/core": "npm:^7.25.2" + "@babel/plugin-transform-modules-commonjs": "npm:^7.24.8" + peerDependencies: + typescript: ^5.0.0 || ^5.0.0-0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/b51e0a8760eb6287aef44f46daaf7f9737465200c0d47b94749246932c3f5f3971acdd2de905bd76233dd070f321ac558688f10dcf2ff532cb450f6cdb0cfb74 + languageName: node + linkType: hard + +"@expo/router-server@npm:^55.0.11": + version: 55.0.11 + resolution: "@expo/router-server@npm:55.0.11" + dependencies: + debug: "npm:^4.3.4" + peerDependencies: + "@expo/metro-runtime": ^55.0.6 + expo: "*" + expo-constants: ^55.0.9 + expo-font: ^55.0.4 + expo-router: "*" + expo-server: ^55.0.6 + react: "*" + react-dom: "*" + react-server-dom-webpack: ~19.0.1 || ~19.1.2 || ~19.2.1 + peerDependenciesMeta: + "@expo/metro-runtime": + optional: true + expo-router: + optional: true + react-dom: + optional: true + react-server-dom-webpack: + optional: true + checksum: 10/b385182d30d770da92ec4744c89dff26da8368318fe63f947c19f741e30057630ea3f25780d55bdb7e7a8c52a2a3833ae9c8ff70688bf21e1d39a486769f5829 + languageName: node + linkType: hard + "@expo/schema-utils@npm:^0.1.7": version: 0.1.7 resolution: "@expo/schema-utils@npm:0.1.7" @@ -2767,6 +2996,13 @@ __metadata: languageName: node linkType: hard +"@expo/schema-utils@npm:^55.0.2": + version: 55.0.2 + resolution: "@expo/schema-utils@npm:55.0.2" + checksum: 10/a5ded5555112f0490af0a9794d876f8c0433a14c46f9f315c581920782d9e8c6e830f401e03e174a5ca245f90d8b07143f3e98f762cd2644d307413792f58dd7 + languageName: node + linkType: hard + "@expo/sdk-runtime-versions@npm:^1.0.0": version: 1.0.0 resolution: "@expo/sdk-runtime-versions@npm:1.0.0" @@ -2783,14 +3019,21 @@ __metadata: languageName: node linkType: hard +"@expo/sudo-prompt@npm:^9.3.1": + version: 9.3.2 + resolution: "@expo/sudo-prompt@npm:9.3.2" + checksum: 10/1b9c12d155053f131dd37e35327aaddeff7046eda50e4d9217f29efdb555779eb6d45b45f2336f3e8dae25ae19ea4a0ed70a69cc9e270d66e56cbef1803ef924 + languageName: node + linkType: hard + "@expo/vector-icons@npm:^15.0.2": - version: 15.0.2 - resolution: "@expo/vector-icons@npm:15.0.2" + version: 15.1.1 + resolution: "@expo/vector-icons@npm:15.1.1" peerDependencies: expo-font: ">=14.0.4" react: "*" react-native: "*" - checksum: 10/c500ec439ff47a6a72f39d7d6a8f423707312cf0fe9ecc0a034c038ed97d6831b5c0d51c8ff4de5f9d534740ed83b7406fa75e4dd27d770c98b2ec97f59167d8 + checksum: 10/204fafd5141c81bd55dd33f6c00cdc48ec1d37b6460be6fa3f851ccb235e1fad1097f22d034470daa49a5b839d058bbcadda1efd349c670c2fdce2ae65fb9bba languageName: node linkType: hard @@ -2801,23 +3044,22 @@ __metadata: languageName: node linkType: hard -"@expo/xcpretty@npm:^4.3.0": - version: 4.3.1 - resolution: "@expo/xcpretty@npm:4.3.1" +"@expo/xcpretty@npm:^4.3.0, @expo/xcpretty@npm:^4.4.0": + version: 4.4.1 + resolution: "@expo/xcpretty@npm:4.4.1" dependencies: - "@babel/code-frame": "npm:7.10.4" + "@babel/code-frame": "npm:^7.20.0" chalk: "npm:^4.1.0" - find-up: "npm:^5.0.0" js-yaml: "npm:^4.1.0" bin: excpretty: build/cli.js - checksum: 10/68f9537e496c8f6a40f9eff27c947eff11f209438a5a9a2771241e586fc3e6fcce87422bd051d2bc9bea01209d164b40746b0cb4cb40b21531a45707d8f66d7c + checksum: 10/56d4c7d54f2b2d4a04d24f77c8e6926c0760c2983c5ac54018a35b754e261d3f31b7cd509342ff161dfbe852c03d5d62096927130069e6020db29c33ca3fa580 languageName: node linkType: hard -"@firebase/ai@npm:2.2.1": - version: 2.2.1 - resolution: "@firebase/ai@npm:2.2.1" +"@firebase/ai@npm:2.6.0": + version: 2.6.0 + resolution: "@firebase/ai@npm:2.6.0" dependencies: "@firebase/app-check-interop-types": "npm:0.3.3" "@firebase/component": "npm:0.7.0" @@ -2827,22 +3069,22 @@ __metadata: peerDependencies: "@firebase/app": 0.x "@firebase/app-types": 0.x - checksum: 10/8f74c77cafa86979a9d72f3d90e450d0da2c31089ab972d180a9101ca94231b34895586217060c24f1bd955360e8a0319a041d090757ef9094e3787b092cd0d4 + checksum: 10/adf5a342377c2506c5d3b4b7ade5596167177e6056ec93317d06de2f3b7bcdf2d5c5beb9b1de45677ecaab1112cec981713856105d5ab74df9406e65c44f2360 languageName: node linkType: hard -"@firebase/analytics-compat@npm:0.2.24": - version: 0.2.24 - resolution: "@firebase/analytics-compat@npm:0.2.24" +"@firebase/analytics-compat@npm:0.2.25": + version: 0.2.25 + resolution: "@firebase/analytics-compat@npm:0.2.25" dependencies: - "@firebase/analytics": "npm:0.10.18" + "@firebase/analytics": "npm:0.10.19" "@firebase/analytics-types": "npm:0.8.3" "@firebase/component": "npm:0.7.0" "@firebase/util": "npm:1.13.0" tslib: "npm:^2.1.0" peerDependencies: "@firebase/app-compat": 0.x - checksum: 10/8b9c67613b363966af9d242acd9d91c4eee9363d7b7abc037a0d9e866a288ebcb412ade040a8cbe2d999ea3f00c6c04c6dc1bf9a80b83cd13cdee7aee1dd39f4 + checksum: 10/409965f1651c6b0e778fdc0754f8927696ab9481bcf66bd5ecf4a4d7045fa695c44aee1c3042f5a7d0819f1d8f99e22198e00befe5e15b779b13c37c20afb627 languageName: node linkType: hard @@ -2853,9 +3095,9 @@ __metadata: languageName: node linkType: hard -"@firebase/analytics@npm:0.10.18": - version: 0.10.18 - resolution: "@firebase/analytics@npm:0.10.18" +"@firebase/analytics@npm:0.10.19": + version: 0.10.19 + resolution: "@firebase/analytics@npm:0.10.19" dependencies: "@firebase/component": "npm:0.7.0" "@firebase/installations": "npm:0.6.19" @@ -2864,7 +3106,7 @@ __metadata: tslib: "npm:^2.1.0" peerDependencies: "@firebase/app": 0.x - checksum: 10/cd292a002f8795e1b224c07d98bf14c85fb86e8ebddf80e721d0dbbd12c8ab9a918e6a132d3ddcca1b83aec94c3c68dfd257d617b66a3275db64bd8fcc7c7027 + checksum: 10/18365094d2900bf227f1a8aaaa279682cbf244a528b7034010cf14be1ee6b60425cc7695924ad6b32e8d3371936ffbf5af3f2119d88b1392d99ff6f0fcc018eb languageName: node linkType: hard @@ -2912,16 +3154,16 @@ __metadata: languageName: node linkType: hard -"@firebase/app-compat@npm:0.5.2": - version: 0.5.2 - resolution: "@firebase/app-compat@npm:0.5.2" +"@firebase/app-compat@npm:0.5.6": + version: 0.5.6 + resolution: "@firebase/app-compat@npm:0.5.6" dependencies: - "@firebase/app": "npm:0.14.2" + "@firebase/app": "npm:0.14.6" "@firebase/component": "npm:0.7.0" "@firebase/logger": "npm:0.5.0" "@firebase/util": "npm:1.13.0" tslib: "npm:^2.1.0" - checksum: 10/e458622f5930c0eb8e92d7584ed9abeb65f9fe6f9b1ac813ad7071e4a652136b134eb6a8c1e9137d8893f00982a8b1dae59204b99b48b06a9c8db9f50b76e95e + checksum: 10/cf495f3473f8ac527ae7990fddc79012b1ec43a06810bcc6bd9de6460a9238d285e2d808a5eca9afb9f78c23a0fbab0204d19d0f6c98a0efc57ecbd639b6ad3c languageName: node linkType: hard @@ -2932,31 +3174,31 @@ __metadata: languageName: node linkType: hard -"@firebase/app@npm:0.14.2": - version: 0.14.2 - resolution: "@firebase/app@npm:0.14.2" +"@firebase/app@npm:0.14.6": + version: 0.14.6 + resolution: "@firebase/app@npm:0.14.6" dependencies: "@firebase/component": "npm:0.7.0" "@firebase/logger": "npm:0.5.0" "@firebase/util": "npm:1.13.0" idb: "npm:7.1.1" tslib: "npm:^2.1.0" - checksum: 10/1b29cd7789bae80fdd6b2080dfb7194ec4b407237899e2b2a3ba57b16ef7272652d9bbceb4067fbf39e661791c005190911c3f5e3bd4393ab931118b68ea89cf + checksum: 10/34281deae3fc615c0dba5266ebf52669b0fccc5716c53562ac7dbad5e687b882366666d06cd2dd1ed34bb57afcffcd8b17047f5314908751da5ae7364cd5c1a7 languageName: node linkType: hard -"@firebase/auth-compat@npm:0.6.0": - version: 0.6.0 - resolution: "@firebase/auth-compat@npm:0.6.0" +"@firebase/auth-compat@npm:0.6.1": + version: 0.6.1 + resolution: "@firebase/auth-compat@npm:0.6.1" dependencies: - "@firebase/auth": "npm:1.11.0" + "@firebase/auth": "npm:1.11.1" "@firebase/auth-types": "npm:0.13.0" "@firebase/component": "npm:0.7.0" "@firebase/util": "npm:1.13.0" tslib: "npm:^2.1.0" peerDependencies: "@firebase/app-compat": 0.x - checksum: 10/a3a4b504ec347c2bbdfef6a296d37a5fc73185fb21fde79a53afd07329c9b7975c94d207aac20a246e1bae92e3db4ed4991666c9778652ae8c1753a63bb9c11c + checksum: 10/3a439d709e7c0d55020e77444f1ee20e5c68efe7892198a3e2b9aa82385bdf98d4da7f2376994d8077cf5ec223996f445ee6791457b89c82c71b7eb782787064 languageName: node linkType: hard @@ -2977,9 +3219,9 @@ __metadata: languageName: node linkType: hard -"@firebase/auth@npm:1.11.0": - version: 1.11.0 - resolution: "@firebase/auth@npm:1.11.0" +"@firebase/auth@npm:1.11.1": + version: 1.11.1 + resolution: "@firebase/auth@npm:1.11.1" dependencies: "@firebase/component": "npm:0.7.0" "@firebase/logger": "npm:0.5.0" @@ -2991,7 +3233,7 @@ __metadata: peerDependenciesMeta: "@react-native-async-storage/async-storage": optional: true - checksum: 10/cd41be1a831f2fc31be3c57f5a3967b62571714db5d4301f2ec9a064c51a122fab8c1d26714cbccd66c61aeaed49dcebc754aaf6f9d192fe59b8e84b3702223f + checksum: 10/f9a62fe7caadb5d223bdefd51132a861491428ea05d1d4ff10ed353ce3ba35a36ab2731d4dd511f54f0e57d842c16e2b24900b3fd9751e4587201febdea7eb28 languageName: node linkType: hard @@ -3005,9 +3247,9 @@ __metadata: languageName: node linkType: hard -"@firebase/data-connect@npm:0.3.11": - version: 0.3.11 - resolution: "@firebase/data-connect@npm:0.3.11" +"@firebase/data-connect@npm:0.3.12": + version: 0.3.12 + resolution: "@firebase/data-connect@npm:0.3.12" dependencies: "@firebase/auth-interop-types": "npm:0.2.4" "@firebase/component": "npm:0.7.0" @@ -3016,7 +3258,7 @@ __metadata: tslib: "npm:^2.1.0" peerDependencies: "@firebase/app": 0.x - checksum: 10/35eba8fca282d24d634bf38b5f25caf5a8b3ba4e3e5e59e7ad77cc42c5fdcd50d905eb8760ab9a0c9e527021e2478771f39a67696333432d9cb58ed12e81a5aa + checksum: 10/cad12d04702fe541e9fd696d2e44b5f670beb6a683fe52327036a894b01d57c296575ed984d18d2bb293e629bb4498dc231e31a08865dff41f9f414bae3c4f63 languageName: node linkType: hard @@ -3059,18 +3301,18 @@ __metadata: languageName: node linkType: hard -"@firebase/firestore-compat@npm:0.4.1": - version: 0.4.1 - resolution: "@firebase/firestore-compat@npm:0.4.1" +"@firebase/firestore-compat@npm:0.4.2": + version: 0.4.2 + resolution: "@firebase/firestore-compat@npm:0.4.2" dependencies: "@firebase/component": "npm:0.7.0" - "@firebase/firestore": "npm:4.9.1" + "@firebase/firestore": "npm:4.9.2" "@firebase/firestore-types": "npm:3.0.3" "@firebase/util": "npm:1.13.0" tslib: "npm:^2.1.0" peerDependencies: "@firebase/app-compat": 0.x - checksum: 10/f442f7a443a868b0c26b20513061d83ff7abadb9983ca62a8fb8b2e51fdd29b7d73892462bb393a9ab28829309f6d6f5010ebbc0af58e27492c4de4513ecd38a + checksum: 10/8f61692769dbc622016efee67f20e79e73a4b3fa02baedebc9f3b4c071ba98f1fb7cec389e32a1d1dad8a4335350a4405c2d3757bb2cbdfba4f41bc7bea7c501 languageName: node linkType: hard @@ -3084,20 +3326,20 @@ __metadata: languageName: node linkType: hard -"@firebase/firestore@npm:4.9.1": - version: 4.9.1 - resolution: "@firebase/firestore@npm:4.9.1" +"@firebase/firestore@npm:4.9.2": + version: 4.9.2 + resolution: "@firebase/firestore@npm:4.9.2" dependencies: "@firebase/component": "npm:0.7.0" "@firebase/logger": "npm:0.5.0" "@firebase/util": "npm:1.13.0" - "@firebase/webchannel-wrapper": "npm:1.0.4" + "@firebase/webchannel-wrapper": "npm:1.0.5" "@grpc/grpc-js": "npm:~1.9.0" "@grpc/proto-loader": "npm:^0.7.8" tslib: "npm:^2.1.0" peerDependencies: "@firebase/app": 0.x - checksum: 10/d1ce9cd2935b05e21186713c33894c55fe98150d47aed795fb9bbd0e726a1565233bf9c4609d90091d1f0dcc6b8455f019584d1d2f410e40e587a16cc0e22c95 + checksum: 10/6772d677aaf6fcd80fd1944bd6f9bb6ad347eea9e8ab396e20025cb9daeca5fd902bd4e3b6d060da9ffc64cb938f15a459ae59dd856613422f840b4e7552c531 languageName: node linkType: hard @@ -3262,32 +3504,32 @@ __metadata: languageName: node linkType: hard -"@firebase/remote-config-compat@npm:0.2.19": - version: 0.2.19 - resolution: "@firebase/remote-config-compat@npm:0.2.19" +"@firebase/remote-config-compat@npm:0.2.20": + version: 0.2.20 + resolution: "@firebase/remote-config-compat@npm:0.2.20" dependencies: "@firebase/component": "npm:0.7.0" "@firebase/logger": "npm:0.5.0" - "@firebase/remote-config": "npm:0.6.6" - "@firebase/remote-config-types": "npm:0.4.0" + "@firebase/remote-config": "npm:0.7.0" + "@firebase/remote-config-types": "npm:0.5.0" "@firebase/util": "npm:1.13.0" tslib: "npm:^2.1.0" peerDependencies: "@firebase/app-compat": 0.x - checksum: 10/e32508f7e9afe1bab60edf2236e677bd3497a7ba7fc33c13ebafc2eae5895ec0997197ce4506023781597770c6c0b94ff0a68b34510d707ce18884699893aaaf + checksum: 10/f01e4f4c39bcb78a02282279a079c7c73bcfc66e639b60aa92d185aead9800ad41b27d3b71637ba8ca4e29b8b7892f13ede7964f584a0858c9d104ea40d2b0ba languageName: node linkType: hard -"@firebase/remote-config-types@npm:0.4.0": - version: 0.4.0 - resolution: "@firebase/remote-config-types@npm:0.4.0" - checksum: 10/67de8c448412974bdbdc10b6bca90d957fa81f967553ff9a4aee316d374f9ebb3a24fa2541af639c1a1ece79070fab0ab64c925bcf6bb807e212cba3297e5ddf +"@firebase/remote-config-types@npm:0.5.0": + version: 0.5.0 + resolution: "@firebase/remote-config-types@npm:0.5.0" + checksum: 10/6e94669de272a32fe04009a73ac59d4bb97cee463d5d0dcde6cb79d5a8e1bd702bd81e7a41025ee7460c9a7ea777c0cc78c8e29d61b9e60cac258820345257da languageName: node linkType: hard -"@firebase/remote-config@npm:0.6.6": - version: 0.6.6 - resolution: "@firebase/remote-config@npm:0.6.6" +"@firebase/remote-config@npm:0.7.0": + version: 0.7.0 + resolution: "@firebase/remote-config@npm:0.7.0" dependencies: "@firebase/component": "npm:0.7.0" "@firebase/installations": "npm:0.6.19" @@ -3296,7 +3538,7 @@ __metadata: tslib: "npm:^2.1.0" peerDependencies: "@firebase/app": 0.x - checksum: 10/a2ab2fc656d1f7d832163218755873cdbc9b90df39cefe1d429f613d45ee46dfbe7eac02b075ea51e736c8b7507435b98c49703ffd869ddd7cb91938762702a1 + checksum: 10/4dc5d2a6404360d9e0078801843e68e2f0e7a77e3ef690edf7cc941727eb8241a6e69f2b8fe1b130cde11e06344d4bac8015d3368c08a936fe93edb7e78d88bc languageName: node linkType: hard @@ -3347,29 +3589,29 @@ __metadata: languageName: node linkType: hard -"@firebase/webchannel-wrapper@npm:1.0.4": - version: 1.0.4 - resolution: "@firebase/webchannel-wrapper@npm:1.0.4" - checksum: 10/dc9b670275ff766de3a5b89cc3109d28641dfdebd3c62111691abc0d5c5ab8ed57ff217d35688d1f7df23079b191eb954d53ada51e528fb1a2e9a5430889ce1e +"@firebase/webchannel-wrapper@npm:1.0.5": + version: 1.0.5 + resolution: "@firebase/webchannel-wrapper@npm:1.0.5" + checksum: 10/def9e11a777fb607ce869de2324c6b9f8230e2a62e97d745d5aeafdb5eda2e206855de609d14c128feb112e53d5c94cd4ca6ecaee180da96d27233a703ad9582 languageName: node linkType: hard -"@floating-ui/core@npm:^1.7.3": - version: 1.7.3 - resolution: "@floating-ui/core@npm:1.7.3" +"@floating-ui/core@npm:^1.7.5": + version: 1.7.5 + resolution: "@floating-ui/core@npm:1.7.5" dependencies: - "@floating-ui/utils": "npm:^0.2.10" - checksum: 10/a8952ff2673ddf28f12feeb86d90c54949e45bcb1af5758b7672850ac0dadb36d4bd61aa45dad1b6a35ba40d4756d3573afac6610b90502639d7266b91e0864e + "@floating-ui/utils": "npm:^0.2.11" + checksum: 10/fecdc9b3ce93f02bf78a6114b93730a4cb9fa8234c62f9a949016186297a039c9f9cd3c5c81ff74b93ebddf0b32048c4af7a528afe7904b75423ed2e7491b888 languageName: node linkType: hard "@floating-ui/dom@npm:^1.6.13, @floating-ui/dom@npm:^1.6.3, @floating-ui/dom@npm:^1.7.4": - version: 1.7.4 - resolution: "@floating-ui/dom@npm:1.7.4" + version: 1.7.6 + resolution: "@floating-ui/dom@npm:1.7.6" dependencies: - "@floating-ui/core": "npm:^1.7.3" - "@floating-ui/utils": "npm:^0.2.10" - checksum: 10/d3d6a23e7b9804ba56338c7c666590258683af14b6026270d32afc1202f72b5b82cca359004bdc7830bf2463a045da6c7bd4e7d5351218cf270ff94206197971 + "@floating-ui/core": "npm:^1.7.5" + "@floating-ui/utils": "npm:^0.2.11" + checksum: 10/84dff2ffdf85c8b92d7edafc543c55869abbeaeb3007fa983159467e050153b507a0f5fe8e84f88c3f28c35a82de9df9c20a6eef5560cbba3afae19141444ff2 languageName: node linkType: hard @@ -3399,10 +3641,10 @@ __metadata: languageName: node linkType: hard -"@floating-ui/utils@npm:^0.2.10": - version: 0.2.10 - resolution: "@floating-ui/utils@npm:0.2.10" - checksum: 10/b635ea865a8be2484b608b7157f5abf9ed439f351011a74b7e988439e2898199a9a8b790f52291e05bdcf119088160dc782d98cff45cc98c5a271bc6f51327ae +"@floating-ui/utils@npm:^0.2.10, @floating-ui/utils@npm:^0.2.11": + version: 0.2.11 + resolution: "@floating-ui/utils@npm:0.2.11" + checksum: 10/72150138ba1c274d757a1da85233202fa9fdfd2272ec1fb0883eb0ffdf138863af81573049ed2c20b98adb4b7ae2236065541ce14037fe328955089831a678d5 languageName: node linkType: hard @@ -3803,22 +4045,6 @@ __metadata: languageName: node linkType: hard -"@isaacs/balanced-match@npm:^4.0.1": - version: 4.0.1 - resolution: "@isaacs/balanced-match@npm:4.0.1" - checksum: 10/102fbc6d2c0d5edf8f6dbf2b3feb21695a21bc850f11bc47c4f06aa83bd8884fde3fe9d6d797d619901d96865fdcb4569ac2a54c937992c48885c5e3d9967fe8 - languageName: node - linkType: hard - -"@isaacs/brace-expansion@npm:^5.0.0": - version: 5.0.0 - resolution: "@isaacs/brace-expansion@npm:5.0.0" - dependencies: - "@isaacs/balanced-match": "npm:^4.0.1" - checksum: 10/cf3b7f206aff12128214a1df764ac8cdbc517c110db85249b945282407e3dfc5c6e66286383a7c9391a059fc8e6e6a8ca82262fc9d2590bd615376141fbebd2d - languageName: node - linkType: hard - "@isaacs/cliui@npm:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -3940,6 +4166,13 @@ __metadata: languageName: node linkType: hard +"@jest/diff-sequences@npm:30.3.0": + version: 30.3.0 + resolution: "@jest/diff-sequences@npm:30.3.0" + checksum: 10/0d5b6e1599c5e0bb702f0804e7f93bbe4911b5929c40fd6a77c06105711eae24d709c8964e8d623cc70c34b7dc7262d76a115a6eb05f1576336cdb6c46593e7c + languageName: node + linkType: hard + "@jest/environment@npm:^29.7.0": version: 29.7.0 resolution: "@jest/environment@npm:29.7.0" @@ -4168,10 +4401,10 @@ __metadata: languageName: node linkType: hard -"@jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.4.15, @jridgewell/sourcemap-codec@npm:^1.5.0": - version: 1.5.0 - resolution: "@jridgewell/sourcemap-codec@npm:1.5.0" - checksum: 10/4ed6123217569a1484419ac53f6ea0d9f3b57e5b57ab30d7c267bdb27792a27eb0e4b08e84a2680aa55cc2f2b411ffd6ec3db01c44fdc6dc43aca4b55f8374fd +"@jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.4.15, @jridgewell/sourcemap-codec@npm:^1.5.0, @jridgewell/sourcemap-codec@npm:^1.5.5": + version: 1.5.5 + resolution: "@jridgewell/sourcemap-codec@npm:1.5.5" + checksum: 10/5d9d207b462c11e322d71911e55e21a4e2772f71ffe8d6f1221b8eb5ae6774458c1d242f897fb0814e8714ca9a6b498abfa74dfe4f434493342902b1a48b33a5 languageName: node linkType: hard @@ -4814,9 +5047,9 @@ __metadata: linkType: hard "@opentelemetry/api@npm:^1.3.0, @opentelemetry/api@npm:^1.9.0": - version: 1.9.0 - resolution: "@opentelemetry/api@npm:1.9.0" - checksum: 10/a607f0eef971893c4f2ee2a4c2069aade6ec3e84e2a1f5c2aac19f65c5d9eeea41aa72db917c1029faafdd71789a1a040bdc18f40d63690e22ccae5d7070f194 + version: 1.9.1 + resolution: "@opentelemetry/api@npm:1.9.1" + checksum: 10/b26032739d3c54ca99b5a2920844a1fbd4c3ee383cacbb0915e8c706a2626fe91e96feaa6e893397abe0545dc8d0a765b220aa18a31b1773176eeaf3a225e10e languageName: node linkType: hard @@ -4829,7 +5062,7 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/core@npm:2.2.0, @opentelemetry/core@npm:^2.0.0, @opentelemetry/core@npm:^2.2.0": +"@opentelemetry/core@npm:2.2.0": version: 2.2.0 resolution: "@opentelemetry/core@npm:2.2.0" dependencies: @@ -4840,6 +5073,17 @@ __metadata: languageName: node linkType: hard +"@opentelemetry/core@npm:^2.0.0, @opentelemetry/core@npm:^2.2.0": + version: 2.6.1 + resolution: "@opentelemetry/core@npm:2.6.1" + dependencies: + "@opentelemetry/semantic-conventions": "npm:^1.29.0" + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.10.0" + checksum: 10/153da58268f6ce33d16bd2931e67c827de2d38c2cb1c45f209270d6294d156b1a5594c3da200a50f26a393b1da67dd8255b87f818a5030af555dffac407921f8 + languageName: node + linkType: hard + "@opentelemetry/instrumentation-amqplib@npm:0.55.0": version: 0.55.0 resolution: "@opentelemetry/instrumentation-amqplib@npm:0.55.0" @@ -5757,6 +6001,21 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-slot@npm:^1.2.0": + version: 1.2.4 + resolution: "@radix-ui/react-slot@npm:1.2.4" + dependencies: + "@radix-ui/react-compose-refs": "npm:1.1.2" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/b37e37455b92789758980359d73ab5a5f5d1c12af480c775519bd15c556b891642d472accf05b30d520751489ca74cdb8fd7866064abc7942f0437371be28e51 + languageName: node + linkType: hard + "@radix-ui/react-tabs@npm:^1.1.12": version: 1.1.13 resolution: "@radix-ui/react-tabs@npm:1.1.13" @@ -5955,6 +6214,18 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-clean@npm:20.0.1": + version: 20.0.1 + resolution: "@react-native-community/cli-clean@npm:20.0.1" + dependencies: + "@react-native-community/cli-tools": "npm:20.0.1" + chalk: "npm:^4.1.2" + execa: "npm:^5.0.0" + fast-glob: "npm:^3.3.2" + checksum: 10/4389266313a0fe2baf0423b4860117040d91920d29a6430f65f8ab62b779efb3e082fc4d6eefb36180ba3aff2c02b34f2dc2cbf632ad18df749608701c1d1f05 + languageName: node + linkType: hard + "@react-native-community/cli-config-android@npm:20.0.0": version: 20.0.0 resolution: "@react-native-community/cli-config-android@npm:20.0.0" @@ -5967,6 +6238,18 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-config-android@npm:20.0.1": + version: 20.0.1 + resolution: "@react-native-community/cli-config-android@npm:20.0.1" + dependencies: + "@react-native-community/cli-tools": "npm:20.0.1" + chalk: "npm:^4.1.2" + fast-glob: "npm:^3.3.2" + fast-xml-parser: "npm:^4.4.1" + checksum: 10/6e3f146509ea3f18c2be9e1c626511d9128519aa7e99b584f576257124c33e9d41d51c6e53ea52c1fcf6a06f30eab9eceee4dff37c91f13b273e4b8c5e94e3fe + languageName: node + linkType: hard + "@react-native-community/cli-config-apple@npm:20.0.0": version: 20.0.0 resolution: "@react-native-community/cli-config-apple@npm:20.0.0" @@ -5979,6 +6262,18 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-config-apple@npm:20.0.1": + version: 20.0.1 + resolution: "@react-native-community/cli-config-apple@npm:20.0.1" + dependencies: + "@react-native-community/cli-tools": "npm:20.0.1" + chalk: "npm:^4.1.2" + execa: "npm:^5.0.0" + fast-glob: "npm:^3.3.2" + checksum: 10/2cab86e8a156b2a23b31e3064593271d233267a9c746c42075864d9c11bea8de35d0956bf7b01f3f446a299f07d60edbb11da4f3b056b5850a6b35159f37ad52 + languageName: node + linkType: hard + "@react-native-community/cli-config@npm:20.0.0": version: 20.0.0 resolution: "@react-native-community/cli-config@npm:20.0.0" @@ -5993,6 +6288,20 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-config@npm:20.0.1": + version: 20.0.1 + resolution: "@react-native-community/cli-config@npm:20.0.1" + dependencies: + "@react-native-community/cli-tools": "npm:20.0.1" + chalk: "npm:^4.1.2" + cosmiconfig: "npm:^9.0.0" + deepmerge: "npm:^4.3.0" + fast-glob: "npm:^3.3.2" + joi: "npm:^17.2.1" + checksum: 10/aac2bb49079585532af854ee3e29b18eba6cc94dcfae831c492379659292a93555dcbf690af14fecadb1ae0f0f9d08a6649e51f6821d3d9baf5983bccdb421da + languageName: node + linkType: hard + "@react-native-community/cli-doctor@npm:20.0.0": version: 20.0.0 resolution: "@react-native-community/cli-doctor@npm:20.0.0" @@ -6016,6 +6325,29 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-doctor@npm:20.0.1": + version: 20.0.1 + resolution: "@react-native-community/cli-doctor@npm:20.0.1" + dependencies: + "@react-native-community/cli-config": "npm:20.0.1" + "@react-native-community/cli-platform-android": "npm:20.0.1" + "@react-native-community/cli-platform-apple": "npm:20.0.1" + "@react-native-community/cli-platform-ios": "npm:20.0.1" + "@react-native-community/cli-tools": "npm:20.0.1" + chalk: "npm:^4.1.2" + command-exists: "npm:^1.2.8" + deepmerge: "npm:^4.3.0" + envinfo: "npm:^7.13.0" + execa: "npm:^5.0.0" + node-stream-zip: "npm:^1.9.1" + ora: "npm:^5.4.1" + semver: "npm:^7.5.2" + wcwidth: "npm:^1.0.1" + yaml: "npm:^2.2.1" + checksum: 10/3f70f8a9c98f1b4b39ff7073628e595620e24520a744af4c6ca3c453cd3f6df3486132fa344b6a0c2d4ff130de65f38e83498a04b9a8e61b1d6a5d080c0c34bc + languageName: node + linkType: hard + "@react-native-community/cli-platform-android@npm:20.0.0": version: 20.0.0 resolution: "@react-native-community/cli-platform-android@npm:20.0.0" @@ -6029,6 +6361,19 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-platform-android@npm:20.0.1": + version: 20.0.1 + resolution: "@react-native-community/cli-platform-android@npm:20.0.1" + dependencies: + "@react-native-community/cli-config-android": "npm:20.0.1" + "@react-native-community/cli-tools": "npm:20.0.1" + chalk: "npm:^4.1.2" + execa: "npm:^5.0.0" + logkitty: "npm:^0.7.1" + checksum: 10/d27eb40b7e7f76cc2f114a9b81600eae19b6bf08791c1e1a0feed6c602792a81e0a749e9e0e008e4eab5cf9b216d476b71de85cad03609343494a516902fb17f + languageName: node + linkType: hard + "@react-native-community/cli-platform-apple@npm:20.0.0": version: 20.0.0 resolution: "@react-native-community/cli-platform-apple@npm:20.0.0" @@ -6042,6 +6387,19 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-platform-apple@npm:20.0.1": + version: 20.0.1 + resolution: "@react-native-community/cli-platform-apple@npm:20.0.1" + dependencies: + "@react-native-community/cli-config-apple": "npm:20.0.1" + "@react-native-community/cli-tools": "npm:20.0.1" + chalk: "npm:^4.1.2" + execa: "npm:^5.0.0" + fast-xml-parser: "npm:^4.4.1" + checksum: 10/3f1c2dbfbdf4b89a9403500fbe9c850e8fbe15e3f8869ac063dca2b181f7f21c1d99c984ec517ee682d0163f5347857ecd31ebfff52e63565583fb358850458e + languageName: node + linkType: hard + "@react-native-community/cli-platform-ios@npm:20.0.0": version: 20.0.0 resolution: "@react-native-community/cli-platform-ios@npm:20.0.0" @@ -6051,6 +6409,15 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-platform-ios@npm:20.0.1": + version: 20.0.1 + resolution: "@react-native-community/cli-platform-ios@npm:20.0.1" + dependencies: + "@react-native-community/cli-platform-apple": "npm:20.0.1" + checksum: 10/8abfd1872cecfa6a70790bac831fa3fea6e19e58e3ff9a70475d65b1c17af7426f4a97ea786cfe428dbf1cf2e64bdf1899b9b30eb00445311e54c78163b701b4 + languageName: node + linkType: hard + "@react-native-community/cli-server-api@npm:20.0.0": version: 20.0.0 resolution: "@react-native-community/cli-server-api@npm:20.0.0" @@ -6069,6 +6436,24 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-server-api@npm:20.0.1": + version: 20.0.1 + resolution: "@react-native-community/cli-server-api@npm:20.0.1" + dependencies: + "@react-native-community/cli-tools": "npm:20.0.1" + body-parser: "npm:^1.20.3" + compression: "npm:^1.7.1" + connect: "npm:^3.6.5" + errorhandler: "npm:^1.5.1" + nocache: "npm:^3.0.1" + open: "npm:^6.2.0" + pretty-format: "npm:^29.7.0" + serve-static: "npm:^1.13.1" + ws: "npm:^6.2.3" + checksum: 10/36bf723828ff03ba86f33489c4296e2d46ba94b04edc3f5c23b680232a68a33547ea3d8b1fc7e8012c105ec97f6704e04d38e412c8e1349ca20157c55ea92616 + languageName: node + linkType: hard + "@react-native-community/cli-tools@npm:20.0.0": version: 20.0.0 resolution: "@react-native-community/cli-tools@npm:20.0.0" @@ -6087,6 +6472,24 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-tools@npm:20.0.1": + version: 20.0.1 + resolution: "@react-native-community/cli-tools@npm:20.0.1" + dependencies: + "@vscode/sudo-prompt": "npm:^9.0.0" + appdirsjs: "npm:^1.2.4" + chalk: "npm:^4.1.2" + execa: "npm:^5.0.0" + find-up: "npm:^5.0.0" + launch-editor: "npm:^2.9.1" + mime: "npm:^2.4.1" + ora: "npm:^5.4.1" + prompts: "npm:^2.4.2" + semver: "npm:^7.5.2" + checksum: 10/dacb2007419c2428cf7a91aeace8cb342c24bee48e09b9bc77cbef4c5b3c6ff67a60e8934d96e046aa00e4da0e2d0c0e70c761c0ab19e06554bdca6d3939e79f + languageName: node + linkType: hard + "@react-native-community/cli-types@npm:20.0.0": version: 20.0.0 resolution: "@react-native-community/cli-types@npm:20.0.0" @@ -6096,6 +6499,15 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-types@npm:20.0.1": + version: 20.0.1 + resolution: "@react-native-community/cli-types@npm:20.0.1" + dependencies: + joi: "npm:^17.2.1" + checksum: 10/8d5153b92b0b2ca409ae31eafa1ad1d5c332ead7a19b896e2be4003a3ae34aac6d999825f1281966e4ed5f88e0479f0b6147d6fb5e6fbb3b7ee378c725889ec1 + languageName: node + linkType: hard + "@react-native-community/cli@npm:20.0.0": version: 20.0.0 resolution: "@react-native-community/cli@npm:20.0.0" @@ -6117,11 +6529,36 @@ __metadata: semver: "npm:^7.5.2" bin: rnc-cli: build/bin.js - checksum: 10/9f52dce4f6c25d414ba4549153b70daef7708f5603f88f90147c9ec4dd29118f62995294eb20cb3f7c4018367bda66dac344d25ed0e9ffaad6b62a94b29ba75b + checksum: 10/9f52dce4f6c25d414ba4549153b70daef7708f5603f88f90147c9ec4dd29118f62995294eb20cb3f7c4018367bda66dac344d25ed0e9ffaad6b62a94b29ba75b + languageName: node + linkType: hard + +"@react-native-community/cli@npm:20.0.1": + version: 20.0.1 + resolution: "@react-native-community/cli@npm:20.0.1" + dependencies: + "@react-native-community/cli-clean": "npm:20.0.1" + "@react-native-community/cli-config": "npm:20.0.1" + "@react-native-community/cli-doctor": "npm:20.0.1" + "@react-native-community/cli-server-api": "npm:20.0.1" + "@react-native-community/cli-tools": "npm:20.0.1" + "@react-native-community/cli-types": "npm:20.0.1" + chalk: "npm:^4.1.2" + commander: "npm:^9.4.1" + deepmerge: "npm:^4.3.0" + execa: "npm:^5.0.0" + find-up: "npm:^5.0.0" + fs-extra: "npm:^8.1.0" + graceful-fs: "npm:^4.1.3" + prompts: "npm:^2.4.2" + semver: "npm:^7.5.2" + bin: + rnc-cli: build/bin.js + checksum: 10/63354c1f1b0bd8c69c15596026d8be950f7a8c570c6260d5d73c82e7e43e0f8e57d820ea68ac36140c13841cc54c64db18d1d0c1c6228f2342821f7c021ea393 languageName: node linkType: hard -"@react-native-community/netinfo@npm:11.4.1, @react-native-community/netinfo@npm:^11.4.1": +"@react-native-community/netinfo@npm:11.4.1": version: 11.4.1 resolution: "@react-native-community/netinfo@npm:11.4.1" peerDependencies: @@ -6130,7 +6567,17 @@ __metadata: languageName: node linkType: hard -"@react-native-community/push-notification-ios@npm:1.11.0, @react-native-community/push-notification-ios@npm:^1.11.0": +"@react-native-community/netinfo@npm:11.5.2, @react-native-community/netinfo@npm:^11.4.1": + version: 11.5.2 + resolution: "@react-native-community/netinfo@npm:11.5.2" + peerDependencies: + react: "*" + react-native: ">=0.59" + checksum: 10/0c388e1beb61d0135eb9303b18d7d953b0f777af15e34a8c0ab03f428669a987d658ead645a37b410afae7844374d07e7ab947897d5720b1e7fee5525fffd5b5 + languageName: node + linkType: hard + +"@react-native-community/push-notification-ios@npm:1.11.0": version: 1.11.0 resolution: "@react-native-community/push-notification-ios@npm:1.11.0" dependencies: @@ -6142,11 +6589,23 @@ __metadata: languageName: node linkType: hard -"@react-native-firebase/app@npm:^23.4.0": - version: 23.4.0 - resolution: "@react-native-firebase/app@npm:23.4.0" +"@react-native-community/push-notification-ios@npm:^1.11.0": + version: 1.12.0 + resolution: "@react-native-community/push-notification-ios@npm:1.12.0" + dependencies: + invariant: "npm:^2.2.4" + peerDependencies: + react: ">=16.6.3" + react-native: ">=0.58.4" + checksum: 10/c1eb786cf548fd559f779d3af75ab47eee62d74b3f8affeb4032a9891ba5f0f53df280299d0506623a04b375a2c6c4903e1ed5b55377c0a478e2e3af4248f02c + languageName: node + linkType: hard + +"@react-native-firebase/app@npm:^23.4.0, @react-native-firebase/app@npm:~23.7.0": + version: 23.7.0 + resolution: "@react-native-firebase/app@npm:23.7.0" dependencies: - firebase: "npm:12.2.1" + firebase: "npm:12.6.0" peerDependencies: expo: ">=47.0.0" react: "*" @@ -6154,20 +6613,20 @@ __metadata: peerDependenciesMeta: expo: optional: true - checksum: 10/9b876eae7b13b35b4e0e2f05f697087c1f28c0fbd425302ddd8b917c114a7759db88b013b139e1c41e7d9f85701d69c304be1b85595e11ec12ffdf06da5272e8 + checksum: 10/f5468bca4ed1046f955c0e2ad863c58e40867db7cdd07a1632fda372086c2105248866fdaad3325106a32bfece6d513fd41255abe41a24c52b125ef2f92dc450 languageName: node linkType: hard -"@react-native-firebase/messaging@npm:^23.4.0": - version: 23.4.0 - resolution: "@react-native-firebase/messaging@npm:23.4.0" +"@react-native-firebase/messaging@npm:^23.4.0, @react-native-firebase/messaging@npm:~23.7.0": + version: 23.7.0 + resolution: "@react-native-firebase/messaging@npm:23.7.0" peerDependencies: - "@react-native-firebase/app": 23.4.0 + "@react-native-firebase/app": 23.7.0 expo: ">=47.0.0" peerDependenciesMeta: expo: optional: true - checksum: 10/310723738ee3f823bdc2e72a429d5a739f9bff3b0e369d29b6c8f8fec73223659558be3ccaf147f6c36bf68b5e80e0e804c23182bfdcc3ad3c1c88ecd06c5b41 + checksum: 10/52dd1a3bc101bc40fb8f31be9f55f2ac85fa58f9d453b8faff9090109e33b029f14275ee22eff4a8da45f6a932dc097135b42ccc8c3a914252168e3c1396bf45 languageName: node linkType: hard @@ -6178,10 +6637,10 @@ __metadata: languageName: node linkType: hard -"@react-native/assets-registry@npm:0.83.2": - version: 0.83.2 - resolution: "@react-native/assets-registry@npm:0.83.2" - checksum: 10/62a4bfd803209795079878ed57ea9275c50added84b3ad514ffae43b0036f7e3319b0241c47f29f454d52b4739c42bf5e0171205697c2b8b45366b37bfca7e1d +"@react-native/assets-registry@npm:0.83.4": + version: 0.83.4 + resolution: "@react-native/assets-registry@npm:0.83.4" + checksum: 10/4c6c04a089ce8d232a8eea473051bd01a2a2a6c67e077f0a155fbc59196a32bc317e97130b80e3d06472e4aff3ee3592f775a6df0db1fd2ee86f9f60b4942d68 languageName: node linkType: hard @@ -6215,6 +6674,16 @@ __metadata: languageName: node linkType: hard +"@react-native/babel-plugin-codegen@npm:0.83.4": + version: 0.83.4 + resolution: "@react-native/babel-plugin-codegen@npm:0.83.4" + dependencies: + "@babel/traverse": "npm:^7.25.3" + "@react-native/codegen": "npm:0.83.4" + checksum: 10/484cb8b9817abd39f0c234b4589e55f04f67ed73806de6d04b0053568cab4ef466fc92047c9b5025dc8b18cbebcf8f27cd6637dd1dfe3a122d3b39bdb476da9d + languageName: node + linkType: hard + "@react-native/babel-preset@npm:0.81.4": version: 0.81.4 resolution: "@react-native/babel-preset@npm:0.81.4" @@ -6270,7 +6739,7 @@ __metadata: languageName: node linkType: hard -"@react-native/babel-preset@npm:0.83.2, @react-native/babel-preset@npm:^0.83.2": +"@react-native/babel-preset@npm:0.83.2": version: 0.83.2 resolution: "@react-native/babel-preset@npm:0.83.2" dependencies: @@ -6325,6 +6794,61 @@ __metadata: languageName: node linkType: hard +"@react-native/babel-preset@npm:0.83.4, @react-native/babel-preset@npm:^0.83.2": + version: 0.83.4 + resolution: "@react-native/babel-preset@npm:0.83.4" + dependencies: + "@babel/core": "npm:^7.25.2" + "@babel/plugin-proposal-export-default-from": "npm:^7.24.7" + "@babel/plugin-syntax-dynamic-import": "npm:^7.8.3" + "@babel/plugin-syntax-export-default-from": "npm:^7.24.7" + "@babel/plugin-syntax-nullish-coalescing-operator": "npm:^7.8.3" + "@babel/plugin-syntax-optional-chaining": "npm:^7.8.3" + "@babel/plugin-transform-arrow-functions": "npm:^7.24.7" + "@babel/plugin-transform-async-generator-functions": "npm:^7.25.4" + "@babel/plugin-transform-async-to-generator": "npm:^7.24.7" + "@babel/plugin-transform-block-scoping": "npm:^7.25.0" + "@babel/plugin-transform-class-properties": "npm:^7.25.4" + "@babel/plugin-transform-classes": "npm:^7.25.4" + "@babel/plugin-transform-computed-properties": "npm:^7.24.7" + "@babel/plugin-transform-destructuring": "npm:^7.24.8" + "@babel/plugin-transform-flow-strip-types": "npm:^7.25.2" + "@babel/plugin-transform-for-of": "npm:^7.24.7" + "@babel/plugin-transform-function-name": "npm:^7.25.1" + "@babel/plugin-transform-literals": "npm:^7.25.2" + "@babel/plugin-transform-logical-assignment-operators": "npm:^7.24.7" + "@babel/plugin-transform-modules-commonjs": "npm:^7.24.8" + "@babel/plugin-transform-named-capturing-groups-regex": "npm:^7.24.7" + "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.24.7" + "@babel/plugin-transform-numeric-separator": "npm:^7.24.7" + "@babel/plugin-transform-object-rest-spread": "npm:^7.24.7" + "@babel/plugin-transform-optional-catch-binding": "npm:^7.24.7" + "@babel/plugin-transform-optional-chaining": "npm:^7.24.8" + "@babel/plugin-transform-parameters": "npm:^7.24.7" + "@babel/plugin-transform-private-methods": "npm:^7.24.7" + "@babel/plugin-transform-private-property-in-object": "npm:^7.24.7" + "@babel/plugin-transform-react-display-name": "npm:^7.24.7" + "@babel/plugin-transform-react-jsx": "npm:^7.25.2" + "@babel/plugin-transform-react-jsx-self": "npm:^7.24.7" + "@babel/plugin-transform-react-jsx-source": "npm:^7.24.7" + "@babel/plugin-transform-regenerator": "npm:^7.24.7" + "@babel/plugin-transform-runtime": "npm:^7.24.7" + "@babel/plugin-transform-shorthand-properties": "npm:^7.24.7" + "@babel/plugin-transform-spread": "npm:^7.24.7" + "@babel/plugin-transform-sticky-regex": "npm:^7.24.7" + "@babel/plugin-transform-typescript": "npm:^7.25.2" + "@babel/plugin-transform-unicode-regex": "npm:^7.24.7" + "@babel/template": "npm:^7.25.0" + "@react-native/babel-plugin-codegen": "npm:0.83.4" + babel-plugin-syntax-hermes-parser: "npm:0.32.0" + babel-plugin-transform-flow-enums: "npm:^0.0.2" + react-refresh: "npm:^0.14.0" + peerDependencies: + "@babel/core": "*" + checksum: 10/bc72958ea0340af26842c88c5f75f7fc8ae7cd0719f01bfbf7ae583b2d8b04a460c93f7f796acf41167cd82fcacdb1a35f1f8541df655db882223b491e0b0fbc + languageName: node + linkType: hard + "@react-native/babel-preset@npm:^0.81.5": version: 0.81.5 resolution: "@react-native/babel-preset@npm:0.81.5" @@ -6431,6 +6955,23 @@ __metadata: languageName: node linkType: hard +"@react-native/codegen@npm:0.83.4": + version: 0.83.4 + resolution: "@react-native/codegen@npm:0.83.4" + dependencies: + "@babel/core": "npm:^7.25.2" + "@babel/parser": "npm:^7.25.3" + glob: "npm:^7.1.1" + hermes-parser: "npm:0.32.0" + invariant: "npm:^2.2.4" + nullthrows: "npm:^1.1.1" + yargs: "npm:^17.6.2" + peerDependencies: + "@babel/core": "*" + checksum: 10/04cb29351cab076377e034f036a43be0a491c1fc2f39ff5637ce778483f59232d1efc179ab940e60d9e30c788717b7fd2616c4c22ae4743a86d1585fbf8b6005 + languageName: node + linkType: hard + "@react-native/community-cli-plugin@npm:0.81.5": version: 0.81.5 resolution: "@react-native/community-cli-plugin@npm:0.81.5" @@ -6454,11 +6995,11 @@ __metadata: languageName: node linkType: hard -"@react-native/community-cli-plugin@npm:0.83.2": - version: 0.83.2 - resolution: "@react-native/community-cli-plugin@npm:0.83.2" +"@react-native/community-cli-plugin@npm:0.83.4": + version: 0.83.4 + resolution: "@react-native/community-cli-plugin@npm:0.83.4" dependencies: - "@react-native/dev-middleware": "npm:0.83.2" + "@react-native/dev-middleware": "npm:0.83.4" debug: "npm:^4.4.0" invariant: "npm:^2.2.4" metro: "npm:^0.83.3" @@ -6473,7 +7014,7 @@ __metadata: optional: true "@react-native/metro-config": optional: true - checksum: 10/2683c34c2c8c56fa9d765baf97701893ff57816f606d115938be0684ac7829782721d927d7957e7075d8399d7cbfa71af205f79fae3bc7e633b5c54ed1de8dbb + checksum: 10/91bec32c6bafd75753401f02188e10875ed364a4a51411634ea2d84d1e9ae5597c5a46d5c2065a5a0fb033a40c6812268aa42f25b554d8efb772358b3490c25b languageName: node linkType: hard @@ -6491,20 +7032,20 @@ __metadata: languageName: node linkType: hard -"@react-native/debugger-frontend@npm:0.83.2": - version: 0.83.2 - resolution: "@react-native/debugger-frontend@npm:0.83.2" - checksum: 10/17e9452c73fc464daa13655d8a9e5868298e47b6b3e67ca41d624f176c6c493ae37c4a56a4b1106edf0a85902127501e8e29d1bf70dcf5205bba7a81a304f359 +"@react-native/debugger-frontend@npm:0.83.4": + version: 0.83.4 + resolution: "@react-native/debugger-frontend@npm:0.83.4" + checksum: 10/c664a7686d7d7da26c2622861f2e4ba948f71099fc7c52034cb9a8862760b9b05704a13c5f6235665d5397ce191df84a287634351c398239038b4f133ad5eb45 languageName: node linkType: hard -"@react-native/debugger-shell@npm:0.83.2": - version: 0.83.2 - resolution: "@react-native/debugger-shell@npm:0.83.2" +"@react-native/debugger-shell@npm:0.83.4": + version: 0.83.4 + resolution: "@react-native/debugger-shell@npm:0.83.4" dependencies: cross-spawn: "npm:^7.0.6" fb-dotslash: "npm:0.5.8" - checksum: 10/214590025f5dd7781dc906c2945dd21f595bc5534c807e2af29133175dcb05d9be979a95857ff186bcc97cc307a99d26a6b993798c643cee7d5203bb56c64b47 + checksum: 10/b47d13946cc9effc094962ab4fbfb2f024ebe51f8cd530ede338475f2cc1030448dd3134a73ea96410cede9512540922b56dab0a49fc94c8dbf8854d116b9780 languageName: node linkType: hard @@ -6546,13 +7087,13 @@ __metadata: languageName: node linkType: hard -"@react-native/dev-middleware@npm:0.83.2": - version: 0.83.2 - resolution: "@react-native/dev-middleware@npm:0.83.2" +"@react-native/dev-middleware@npm:0.83.4": + version: 0.83.4 + resolution: "@react-native/dev-middleware@npm:0.83.4" dependencies: "@isaacs/ttlcache": "npm:^1.4.1" - "@react-native/debugger-frontend": "npm:0.83.2" - "@react-native/debugger-shell": "npm:0.83.2" + "@react-native/debugger-frontend": "npm:0.83.4" + "@react-native/debugger-shell": "npm:0.83.4" chrome-launcher: "npm:^0.15.2" chromium-edge-launcher: "npm:^0.2.0" connect: "npm:^3.6.5" @@ -6562,7 +7103,7 @@ __metadata: open: "npm:^7.0.3" serve-static: "npm:^1.16.2" ws: "npm:^7.5.10" - checksum: 10/cb5f90aa8c64c20efeaa36a9cc66ea8e7180a8d5b6c1068d52f42e269d4786049829a9b620f5090542c48f0dbb4100527bb61fbe0db8b39050cfc0f76a94388e + checksum: 10/c0313ffdf9d5ea48cd31e5404574ae33195530aae0ce21e6017165325e2e4359163a5dd33807f0d0d0408b263361621f91a9254b3b12b900129ccc7bb7bdb92c languageName: node linkType: hard @@ -6573,10 +7114,10 @@ __metadata: languageName: node linkType: hard -"@react-native/gradle-plugin@npm:0.83.2": - version: 0.83.2 - resolution: "@react-native/gradle-plugin@npm:0.83.2" - checksum: 10/09517663800636f2352ce95c183e51c5c69037baf93bf6cf3cad947fe062510b77aca38dc8e85164fad54c73db8a8e83968652f76ee1d9965e2e417da45522c0 +"@react-native/gradle-plugin@npm:0.83.4": + version: 0.83.4 + resolution: "@react-native/gradle-plugin@npm:0.83.4" + checksum: 10/df7bfa8d298a1cd3e465f724a11741c934855d3c255264464e63c3807028bceb912c1f014554b5413b7ac9230d76dfe1cfcc73901ef7f987af75d7a2b9fba963 languageName: node linkType: hard @@ -6594,6 +7135,13 @@ __metadata: languageName: node linkType: hard +"@react-native/js-polyfills@npm:0.83.4": + version: 0.83.4 + resolution: "@react-native/js-polyfills@npm:0.83.4" + checksum: 10/cf9a0985979cb7da7fa4be6a002d0e0ef8fc10efcc6c9f0d98dd0683bbe253240fd642b471dc68b9c376935ca027d01649eb3b2e08b45301b9a030a49c3e7f8c + languageName: node + linkType: hard + "@react-native/metro-babel-transformer@npm:0.83.2": version: 0.83.2 resolution: "@react-native/metro-babel-transformer@npm:0.83.2" @@ -6634,10 +7182,10 @@ __metadata: languageName: node linkType: hard -"@react-native/normalize-colors@npm:0.83.2": - version: 0.83.2 - resolution: "@react-native/normalize-colors@npm:0.83.2" - checksum: 10/57e09d151ac697b55207fd9ef47de79598682610c13d6d6c40be2de40abd1c0bc2b52e8acadead779ab19538e8a56e17f205a3c13ab7fa51b368e342c9f94d08 +"@react-native/normalize-colors@npm:0.83.4": + version: 0.83.4 + resolution: "@react-native/normalize-colors@npm:0.83.4" + checksum: 10/3283dfa32db801adf91268701f934254a50a4146ffb8ab0ef125e75564e98a8cc545981980c3afcf383bd58b07b56eba814e6630d3b03a6155482dce4f1fb9e6 languageName: node linkType: hard @@ -6672,9 +7220,9 @@ __metadata: languageName: node linkType: hard -"@react-native/virtualized-lists@npm:0.83.2": - version: 0.83.2 - resolution: "@react-native/virtualized-lists@npm:0.83.2" +"@react-native/virtualized-lists@npm:0.83.4": + version: 0.83.4 + resolution: "@react-native/virtualized-lists@npm:0.83.4" dependencies: invariant: "npm:^2.2.4" nullthrows: "npm:^1.1.1" @@ -6685,32 +7233,34 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 10/ba4d794330f869f51565abb1717af929a817bcfdfabb6684b3307e4289706345db174062ed79ca6b340f5a7b2952e02e62a821cfb86bc24f41bbf2ee931a882c + checksum: 10/44798853aeed19b06fd1267cd2f85dab0be07919aa8124ec106474c49aeb02849f78eb134efccd1e5443367d431e3bd2511c142ce961a37dbdfa0a6843a8369d languageName: node linkType: hard -"@react-navigation/bottom-tabs@npm:^7.4.0, @react-navigation/bottom-tabs@npm:^7.4.8": - version: 7.4.8 - resolution: "@react-navigation/bottom-tabs@npm:7.4.8" +"@react-navigation/bottom-tabs@npm:^7.15.5, @react-navigation/bottom-tabs@npm:^7.4.0, @react-navigation/bottom-tabs@npm:^7.4.8": + version: 7.15.9 + resolution: "@react-navigation/bottom-tabs@npm:7.15.9" dependencies: - "@react-navigation/elements": "npm:^2.6.5" + "@react-navigation/elements": "npm:^2.9.14" color: "npm:^4.2.3" + sf-symbols-typescript: "npm:^2.1.0" peerDependencies: - "@react-navigation/native": ^7.1.18 + "@react-navigation/native": ^7.2.2 react: ">= 18.2.0" react-native: "*" react-native-safe-area-context: ">= 4.0.0" react-native-screens: ">= 4.0.0" - checksum: 10/6b1943cc6a7cd2be35c9e40aee60cfb04e0af50ed93c5d48be6e0c0ad723c486b2c3bf382d8038c4b7d2bfefe6d32037db903e29bdd4fcd5748458f8c8ca5dc2 + checksum: 10/8f99aee876b2974e197cde8e4a0a595a2c85dad3115b8de2a8f168c894fd080222f15383d999c69d49d18c103e441b736389ae6881a727e96f9ca448236c3d56 languageName: node linkType: hard -"@react-navigation/core@npm:^7.12.4": - version: 7.12.4 - resolution: "@react-navigation/core@npm:7.12.4" +"@react-navigation/core@npm:^7.17.2": + version: 7.17.2 + resolution: "@react-navigation/core@npm:7.17.2" dependencies: - "@react-navigation/routers": "npm:^7.5.1" + "@react-navigation/routers": "npm:^7.5.3" escape-string-regexp: "npm:^4.0.0" + fast-deep-equal: "npm:^3.1.3" nanoid: "npm:^3.3.11" query-string: "npm:^7.1.3" react-is: "npm:^19.1.0" @@ -6718,51 +7268,53 @@ __metadata: use-sync-external-store: "npm:^1.5.0" peerDependencies: react: ">= 18.2.0" - checksum: 10/6258d645be5d3b29293a7f82a7fbcfbefec93f974b332e7b1c510b4557f772955f227be50d020679d5ea3ba764a34ad3ff3ada531a4b85d9b4395ee02cdc3777 + checksum: 10/79d63f1e6f50a63bcaa21dd920a29fde2c88845e12dead83c9abf9ff3b837f993b9179685bcc6c7f21d4c2ccde60abfc57853e62b9dd397264455a867fd92ec3 languageName: node linkType: hard -"@react-navigation/elements@npm:^2.6.5": - version: 2.6.5 - resolution: "@react-navigation/elements@npm:2.6.5" +"@react-navigation/elements@npm:^2.9.14": + version: 2.9.14 + resolution: "@react-navigation/elements@npm:2.9.14" dependencies: color: "npm:^4.2.3" use-latest-callback: "npm:^0.2.4" use-sync-external-store: "npm:^1.5.0" peerDependencies: "@react-native-masked-view/masked-view": ">= 0.2.0" - "@react-navigation/native": ^7.1.18 + "@react-navigation/native": ^7.2.2 react: ">= 18.2.0" react-native: "*" react-native-safe-area-context: ">= 4.0.0" peerDependenciesMeta: "@react-native-masked-view/masked-view": optional: true - checksum: 10/b939f1dc1981c12379cea7ee26348e29bff94de6f29fefe74e8f1d311662d2235e6d87322c5e27e10a59648b88fa5648cb64c646daf65e94dd2664f060408ed2 + checksum: 10/4c466b3017e4380ed867303740af4b8ad06fda8eb8a262ccebe09664327cf60b659a24ffd3d82fb62d7586d1584af7e1da0f712479494edcee9213dfe02df565 languageName: node linkType: hard -"@react-navigation/native-stack@npm:^7.3.16, @react-navigation/native-stack@npm:^7.3.27": - version: 7.3.27 - resolution: "@react-navigation/native-stack@npm:7.3.27" +"@react-navigation/native-stack@npm:^7.14.5, @react-navigation/native-stack@npm:^7.3.16, @react-navigation/native-stack@npm:^7.3.27": + version: 7.14.10 + resolution: "@react-navigation/native-stack@npm:7.14.10" dependencies: - "@react-navigation/elements": "npm:^2.6.5" + "@react-navigation/elements": "npm:^2.9.14" + color: "npm:^4.2.3" + sf-symbols-typescript: "npm:^2.1.0" warn-once: "npm:^0.1.1" peerDependencies: - "@react-navigation/native": ^7.1.18 + "@react-navigation/native": ^7.2.2 react: ">= 18.2.0" react-native: "*" react-native-safe-area-context: ">= 4.0.0" react-native-screens: ">= 4.0.0" - checksum: 10/89f13700464d8938210471f4c9c4599e0070e7159b0659aef2f78f328684f73f00021e3bccc3b86f6bd83a26f7e91512292dab1e433409b0143bbc80f2f58689 + checksum: 10/aebb86454e364f8eaa8e5d2ce576a6445e28d5999cb3dceba786ffe03d43dd1b508ae3f82cdc193405d2a94e93a420a1761efa7e564ae1be2ed96c4823528b36 languageName: node linkType: hard -"@react-navigation/native@npm:^7.1.18, @react-navigation/native@npm:^7.1.8": - version: 7.1.18 - resolution: "@react-navigation/native@npm:7.1.18" +"@react-navigation/native@npm:^7.1.18, @react-navigation/native@npm:^7.1.33, @react-navigation/native@npm:^7.1.8": + version: 7.2.2 + resolution: "@react-navigation/native@npm:7.2.2" dependencies: - "@react-navigation/core": "npm:^7.12.4" + "@react-navigation/core": "npm:^7.17.2" escape-string-regexp: "npm:^4.0.0" fast-deep-equal: "npm:^3.1.3" nanoid: "npm:^3.3.11" @@ -6770,16 +7322,16 @@ __metadata: peerDependencies: react: ">= 18.2.0" react-native: "*" - checksum: 10/b867e9c5164943cd22a3d39d563dd8986b7f012ea9dac6b2df0b0321c42b2c00a1ab3692a372fa31c77a5335f3a78cf6967ee7186fe5322237f8f95035f1f0c5 + checksum: 10/83e8c4f9979378a932c7cce8106e2fde1774b5ff902e9da9b96e0a188183706a9e24bcfbd0f3e63fb5a5211617e66d69c8c0ee89696f93c12c1c4ed4998e9d00 languageName: node linkType: hard -"@react-navigation/routers@npm:^7.5.1": - version: 7.5.1 - resolution: "@react-navigation/routers@npm:7.5.1" +"@react-navigation/routers@npm:^7.5.3": + version: 7.5.3 + resolution: "@react-navigation/routers@npm:7.5.3" dependencies: nanoid: "npm:^3.3.11" - checksum: 10/b2f41b084d9ff69ac934e798fabca149a7d2cfc6ca1899d9ffbb68f8378c02277752e68783c264ea5068be9c8738d0d5112abb177c00c0365cfd2a133d560a8c + checksum: 10/8b02cf4c9acd7d1ccb0771ebfbf18fa27aa8db4e5653403d9d78a08d1792b9f22654cb36ce3a1150181b141d8cf694d7665007ef005c041bce404d33f44acc73 languageName: node linkType: hard @@ -6880,9 +7432,11 @@ __metadata: linkType: hard "@rnx-kit/tools-node@npm:^3.0.0": - version: 3.0.2 - resolution: "@rnx-kit/tools-node@npm:3.0.2" - checksum: 10/427db5099999463de60cf72b62e771b1ef9178900aa9e6b433e0c754c19255e50720ca37f07bdea4fc7a46fb20f94c432b66c6dc4015c40eec3936bd970e3988 + version: 3.0.4 + resolution: "@rnx-kit/tools-node@npm:3.0.4" + dependencies: + "@rnx-kit/types-node": "npm:^1.0.0" + checksum: 10/8656900707b9f8ebcb0c67ffb5beb0c111421029b686a05de501484252fbe45393f53bd9abb4d384f092d2eff21d574f381f8b0c33d1cee43c0e414d15fc17a3 languageName: node linkType: hard @@ -6908,6 +7462,69 @@ __metadata: languageName: node linkType: hard +"@rnx-kit/types-bundle-config@npm:^1.0.0": + version: 1.0.0 + resolution: "@rnx-kit/types-bundle-config@npm:1.0.0" + dependencies: + "@rnx-kit/types-metro-serializer-esbuild": "npm:^1.0.0" + "@rnx-kit/types-plugin-cyclic-dependencies": "npm:^1.0.0" + "@rnx-kit/types-plugin-duplicates-checker": "npm:^1.0.0" + "@rnx-kit/types-plugin-typescript": "npm:^1.0.0" + peerDependencies: + metro: ">=0.83.0" + peerDependenciesMeta: + metro: + optional: true + checksum: 10/ebc943dd388452d79821a752509d68968d1297e59412fdf84bb6ccd5844ed97a2cfc3b6ffedaa6c66654b75960026208ea53110a6b76cd5f55014e10f2a7a766 + languageName: node + linkType: hard + +"@rnx-kit/types-kit-config@npm:^1.0.0": + version: 1.0.0 + resolution: "@rnx-kit/types-kit-config@npm:1.0.0" + dependencies: + "@rnx-kit/types-bundle-config": "npm:^1.0.0" + checksum: 10/2ccdde05b015e9e2b95156db3cfc313f81ad6f843e23cc5832cd99b9f2cdcae344adbfe25f93cff8db2572448b986daf964f6e2d2e5e52d75f2caef02795e5a8 + languageName: node + linkType: hard + +"@rnx-kit/types-metro-serializer-esbuild@npm:^1.0.0": + version: 1.0.1 + resolution: "@rnx-kit/types-metro-serializer-esbuild@npm:1.0.1" + checksum: 10/edf0948166d0906c77d795ae6f4e431bf5095ff3fe4a403d0e6e5f05a9385fda88f95270d95dbcce072ca5f902f455d6024eda917bfadb1e47af048f54e81d69 + languageName: node + linkType: hard + +"@rnx-kit/types-node@npm:^1.0.0": + version: 1.0.0 + resolution: "@rnx-kit/types-node@npm:1.0.0" + dependencies: + "@rnx-kit/types-kit-config": "npm:^1.0.0" + checksum: 10/c29959aee3323b8e374dac4f556559c97b7c9878982a8b3bb131646ea57575f606df6c7e24298e12f66f87715b9a514dc24e2923399ffcda6e8b4936df9cb0f1 + languageName: node + linkType: hard + +"@rnx-kit/types-plugin-cyclic-dependencies@npm:^1.0.0": + version: 1.0.0 + resolution: "@rnx-kit/types-plugin-cyclic-dependencies@npm:1.0.0" + checksum: 10/780a03fb6c58ccb82394ea7258bcff9274b6ddf6eed8598dea5eb6a6cfff683574ae87591c9134f3cb3b8f7aceea99dfe4a362c19dcd9f71eef8306f08b43e9c + languageName: node + linkType: hard + +"@rnx-kit/types-plugin-duplicates-checker@npm:^1.0.0": + version: 1.0.0 + resolution: "@rnx-kit/types-plugin-duplicates-checker@npm:1.0.0" + checksum: 10/8522f20ff06857444417205226625926ccb68b2ae683e51deba30bcb62121e3498eed7347fb51b12e36c11831d3fd4e41fc551086ac23c6b0d4ec47906f763ba + languageName: node + linkType: hard + +"@rnx-kit/types-plugin-typescript@npm:^1.0.0": + version: 1.0.0 + resolution: "@rnx-kit/types-plugin-typescript@npm:1.0.0" + checksum: 10/db4479db3afa7cebcc7970ddad36c6f3a13add110723fadaa7cb77be31a2f9233dfabfddf36239f98deed17a9bc91a06b9a42f864cd8fd49abd059f8c64abff5 + languageName: node + linkType: hard + "@rolldown/pluginutils@npm:1.0.0-beta.38": version: 1.0.0-beta.38 resolution: "@rolldown/pluginutils@npm:1.0.0-beta.38" @@ -7193,6 +7810,15 @@ __metadata: languageName: node linkType: hard +"@sentry-internal/browser-utils@npm:10.47.0": + version: 10.47.0 + resolution: "@sentry-internal/browser-utils@npm:10.47.0" + dependencies: + "@sentry/core": "npm:10.47.0" + checksum: 10/34d97af72050fdfade0704473f97cc54bff44e9ba091925b38dc7f70a4debb2d39b1319c7232e84dfa03f38207cf363a9f150ec443983c646c786c03438635f0 + languageName: node + linkType: hard + "@sentry-internal/feedback@npm:10.30.0": version: 10.30.0 resolution: "@sentry-internal/feedback@npm:10.30.0" @@ -7202,6 +7828,15 @@ __metadata: languageName: node linkType: hard +"@sentry-internal/feedback@npm:10.47.0": + version: 10.47.0 + resolution: "@sentry-internal/feedback@npm:10.47.0" + dependencies: + "@sentry/core": "npm:10.47.0" + checksum: 10/60c169aa1e061d2372d074c74a666baf65b4d57eb29a1238c2368c51f133dacb60b0358fc32b7cf29014018ade5a42f36afa2ff8a2ea6065bbcde3037da6e166 + languageName: node + linkType: hard + "@sentry-internal/replay-canvas@npm:10.30.0": version: 10.30.0 resolution: "@sentry-internal/replay-canvas@npm:10.30.0" @@ -7212,6 +7847,16 @@ __metadata: languageName: node linkType: hard +"@sentry-internal/replay-canvas@npm:10.47.0": + version: 10.47.0 + resolution: "@sentry-internal/replay-canvas@npm:10.47.0" + dependencies: + "@sentry-internal/replay": "npm:10.47.0" + "@sentry/core": "npm:10.47.0" + checksum: 10/ce292c302d8b10195a302b443f06b4842588ef1ce253856ff1e2238d9002164e1876e9538d64f45ff48aef45f9d37ee9cf5d3a5cb339a5d5483b244ca8d36d3b + languageName: node + linkType: hard + "@sentry-internal/replay@npm:10.30.0": version: 10.30.0 resolution: "@sentry-internal/replay@npm:10.30.0" @@ -7222,6 +7867,16 @@ __metadata: languageName: node linkType: hard +"@sentry-internal/replay@npm:10.47.0": + version: 10.47.0 + resolution: "@sentry-internal/replay@npm:10.47.0" + dependencies: + "@sentry-internal/browser-utils": "npm:10.47.0" + "@sentry/core": "npm:10.47.0" + checksum: 10/4077d8eba4a3424009663c9be6d29d30adf7c6d93d8cea7a9851a57a7137c95bba4693ef09921ed11656d6cae366d8b6082fe813304fb0d2e7b7104e3ffd0b8f + languageName: node + linkType: hard + "@sentry/babel-plugin-component-annotate@npm:4.6.1": version: 4.6.1 resolution: "@sentry/babel-plugin-component-annotate@npm:4.6.1" @@ -7242,6 +7897,19 @@ __metadata: languageName: node linkType: hard +"@sentry/browser@npm:10.47.0": + version: 10.47.0 + resolution: "@sentry/browser@npm:10.47.0" + dependencies: + "@sentry-internal/browser-utils": "npm:10.47.0" + "@sentry-internal/feedback": "npm:10.47.0" + "@sentry-internal/replay": "npm:10.47.0" + "@sentry-internal/replay-canvas": "npm:10.47.0" + "@sentry/core": "npm:10.47.0" + checksum: 10/09b833ed47789fe3dd8382f56e2d6c8ce7ec8f1231469f16c1555476c5eb9a811a13a8eae1dff97a82f5dbb0893425f11fb0fb99842c2bb43c0d358c500780c3 + languageName: node + linkType: hard + "@sentry/bundler-plugin-core@npm:4.6.1, @sentry/bundler-plugin-core@npm:^4.6.1": version: 4.6.1 resolution: "@sentry/bundler-plugin-core@npm:4.6.1" @@ -7258,74 +7926,74 @@ __metadata: languageName: node linkType: hard -"@sentry/cli-darwin@npm:2.58.3": - version: 2.58.3 - resolution: "@sentry/cli-darwin@npm:2.58.3" +"@sentry/cli-darwin@npm:2.58.5": + version: 2.58.5 + resolution: "@sentry/cli-darwin@npm:2.58.5" conditions: os=darwin languageName: node linkType: hard -"@sentry/cli-linux-arm64@npm:2.58.3": - version: 2.58.3 - resolution: "@sentry/cli-linux-arm64@npm:2.58.3" +"@sentry/cli-linux-arm64@npm:2.58.5": + version: 2.58.5 + resolution: "@sentry/cli-linux-arm64@npm:2.58.5" conditions: (os=linux | os=freebsd | os=android) & cpu=arm64 languageName: node linkType: hard -"@sentry/cli-linux-arm@npm:2.58.3": - version: 2.58.3 - resolution: "@sentry/cli-linux-arm@npm:2.58.3" +"@sentry/cli-linux-arm@npm:2.58.5": + version: 2.58.5 + resolution: "@sentry/cli-linux-arm@npm:2.58.5" conditions: (os=linux | os=freebsd | os=android) & cpu=arm languageName: node linkType: hard -"@sentry/cli-linux-i686@npm:2.58.3": - version: 2.58.3 - resolution: "@sentry/cli-linux-i686@npm:2.58.3" +"@sentry/cli-linux-i686@npm:2.58.5": + version: 2.58.5 + resolution: "@sentry/cli-linux-i686@npm:2.58.5" conditions: (os=linux | os=freebsd | os=android) & (cpu=x86 | cpu=ia32) languageName: node linkType: hard -"@sentry/cli-linux-x64@npm:2.58.3": - version: 2.58.3 - resolution: "@sentry/cli-linux-x64@npm:2.58.3" +"@sentry/cli-linux-x64@npm:2.58.5": + version: 2.58.5 + resolution: "@sentry/cli-linux-x64@npm:2.58.5" conditions: (os=linux | os=freebsd | os=android) & cpu=x64 languageName: node linkType: hard -"@sentry/cli-win32-arm64@npm:2.58.3": - version: 2.58.3 - resolution: "@sentry/cli-win32-arm64@npm:2.58.3" +"@sentry/cli-win32-arm64@npm:2.58.5": + version: 2.58.5 + resolution: "@sentry/cli-win32-arm64@npm:2.58.5" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@sentry/cli-win32-i686@npm:2.58.3": - version: 2.58.3 - resolution: "@sentry/cli-win32-i686@npm:2.58.3" +"@sentry/cli-win32-i686@npm:2.58.5": + version: 2.58.5 + resolution: "@sentry/cli-win32-i686@npm:2.58.5" conditions: os=win32 & (cpu=x86 | cpu=ia32) languageName: node linkType: hard -"@sentry/cli-win32-x64@npm:2.58.3": - version: 2.58.3 - resolution: "@sentry/cli-win32-x64@npm:2.58.3" +"@sentry/cli-win32-x64@npm:2.58.5": + version: 2.58.5 + resolution: "@sentry/cli-win32-x64@npm:2.58.5" conditions: os=win32 & cpu=x64 languageName: node linkType: hard "@sentry/cli@npm:^2.57.0": - version: 2.58.3 - resolution: "@sentry/cli@npm:2.58.3" - dependencies: - "@sentry/cli-darwin": "npm:2.58.3" - "@sentry/cli-linux-arm": "npm:2.58.3" - "@sentry/cli-linux-arm64": "npm:2.58.3" - "@sentry/cli-linux-i686": "npm:2.58.3" - "@sentry/cli-linux-x64": "npm:2.58.3" - "@sentry/cli-win32-arm64": "npm:2.58.3" - "@sentry/cli-win32-i686": "npm:2.58.3" - "@sentry/cli-win32-x64": "npm:2.58.3" + version: 2.58.5 + resolution: "@sentry/cli@npm:2.58.5" + dependencies: + "@sentry/cli-darwin": "npm:2.58.5" + "@sentry/cli-linux-arm": "npm:2.58.5" + "@sentry/cli-linux-arm64": "npm:2.58.5" + "@sentry/cli-linux-i686": "npm:2.58.5" + "@sentry/cli-linux-x64": "npm:2.58.5" + "@sentry/cli-win32-arm64": "npm:2.58.5" + "@sentry/cli-win32-i686": "npm:2.58.5" + "@sentry/cli-win32-x64": "npm:2.58.5" https-proxy-agent: "npm:^5.0.0" node-fetch: "npm:^2.6.7" progress: "npm:^2.0.3" @@ -7350,7 +8018,7 @@ __metadata: optional: true bin: sentry-cli: bin/sentry-cli - checksum: 10/2abdf1f395755eae82b2db984bfd8bf587e91820a35f4688cbe600b0837a156bd599618bbebe92959d2f5c88ecf6f90ab896deb6d362045364d80ead2ff88c8d + checksum: 10/347fb8236b1db52ccf111b397df379af798373b4a022a03b5136f9e5db5d873e24616094a92f5216b1a218fac54c8e8f47e91c9fa089e6bf31c6989691216b2a languageName: node linkType: hard @@ -7361,6 +8029,13 @@ __metadata: languageName: node linkType: hard +"@sentry/core@npm:10.47.0": + version: 10.47.0 + resolution: "@sentry/core@npm:10.47.0" + checksum: 10/c5c5873f58c81cd8df860d0f180002cf8a487b653e1375b1f65b7a012a52b5c383a5018c3fcc8914a758a624d89a8e540be7bf37bd7c86c5a2b4fb6d0e926cdf + languageName: node + linkType: hard + "@sentry/nextjs@npm:^10.30.0": version: 10.30.0 resolution: "@sentry/nextjs@npm:10.30.0" @@ -7463,7 +8138,7 @@ __metadata: languageName: node linkType: hard -"@sentry/react@npm:10.30.0, @sentry/react@npm:^10.30.0": +"@sentry/react@npm:10.30.0": version: 10.30.0 resolution: "@sentry/react@npm:10.30.0" dependencies: @@ -7476,6 +8151,18 @@ __metadata: languageName: node linkType: hard +"@sentry/react@npm:^10.30.0": + version: 10.47.0 + resolution: "@sentry/react@npm:10.47.0" + dependencies: + "@sentry/browser": "npm:10.47.0" + "@sentry/core": "npm:10.47.0" + peerDependencies: + react: ^16.14.0 || 17.x || 18.x || 19.x + checksum: 10/e6ae6c85fa22a364596af25d52e1b861fbb84e01bf57b3f46fe1ffc0b35b528008e5637e5dd403c5943bc79431a6af185dcca5ee5d5f3365951d1a1415a1df92 + languageName: node + linkType: hard + "@sentry/vercel-edge@npm:10.30.0": version: 10.30.0 resolution: "@sentry/vercel-edge@npm:10.30.0" @@ -7547,6 +8234,13 @@ __metadata: languageName: node linkType: hard +"@sindresorhus/merge-streams@npm:^2.1.0": + version: 2.3.0 + resolution: "@sindresorhus/merge-streams@npm:2.3.0" + checksum: 10/798bcb53cd1ace9df84fcdd1ba86afdc9e0cd84f5758d26ae9b1eefd8e8887e5fc30051132b9e74daf01bb41fa5a2faf1369361f83d76a3b3d7ee938058fd71c + languageName: node + linkType: hard + "@sinonjs/commons@npm:^2.0.0": version: 2.0.0 resolution: "@sinonjs/commons@npm:2.0.0" @@ -7660,16 +8354,16 @@ __metadata: "@babel/core": "npm:^7.28.4" "@babel/preset-env": "npm:^7.28.3" "@babel/runtime": "npm:^7.28.4" - "@config-plugins/react-native-callkeep": "npm:^12.0.0" "@config-plugins/react-native-webrtc": "npm:^13.0.0" "@notifee/react-native": "npm:9.1.8" "@react-native-async-storage/async-storage": "npm:2.2.0" "@react-native-community/netinfo": "npm:11.4.1" - "@react-native-firebase/app": "npm:^23.4.0" - "@react-native-firebase/messaging": "npm:^23.4.0" + "@react-native-firebase/app": "npm:~23.7.0" + "@react-native-firebase/messaging": "npm:~23.7.0" "@rnx-kit/metro-config": "npm:^2.1.2" "@rnx-kit/metro-resolver-symlinks": "npm:^0.2.6" "@stream-io/noise-cancellation-react-native": "workspace:^" + "@stream-io/react-native-callingx": "workspace:^" "@stream-io/react-native-webrtc": "npm:137.1.3" "@stream-io/video-filters-react-native": "workspace:^" "@stream-io/video-react-native-sdk": "workspace:^" @@ -7688,13 +8382,11 @@ __metadata: react: "npm:19.1.0" react-dom: "npm:19.1.0" react-native: "npm:^0.81.5" - react-native-callkeep: "npm:^4.3.16" react-native-gesture-handler: "npm:^2.28.0" react-native-reanimated: "npm:~4.1.2" react-native-safe-area-context: "npm:~5.6.1" react-native-screens: "npm:~4.16.0" react-native-svg: "npm:^15.14.0" - react-native-voip-push-notification: "npm:^3.3.3" react-native-worklets: "npm:^0.5.0" typescript: "npm:~5.9.3" languageName: unknown @@ -7782,7 +8474,29 @@ __metadata: rimraf: "npm:^6.0.1" typescript: "npm:^5.9.3" peerDependencies: - "@stream-io/react-native-webrtc": ">=125.3.0" + "@stream-io/react-native-webrtc": ">=137.1.2" + react-native: "*" + languageName: unknown + linkType: soft + +"@stream-io/react-native-callingx@workspace:^, @stream-io/react-native-callingx@workspace:packages/react-native-callingx": + version: 0.0.0-use.local + resolution: "@stream-io/react-native-callingx@workspace:packages/react-native-callingx" + dependencies: + "@react-native-community/cli": "npm:20.0.1" + "@react-native/babel-preset": "npm:^0.81.5" + "@stream-io/react-native-webrtc": "npm:137.1.3" + "@types/react": "npm:^19.1.0" + del-cli: "npm:^6.0.0" + react: "npm:19.1.0" + react-native: "npm:^0.81.5" + react-native-builder-bob: "npm:^0.40.15" + typescript: "npm:^5.9.2" + peerDependencies: + "@react-native-firebase/app": ">=23.0.0" + "@react-native-firebase/messaging": ">=23.0.0" + "@stream-io/react-native-webrtc": ">=137.1.2" + react: "*" react-native: "*" languageName: unknown linkType: soft @@ -7893,7 +8607,7 @@ __metadata: rimraf: "npm:^6.0.1" typescript: "npm:^5.9.3" peerDependencies: - "@stream-io/react-native-webrtc": ">=125.2.1" + "@stream-io/react-native-webrtc": ">=137.1.2" react-native: "*" languageName: unknown linkType: soft @@ -8016,8 +8730,8 @@ __metadata: "@react-native-community/cli-platform-ios": "npm:20.0.0" "@react-native-community/netinfo": "npm:^11.4.1" "@react-native-community/push-notification-ios": "npm:^1.11.0" - "@react-native-firebase/app": "npm:^23.4.0" - "@react-native-firebase/messaging": "npm:^23.4.0" + "@react-native-firebase/app": "npm:~23.7.0" + "@react-native-firebase/messaging": "npm:~23.7.0" "@react-native/babel-preset": "npm:^0.83.2" "@react-native/metro-config": "npm:^0.83.2" "@react-native/typescript-config": "npm:^0.83.2" @@ -8027,6 +8741,7 @@ __metadata: "@rnx-kit/metro-config": "npm:^2.1.2" "@rnx-kit/metro-resolver-symlinks": "npm:^0.2.6" "@stream-io/noise-cancellation-react-native": "workspace:^" + "@stream-io/react-native-callingx": "workspace:^" "@stream-io/react-native-webrtc": "npm:137.1.3" "@stream-io/video-filters-react-native": "workspace:^" "@stream-io/video-react-native-sdk": "workspace:^" @@ -8036,7 +8751,6 @@ __metadata: react: "npm:19.2.0" react-native: "npm:^0.83.2" react-native-blob-util: "npm:^0.22.2" - react-native-callkeep: "npm:^4.3.16" react-native-device-info: "npm:^14.1.1" react-native-dotenv: "npm:^3.4.11" react-native-gesture-handler: "npm:^2.28.0" @@ -8051,7 +8765,6 @@ __metadata: react-native-toast-message: "npm:^2.3.3" react-native-video: "npm:^6.17.0" react-native-vision-camera: "npm:^4.7.2" - react-native-voip-push-notification: "npm:~3.3.3" react-native-worklets: "npm:^0.7.3" rxjs: "npm:~7.8.2" stream-chat: "npm:^9.33.0" @@ -8067,48 +8780,45 @@ __metadata: "@babel/core": "npm:^7.28.4" "@babel/preset-env": "npm:^7.28.3" "@babel/runtime": "npm:^7.28.4" - "@config-plugins/react-native-callkeep": "npm:^12.0.0" "@config-plugins/react-native-webrtc": "npm:^13.0.0" "@expo/vector-icons": "npm:^15.0.2" - "@notifee/react-native": "npm:9.1.8" "@react-native-async-storage/async-storage": "npm:2.2.0" - "@react-native-community/netinfo": "npm:11.4.1" - "@react-native-firebase/app": "npm:^23.4.0" - "@react-native-firebase/messaging": "npm:^23.4.0" + "@react-native-community/netinfo": "npm:11.5.2" + "@react-native-firebase/app": "npm:~23.7.0" + "@react-native-firebase/messaging": "npm:~23.7.0" "@react-navigation/bottom-tabs": "npm:^7.4.8" "@react-navigation/native": "npm:^7.1.18" "@rnx-kit/metro-config": "npm:^2.1.2" "@rnx-kit/metro-resolver-symlinks": "npm:^0.2.6" + "@stream-io/react-native-callingx": "workspace:^" "@stream-io/react-native-webrtc": "npm:137.1.3" "@stream-io/video-react-native-sdk": "workspace:^" - "@types/react": "npm:~19.1.17" - expo: "npm:^54.0.12" - expo-blur: "npm:~15.0.7" - expo-build-properties: "npm:~1.0.9" - expo-constants: "npm:~18.0.9" - expo-dev-client: "npm:~6.0.13" - expo-font: "npm:~14.0.8" - expo-haptics: "npm:~15.0.7" - expo-linking: "npm:~8.0.8" - expo-router: "npm:~6.0.10" - expo-splash-screen: "npm:~31.0.10" - expo-status-bar: "npm:~3.0.8" - expo-symbols: "npm:~1.0.7" - expo-system-ui: "npm:~6.0.7" - expo-web-browser: "npm:~15.0.8" - react: "npm:19.1.0" - react-dom: "npm:19.1.0" - react-native: "npm:^0.81.5" - react-native-callkeep: "npm:^4.3.16" - react-native-gesture-handler: "npm:^2.28.0" - react-native-reanimated: "npm:~4.1.2" - react-native-safe-area-context: "npm:~5.6.1" - react-native-screens: "npm:~4.16.0" - react-native-svg: "npm:^15.14.0" - react-native-voip-push-notification: "npm:^3.3.3" + "@types/react": "npm:~19.2.10" + expo: "npm:^55.0.0" + expo-blur: "npm:~55.0.10" + expo-build-properties: "npm:~55.0.10" + expo-constants: "npm:~55.0.9" + expo-dev-client: "npm:~55.0.19" + expo-font: "npm:~55.0.4" + expo-haptics: "npm:~55.0.9" + expo-linking: "npm:~55.0.9" + expo-router: "npm:~55.0.8" + expo-splash-screen: "npm:~55.0.13" + expo-status-bar: "npm:~55.0.4" + expo-symbols: "npm:~55.0.5" + expo-system-ui: "npm:~55.0.11" + expo-web-browser: "npm:~55.0.10" + react: "npm:19.2.0" + react-dom: "npm:19.2.0" + react-native: "npm:0.83.4" + react-native-gesture-handler: "npm:~2.30.0" + react-native-reanimated: "npm:4.2.1" + react-native-safe-area-context: "npm:~5.6.2" + react-native-screens: "npm:~4.23.0" + react-native-svg: "npm:15.15.3" react-native-web: "npm:^0.21.1" react-native-webview: "npm:13.16.0" - react-native-worklets: "npm:^0.5.0" + react-native-worklets: "npm:0.7.2" typescript: "npm:~5.9.3" languageName: unknown linkType: soft @@ -8128,6 +8838,7 @@ __metadata: "@react-native-firebase/messaging": "npm:^23.4.0" "@react-native/babel-preset": "npm:^0.81.5" "@stream-io/noise-cancellation-react-native": "workspace:^" + "@stream-io/react-native-callingx": "workspace:^" "@stream-io/react-native-webrtc": "npm:137.1.3" "@stream-io/video-client": "workspace:*" "@stream-io/video-filters-react-native": "workspace:^" @@ -8147,12 +8858,10 @@ __metadata: react: "npm:19.1.0" react-native: "npm:^0.81.5" react-native-builder-bob: "npm:~0.23" - react-native-callkeep: "npm:^4.3.16" react-native-gesture-handler: "npm:^2.28.0" react-native-reanimated: "npm:~4.1.2" react-native-svg: "npm:^15.14.0" react-native-url-polyfill: "npm:^3.0.0" - react-native-voip-push-notification: "npm:3.3.3" react-native-worklets: "npm:^0.5.0" react-test-renderer: "npm:19.1.0" rimraf: "npm:^6.0.1" @@ -8166,6 +8875,7 @@ __metadata: "@react-native-firebase/app": ">=17.5.0" "@react-native-firebase/messaging": ">=17.5.0" "@stream-io/noise-cancellation-react-native": ">=0.1.0" + "@stream-io/react-native-callingx": ">=0.1.0" "@stream-io/react-native-webrtc": ">=137.1.3" "@stream-io/video-filters-react-native": ">=0.1.0" expo: ">=47.0.0" @@ -8173,11 +8883,9 @@ __metadata: expo-notifications: "*" react: ">=17.0.0" react-native: ">=0.73.0" - react-native-callkeep: ">=4.3.11" react-native-gesture-handler: ">=2.8.0" react-native-reanimated: ">=2.7.0" react-native-svg: ">=13.6.0" - react-native-voip-push-notification: ">=3.3.1" peerDependenciesMeta: "@notifee/react-native": optional: true @@ -8189,6 +8897,8 @@ __metadata: optional: true "@stream-io/noise-cancellation-react-native": optional: true + "@stream-io/react-native-callingx": + optional: true "@stream-io/video-filters-react-native": optional: true expo: @@ -8197,14 +8907,10 @@ __metadata: optional: true expo-notifications: optional: true - react-native-callkeep: - optional: true react-native-gesture-handler: optional: true react-native-reanimated: optional: true - react-native-voip-push-notification: - optional: true languageName: unknown linkType: soft @@ -8655,11 +9361,11 @@ __metadata: linkType: hard "@types/node@npm:*, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0": - version: 22.9.1 - resolution: "@types/node@npm:22.9.1" + version: 25.5.0 + resolution: "@types/node@npm:25.5.0" dependencies: - undici-types: "npm:~6.19.8" - checksum: 10/43fadcb3a914a1daff8e559839f235eec65fe80bfef5016b361dbc7952c9bc9d79456c78d89beab275a9e9e5accff37e838c019ab519f821f12c953cd6c24b50 + undici-types: "npm:~7.18.0" + checksum: 10/b1e8116bd8c9ff62e458b76d28a59cf7631537bb17e8961464bf754dd5b07b46f1620f568b2f89970505af9eef478dd74c614651b454c1ea95949ec472c64fcb languageName: node linkType: hard @@ -8749,16 +9455,7 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:*, @types/react@npm:>=16.0.0, @types/react@npm:~19.1.17": - version: 19.1.17 - resolution: "@types/react@npm:19.1.17" - dependencies: - csstype: "npm:^3.0.2" - checksum: 10/b21d0ce3296e758e0bbbaa177bea0ed2d7c6a08810d3889d2e7777b2d7ff76d9454690b964883ce2c9d3da73412380dd6cff05794cbd54890e0ef1f3c4b4332d - languageName: node - linkType: hard - -"@types/react@npm:^19.2.0": +"@types/react@npm:*, @types/react@npm:>=16.0.0, @types/react@npm:^19.1.0, @types/react@npm:^19.2.0, @types/react@npm:~19.2.10": version: 19.2.14 resolution: "@types/react@npm:19.2.14" dependencies: @@ -8767,6 +9464,15 @@ __metadata: languageName: node linkType: hard +"@types/react@npm:~19.1.17": + version: 19.1.17 + resolution: "@types/react@npm:19.1.17" + dependencies: + csstype: "npm:^3.0.2" + checksum: 10/b21d0ce3296e758e0bbbaa177bea0ed2d7c6a08810d3889d2e7777b2d7ff76d9454690b964883ce2c9d3da73412380dd6cff05794cbd54890e0ef1f3c4b4332d + languageName: node + linkType: hard + "@types/sdp-transform@npm:^2.15.0": version: 2.15.0 resolution: "@types/sdp-transform@npm:2.15.0" @@ -8850,7 +9556,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:8.46.0, @typescript-eslint/eslint-plugin@npm:^8.29.1": +"@typescript-eslint/eslint-plugin@npm:8.46.0": version: 8.46.0 resolution: "@typescript-eslint/eslint-plugin@npm:8.46.0" dependencies: @@ -8871,7 +9577,27 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/parser@npm:8.46.0, @typescript-eslint/parser@npm:^8.29.1": +"@typescript-eslint/eslint-plugin@npm:^8.29.1": + version: 8.58.0 + resolution: "@typescript-eslint/eslint-plugin@npm:8.58.0" + dependencies: + "@eslint-community/regexpp": "npm:^4.12.2" + "@typescript-eslint/scope-manager": "npm:8.58.0" + "@typescript-eslint/type-utils": "npm:8.58.0" + "@typescript-eslint/utils": "npm:8.58.0" + "@typescript-eslint/visitor-keys": "npm:8.58.0" + ignore: "npm:^7.0.5" + natural-compare: "npm:^1.4.0" + ts-api-utils: "npm:^2.5.0" + peerDependencies: + "@typescript-eslint/parser": ^8.58.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10/0b1f4d4e62279fc5925c0a89d94816db0e236648eb4aec522cf22071877c6ecc63146b6830a1075049f402b842f45d7332ce6ae67883639754dcbb1881d07c34 + languageName: node + linkType: hard + +"@typescript-eslint/parser@npm:8.46.0": version: 8.46.0 resolution: "@typescript-eslint/parser@npm:8.46.0" dependencies: @@ -8887,6 +9613,22 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/parser@npm:^8.29.1": + version: 8.58.0 + resolution: "@typescript-eslint/parser@npm:8.58.0" + dependencies: + "@typescript-eslint/scope-manager": "npm:8.58.0" + "@typescript-eslint/types": "npm:8.58.0" + "@typescript-eslint/typescript-estree": "npm:8.58.0" + "@typescript-eslint/visitor-keys": "npm:8.58.0" + debug: "npm:^4.4.3" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10/0498e593b14841023b7495544637acaca84807de1ac17174fa9331af61ba35791da919e16680c6897002b4a843d5f0fe812c460a28a65fca7e3ae8e5971baf7a + languageName: node + linkType: hard + "@typescript-eslint/project-service@npm:8.46.0": version: 8.46.0 resolution: "@typescript-eslint/project-service@npm:8.46.0" @@ -8900,17 +9642,40 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/project-service@npm:8.58.0": + version: 8.58.0 + resolution: "@typescript-eslint/project-service@npm:8.58.0" + dependencies: + "@typescript-eslint/tsconfig-utils": "npm:^8.58.0" + "@typescript-eslint/types": "npm:^8.58.0" + debug: "npm:^4.4.3" + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + checksum: 10/fab2601f76b2df61b09e3b7ff364d0e17e6d80e65e84e8a8d11f6a0813748bed3912da098659d00f46b1f277d462bd7529157182b72b5e2e0b41ee6176a0edd7 + languageName: node + linkType: hard + "@typescript-eslint/scope-manager@npm:8.46.0": version: 8.46.0 resolution: "@typescript-eslint/scope-manager@npm:8.46.0" dependencies: - "@typescript-eslint/types": "npm:8.46.0" - "@typescript-eslint/visitor-keys": "npm:8.46.0" - checksum: 10/ed85abd08c0edf088b1b11757c658acf593cf84051bddde651304a609d3a6cd9e331149e88653676606a565c3f92c191d4af049f540f6e3bb692a4f38305fd71 + "@typescript-eslint/types": "npm:8.46.0" + "@typescript-eslint/visitor-keys": "npm:8.46.0" + checksum: 10/ed85abd08c0edf088b1b11757c658acf593cf84051bddde651304a609d3a6cd9e331149e88653676606a565c3f92c191d4af049f540f6e3bb692a4f38305fd71 + languageName: node + linkType: hard + +"@typescript-eslint/scope-manager@npm:8.58.0": + version: 8.58.0 + resolution: "@typescript-eslint/scope-manager@npm:8.58.0" + dependencies: + "@typescript-eslint/types": "npm:8.58.0" + "@typescript-eslint/visitor-keys": "npm:8.58.0" + checksum: 10/97293f1215faa785a3c1ee8d630591db9dcd5fb6bdcdd0b2e818c80478d41e59a05003fb33000530780dc466fb8cf662352932080ee7406c4aaac72af4000541 languageName: node linkType: hard -"@typescript-eslint/tsconfig-utils@npm:8.46.0, @typescript-eslint/tsconfig-utils@npm:^8.46.0": +"@typescript-eslint/tsconfig-utils@npm:8.46.0": version: 8.46.0 resolution: "@typescript-eslint/tsconfig-utils@npm:8.46.0" peerDependencies: @@ -8919,6 +9684,15 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/tsconfig-utils@npm:8.58.0, @typescript-eslint/tsconfig-utils@npm:^8.46.0, @typescript-eslint/tsconfig-utils@npm:^8.58.0": + version: 8.58.0 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.58.0" + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + checksum: 10/4f47212c0e26e6b06e97044ec5e483007d5145ef6b205393a0b43cbc0b385c75c14ba5749d01cf7d1ff100332c2cf1d336f060f7d2191bb67fb892bb4446afaa + languageName: node + linkType: hard + "@typescript-eslint/type-utils@npm:8.46.0": version: 8.46.0 resolution: "@typescript-eslint/type-utils@npm:8.46.0" @@ -8935,13 +9709,36 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:8.46.0, @typescript-eslint/types@npm:^8.46.0": +"@typescript-eslint/type-utils@npm:8.58.0": + version: 8.58.0 + resolution: "@typescript-eslint/type-utils@npm:8.58.0" + dependencies: + "@typescript-eslint/types": "npm:8.58.0" + "@typescript-eslint/typescript-estree": "npm:8.58.0" + "@typescript-eslint/utils": "npm:8.58.0" + debug: "npm:^4.4.3" + ts-api-utils: "npm:^2.5.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10/da09868cd0b2cebb8cc4494e73aeed3997a7a4ff899fef496c54731610af1f92f8e3169f9b7f23060105db892f3bc880aacc0bdc7c1734ea5829252230c13aea + languageName: node + linkType: hard + +"@typescript-eslint/types@npm:8.46.0": version: 8.46.0 resolution: "@typescript-eslint/types@npm:8.46.0" checksum: 10/0118b0dd592bf4beaf41e8c6be812980dd0adea44d48c90d8b0272777b58d4cfd6326b8bc363efa3c640be476a6bf3632aee2d97052d5e34071e6576b9c28264 languageName: node linkType: hard +"@typescript-eslint/types@npm:8.58.0, @typescript-eslint/types@npm:^8.46.0, @typescript-eslint/types@npm:^8.58.0": + version: 8.58.0 + resolution: "@typescript-eslint/types@npm:8.58.0" + checksum: 10/c68eac0bc25812fdbb2ed4a121e42bfca9f24f3c6be95f6a9c4e7b9af767f1bcfacd6d496e358166143e0a1801dc7d042ce1b5e69946ac2768d9114ff6b8d375 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:8.46.0": version: 8.46.0 resolution: "@typescript-eslint/typescript-estree@npm:8.46.0" @@ -8962,6 +9759,25 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:8.58.0": + version: 8.58.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.58.0" + dependencies: + "@typescript-eslint/project-service": "npm:8.58.0" + "@typescript-eslint/tsconfig-utils": "npm:8.58.0" + "@typescript-eslint/types": "npm:8.58.0" + "@typescript-eslint/visitor-keys": "npm:8.58.0" + debug: "npm:^4.4.3" + minimatch: "npm:^10.2.2" + semver: "npm:^7.7.3" + tinyglobby: "npm:^0.2.15" + ts-api-utils: "npm:^2.5.0" + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + checksum: 10/4d6c4175e8a4d5c097393d161016836cc322f090c3f69fd751f5bbc25afce64df9ea0c97cee8b36ac060e06dc2cca2a4de7a0c7e04e19727cc4bd98ab3291fed + languageName: node + linkType: hard + "@typescript-eslint/utils@npm:8.46.0": version: 8.46.0 resolution: "@typescript-eslint/utils@npm:8.46.0" @@ -8977,6 +9793,21 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/utils@npm:8.58.0": + version: 8.58.0 + resolution: "@typescript-eslint/utils@npm:8.58.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.9.1" + "@typescript-eslint/scope-manager": "npm:8.58.0" + "@typescript-eslint/types": "npm:8.58.0" + "@typescript-eslint/typescript-estree": "npm:8.58.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10/936433b761a990147612d78bb4afc79244239541b4a4061fbbc2de1810b40ec7f78eb4e9181e5d9c5ab7acbd9bf49fc6195dbb1d823370f717f07ad492ad6c7e + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:8.46.0": version: 8.46.0 resolution: "@typescript-eslint/visitor-keys@npm:8.46.0" @@ -8987,6 +9818,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:8.58.0": + version: 8.58.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.58.0" + dependencies: + "@typescript-eslint/types": "npm:8.58.0" + eslint-visitor-keys: "npm:^5.0.0" + checksum: 10/50b0779e19079dedf3723323a4dfa398c639b3da48f2fcf071c22ca69342e03592f1726d68ea59b9b5a51f14ab112eabc5c93fd2579c84b02a3320042ae20066 + languageName: node + linkType: hard + "@ungap/structured-clone@npm:^1.0.0, @ungap/structured-clone@npm:^1.3.0": version: 1.3.0 resolution: "@ungap/structured-clone@npm:1.3.0" @@ -8995,12 +9836,12 @@ __metadata: linkType: hard "@urql/core@npm:^5.0.0, @urql/core@npm:^5.0.6": - version: 5.1.0 - resolution: "@urql/core@npm:5.1.0" + version: 5.2.0 + resolution: "@urql/core@npm:5.2.0" dependencies: - "@0no-co/graphql.web": "npm:^1.0.5" + "@0no-co/graphql.web": "npm:^1.0.13" wonka: "npm:^6.3.2" - checksum: 10/c3573f03af0d73fa8b0fc24bbc27e1f27815b1c4430f147fa248fec36f905a69d59d186210b7d02551517eb00ba9ca47232b96613d5cc12e0ab56fd0d96c858c + checksum: 10/b49378550b7581e223f96c3abff33952e0409cebdef6f233250275b9548244ae99e793c9f5791b0ce707955f85c27fed5031719ea1f1279a190ffa0f9299231a languageName: node linkType: hard @@ -9263,6 +10104,16 @@ __metadata: languageName: node linkType: hard +"accepts@npm:^2.0.0": + version: 2.0.0 + resolution: "accepts@npm:2.0.0" + dependencies: + mime-types: "npm:^3.0.0" + negotiator: "npm:^1.0.0" + checksum: 10/ea1343992b40b2bfb3a3113fa9c3c2f918ba0f9197ae565c48d3f84d44b174f6b1d5cd9989decd7655963eb03a272abc36968cc439c2907f999bd5ef8653d5a7 + languageName: node + linkType: hard + "acorn-globals@npm:^7.0.0": version: 7.0.1 resolution: "acorn-globals@npm:7.0.1" @@ -9309,12 +10160,12 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.1.0, acorn@npm:^8.11.0, acorn@npm:^8.14.0, acorn@npm:^8.15.0, acorn@npm:^8.5.0, acorn@npm:^8.8.1": - version: 8.15.0 - resolution: "acorn@npm:8.15.0" +"acorn@npm:^8.1.0, acorn@npm:^8.11.0, acorn@npm:^8.15.0, acorn@npm:^8.5.0, acorn@npm:^8.8.1": + version: 8.16.0 + resolution: "acorn@npm:8.16.0" bin: acorn: bin/acorn - checksum: 10/77f2de5051a631cf1729c090e5759148459cdb76b5f5c70f890503d629cf5052357b0ce783c0f976dd8a93c5150f59f6d18df1def3f502396a20f81282482fa4 + checksum: 10/690c673bb4d61b38ef82795fab58526471ad7f7e67c0e40c4ff1e10ecd80ce5312554ef633c9995bfc4e6d170cef165711f9ca9e49040b62c0c66fbf2dd3df2b languageName: node linkType: hard @@ -9400,7 +10251,7 @@ __metadata: languageName: node linkType: hard -"ansi-escapes@npm:^4.2.1, ansi-escapes@npm:^4.3.0, ansi-escapes@npm:^4.3.2": +"ansi-escapes@npm:^4.2.1, ansi-escapes@npm:^4.3.0": version: 4.3.2 resolution: "ansi-escapes@npm:4.3.2" dependencies: @@ -9450,10 +10301,10 @@ __metadata: languageName: node linkType: hard -"ansi-regex@npm:^6.0.1": - version: 6.0.1 - resolution: "ansi-regex@npm:6.0.1" - checksum: 10/1ff8b7667cded1de4fa2c9ae283e979fc87036864317da86a2e546725f96406746411d0d85e87a2d12fa5abd715d90006de7fa4fa0477c92321ad3b4c7d4e169 +"ansi-regex@npm:^6.2.2": + version: 6.2.2 + resolution: "ansi-regex@npm:6.2.2" + checksum: 10/9b17ce2c6daecc75bcd5966b9ad672c23b184dc3ed9bf3c98a0702f0d2f736c15c10d461913568f2cf527a5e64291c7473358885dd493305c84a1cfed66ba94f languageName: node linkType: hard @@ -9483,9 +10334,9 @@ __metadata: linkType: hard "ansi-styles@npm:^6.1.0, ansi-styles@npm:^6.2.1": - version: 6.2.1 - resolution: "ansi-styles@npm:6.2.1" - checksum: 10/70fdf883b704d17a5dfc9cde206e698c16bcd74e7f196ab821511651aee4f9f76c9514bdfa6ca3a27b5e49138b89cb222a28caf3afe4567570139577f991df32 + version: 6.2.3 + resolution: "ansi-styles@npm:6.2.3" + checksum: 10/c49dad7639f3e48859bd51824c93b9eb0db628afc243c51c3dd2410c4a15ede1a83881c6c7341aa2b159c4f90c11befb38f2ba848c07c66c9f9de4bcd7cb9f30 languageName: node linkType: hard @@ -9513,13 +10364,6 @@ __metadata: languageName: node linkType: hard -"application-config-path@npm:^0.1.0": - version: 0.1.1 - resolution: "application-config-path@npm:0.1.1" - checksum: 10/380f4c49585511813526632c8366318f52941526dbb284a887e5af328caa76424a056795ab18f03f5009197f2dea0ef01a8a9812d85724f26d2f5cf9bf9bf1f9 - languageName: node - linkType: hard - "aproba@npm:^1.0.3 || ^2.0.0": version: 2.0.0 resolution: "aproba@npm:2.0.0" @@ -9569,6 +10413,26 @@ __metadata: languageName: node linkType: hard +"arkregex@npm:0.0.5": + version: 0.0.5 + resolution: "arkregex@npm:0.0.5" + dependencies: + "@ark/util": "npm:0.56.0" + checksum: 10/c5eca109df57639b3245e1e72efe1b43cf881a2234b29736b11f57d29674d9ef78a2dcf54f5381a33690d53ce8989520bc123bb686dcce83f15c44f141c7f8a9 + languageName: node + linkType: hard + +"arktype@npm:^2.1.15": + version: 2.1.29 + resolution: "arktype@npm:2.1.29" + dependencies: + "@ark/schema": "npm:0.56.0" + "@ark/util": "npm:0.56.0" + arkregex: "npm:0.0.5" + checksum: 10/091df54e5df0282a26f5de74cc001569483fc61b3297277a51cb8244f277334a549cf8ae3342ca3bbde95bd10172aaa2f86e6c5738e2853b2b66c088a7c9f398 + languageName: node + linkType: hard + "array-buffer-byte-length@npm:^1.0.1, array-buffer-byte-length@npm:^1.0.2": version: 1.0.2 resolution: "array-buffer-byte-length@npm:1.0.2" @@ -9815,7 +10679,7 @@ __metadata: languageName: node linkType: hard -"axios@npm:1.12.2, axios@npm:^1.12.0, axios@npm:^1.12.2": +"axios@npm:1.12.2": version: 1.12.2 resolution: "axios@npm:1.12.2" dependencies: @@ -9826,6 +10690,17 @@ __metadata: languageName: node linkType: hard +"axios@npm:^1.12.0, axios@npm:^1.12.2": + version: 1.14.0 + resolution: "axios@npm:1.14.0" + dependencies: + follow-redirects: "npm:^1.15.11" + form-data: "npm:^4.0.5" + proxy-from-env: "npm:^2.1.0" + checksum: 10/c3444e9e3da1714916e4ddd7cda05bb41a5d5d80e3e27b099a116439684c63f2280c88503d1acd65841698b63af0b542b4d5780454e28fd0aed2d783ef90943e + languageName: node + linkType: hard + "babel-jest@npm:^29.2.1, babel-jest@npm:^29.7.0": version: 29.7.0 resolution: "babel-jest@npm:29.7.0" @@ -9915,15 +10790,15 @@ __metadata: linkType: hard "babel-plugin-polyfill-corejs2@npm:^0.4.10, babel-plugin-polyfill-corejs2@npm:^0.4.14": - version: 0.4.14 - resolution: "babel-plugin-polyfill-corejs2@npm:0.4.14" + version: 0.4.17 + resolution: "babel-plugin-polyfill-corejs2@npm:0.4.17" dependencies: - "@babel/compat-data": "npm:^7.27.7" - "@babel/helper-define-polyfill-provider": "npm:^0.6.5" + "@babel/compat-data": "npm:^7.28.6" + "@babel/helper-define-polyfill-provider": "npm:^0.6.8" semver: "npm:^6.3.1" peerDependencies: "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 10/8ec00a1b821ccbfcc432630da66e98bc417f5301f4ce665269d50d245a18ad3ce8a8af2a007f28e3defcd555bb8ce65f16b0d4b6d131bd788e2b97d8b8953332 + checksum: 10/35796b7f960d2e90ae78e9eb60491550976b839bbb4ce4c060df822cce191e4b5d93f13f0e64c2ba3ffc6ab3d32d3ced3f84ec567cc141088a11fa5a1628265d languageName: node linkType: hard @@ -9952,13 +10827,13 @@ __metadata: linkType: hard "babel-plugin-polyfill-regenerator@npm:^0.6.1, babel-plugin-polyfill-regenerator@npm:^0.6.5": - version: 0.6.5 - resolution: "babel-plugin-polyfill-regenerator@npm:0.6.5" + version: 0.6.8 + resolution: "babel-plugin-polyfill-regenerator@npm:0.6.8" dependencies: - "@babel/helper-define-polyfill-provider": "npm:^0.6.5" + "@babel/helper-define-polyfill-provider": "npm:^0.6.8" peerDependencies: "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 10/ed1932fa9a31e0752fd10ebf48ab9513a654987cab1182890839523cb898559d24ae0578fdc475d9f995390420e64eeaa4b0427045b56949dace3c725bc66dbb + checksum: 10/974464353d6f974e97673385aff616a913c0b76039eab8c5317a2d07c661e080f3dcc213e86f3eae40010172a27ab793cda7a290a8a899716f9a22df9b1d92d2 languageName: node linkType: hard @@ -10005,6 +10880,24 @@ __metadata: languageName: node linkType: hard +"babel-plugin-syntax-hermes-parser@npm:^0.28.0": + version: 0.28.1 + resolution: "babel-plugin-syntax-hermes-parser@npm:0.28.1" + dependencies: + hermes-parser: "npm:0.28.1" + checksum: 10/2cbc921e663463480ead9ccc8bb229a5196032367ba2b5ccb18a44faa3afa84b4dc493297749983b9a837a3d76b0b123664aecc06f9122618c3246f03e076a9d + languageName: node + linkType: hard + +"babel-plugin-syntax-hermes-parser@npm:^0.32.0": + version: 0.32.1 + resolution: "babel-plugin-syntax-hermes-parser@npm:0.32.1" + dependencies: + hermes-parser: "npm:0.32.1" + checksum: 10/b8b6c4d2ffa2cf0c6835c58693899023da86dd42a785355c0d005abda5a857cb701fd7b879ccbebafdc146ebfa635aeb4650dd69dc245f21f1378060ebfde9ed + languageName: node + linkType: hard + "babel-plugin-transform-flow-enums@npm:^0.0.2": version: 0.0.2 resolution: "babel-plugin-transform-flow-enums@npm:0.0.2" @@ -10075,6 +10968,49 @@ __metadata: languageName: node linkType: hard +"babel-preset-expo@npm:~55.0.13": + version: 55.0.13 + resolution: "babel-preset-expo@npm:55.0.13" + dependencies: + "@babel/generator": "npm:^7.20.5" + "@babel/helper-module-imports": "npm:^7.25.9" + "@babel/plugin-proposal-decorators": "npm:^7.12.9" + "@babel/plugin-proposal-export-default-from": "npm:^7.24.7" + "@babel/plugin-syntax-export-default-from": "npm:^7.24.7" + "@babel/plugin-transform-class-static-block": "npm:^7.27.1" + "@babel/plugin-transform-export-namespace-from": "npm:^7.25.9" + "@babel/plugin-transform-flow-strip-types": "npm:^7.25.2" + "@babel/plugin-transform-modules-commonjs": "npm:^7.24.8" + "@babel/plugin-transform-object-rest-spread": "npm:^7.24.7" + "@babel/plugin-transform-parameters": "npm:^7.24.7" + "@babel/plugin-transform-private-methods": "npm:^7.24.7" + "@babel/plugin-transform-private-property-in-object": "npm:^7.24.7" + "@babel/plugin-transform-runtime": "npm:^7.24.7" + "@babel/preset-react": "npm:^7.22.15" + "@babel/preset-typescript": "npm:^7.23.0" + "@react-native/babel-preset": "npm:0.83.4" + babel-plugin-react-compiler: "npm:^1.0.0" + babel-plugin-react-native-web: "npm:~0.21.0" + babel-plugin-syntax-hermes-parser: "npm:^0.32.0" + babel-plugin-transform-flow-enums: "npm:^0.0.2" + debug: "npm:^4.3.4" + resolve-from: "npm:^5.0.0" + peerDependencies: + "@babel/runtime": ^7.20.0 + expo: "*" + expo-widgets: ^55.0.8 + react-refresh: ">=0.14.0 <1.0.0" + peerDependenciesMeta: + "@babel/runtime": + optional: true + expo: + optional: true + expo-widgets: + optional: true + checksum: 10/056c2e0ea8a4f2939448194f01467ba57010d618cf31801f5c60efd2d665b2e099a03cc3b66653f932e8f1be1493189ce326bd7d7ca35520f09e01b9cf2127b7 + languageName: node + linkType: hard + "babel-preset-jest@npm:^29.6.3": version: 29.6.3 resolution: "babel-preset-jest@npm:29.6.3" @@ -10108,6 +11044,13 @@ __metadata: languageName: node linkType: hard +"balanced-match@npm:^4.0.2": + version: 4.0.4 + resolution: "balanced-match@npm:4.0.4" + checksum: 10/fb07bb66a0959c2843fc055838047e2a95ccebb837c519614afb067ebfdf2fa967ca8d712c35ced07f2cd26fc6f07964230b094891315ad74f11eba3d53178a0 + languageName: node + linkType: hard + "base-64@npm:0.1.0": version: 0.1.0 resolution: "base-64@npm:0.1.0" @@ -10129,12 +11072,12 @@ __metadata: languageName: node linkType: hard -"baseline-browser-mapping@npm:^2.8.9": - version: 2.8.14 - resolution: "baseline-browser-mapping@npm:2.8.14" +"baseline-browser-mapping@npm:^2.10.12": + version: 2.10.13 + resolution: "baseline-browser-mapping@npm:2.10.13" bin: - baseline-browser-mapping: dist/cli.js - checksum: 10/ab1b7f056185594cd6d970afd176d0e37a7f98246be34a91fd607acb9209c3cbfdec932e4346e5c14405f3f395fa62dc23b8b23d748e20f29e25ec5b18c07c36 + baseline-browser-mapping: dist/cli.cjs + checksum: 10/c33f047cb7398b557a86a38b4fe019bbc701a3c368b510e1d3553e9aee33b67dfaa1fc603f00e24c364fb0060c1a2e304cb3360c69b614b41da1ddee08465333 languageName: node linkType: hard @@ -10244,11 +11187,20 @@ __metadata: linkType: hard "brace-expansion@npm:^2.0.1": - version: 2.0.1 - resolution: "brace-expansion@npm:2.0.1" + version: 2.0.3 + resolution: "brace-expansion@npm:2.0.3" dependencies: balanced-match: "npm:^1.0.0" - checksum: 10/a61e7cd2e8a8505e9f0036b3b6108ba5e926b4b55089eeb5550cd04a471fe216c96d4fe7e4c7f995c728c554ae20ddfc4244cad10aef255e72b62930afd233d1 + checksum: 10/e9dd66caaf0784126e1654f1bc19adb28f3ef86f39f2226f833f7700ec727c141f6cd85eaa47bacf3426beda01c9fbc3a2f28174cf59330dc9b58ffaf9e09d96 + languageName: node + linkType: hard + +"brace-expansion@npm:^5.0.5": + version: 5.0.5 + resolution: "brace-expansion@npm:5.0.5" + dependencies: + balanced-match: "npm:^4.0.2" + checksum: 10/f259b2ddf04489da9512ad637ba6b4ef2d77abd4445d20f7f1714585f153435200a53fa6a2e4a5ee974df14ddad4cd16421f6f803e96e8b452bd48598878d0ee languageName: node linkType: hard @@ -10261,18 +11213,18 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.20.4, browserslist@npm:^4.24.0, browserslist@npm:^4.24.4, browserslist@npm:^4.25.0, browserslist@npm:^4.25.3": - version: 4.26.3 - resolution: "browserslist@npm:4.26.3" +"browserslist@npm:^4.20.4, browserslist@npm:^4.24.0, browserslist@npm:^4.24.4, browserslist@npm:^4.25.0, browserslist@npm:^4.28.1": + version: 4.28.2 + resolution: "browserslist@npm:4.28.2" dependencies: - baseline-browser-mapping: "npm:^2.8.9" - caniuse-lite: "npm:^1.0.30001746" - electron-to-chromium: "npm:^1.5.227" - node-releases: "npm:^2.0.21" - update-browserslist-db: "npm:^1.1.3" + baseline-browser-mapping: "npm:^2.10.12" + caniuse-lite: "npm:^1.0.30001782" + electron-to-chromium: "npm:^1.5.328" + node-releases: "npm:^2.0.36" + update-browserslist-db: "npm:^1.2.3" bin: browserslist: cli.js - checksum: 10/49add06fd753a2514d84c75a7de8d9fb3d70be675e53b72981d87f0c0ff40d8a8cd0bd92f77400381704be0bf1c9c5c65aef95d03843d69475ff55188aa12124 + checksum: 10/cff88386e5b5ba5614c9063bd32ef94865bba22b6a381844c7d09ea1eea62a2247e7106e516abdbfda6b75b9986044c991dfe45f92f10add5ad63dccc07589ec languageName: node linkType: hard @@ -10443,10 +11395,10 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.30001579, caniuse-lite@npm:^1.0.30001702, caniuse-lite@npm:^1.0.30001746": - version: 1.0.30001749 - resolution: "caniuse-lite@npm:1.0.30001749" - checksum: 10/017a9e02f33a870ad2da47245e06ba6eb3e2b339218ebf45b5caa7d7202db562f06a5f5ba62fef5b8864ad89b2370087be49ced336980a8847f45c097d8d734f +"caniuse-lite@npm:^1.0.30001579, caniuse-lite@npm:^1.0.30001702, caniuse-lite@npm:^1.0.30001782": + version: 1.0.30001784 + resolution: "caniuse-lite@npm:1.0.30001784" + checksum: 10/1450e306d0517cea65931e417dada2266c797ebab86de484af7510dbae9a7dd60abab1a70bcd725f7b87e5d1816e1526a495fb089a7447dea046adf2a24fff97 languageName: node linkType: hard @@ -10657,13 +11609,20 @@ __metadata: languageName: node linkType: hard -"cjs-module-lexer@npm:^1.0.0, cjs-module-lexer@npm:^1.2.2": +"cjs-module-lexer@npm:^1.0.0": version: 1.3.1 resolution: "cjs-module-lexer@npm:1.3.1" checksum: 10/6629188d5ce74b57e5dce2222db851b5496a8d65b533a05957fb24089a3cec8d769378013c375a954c5a0f7522cde6a36d5a65bfd88f5575cb2de3176046fa8e languageName: node linkType: hard +"cjs-module-lexer@npm:^2.2.0": + version: 2.2.0 + resolution: "cjs-module-lexer@npm:2.2.0" + checksum: 10/fc8eb5c1919504366d8260a150d93c4e857740e770467dc59ca0cc34de4b66c93075559a5af65618f359187866b1be40e036f4e1a1bab2f1e06001c216415f74 + languageName: node + linkType: hard + "clean-stack@npm:^2.0.0": version: 2.2.0 resolution: "clean-stack@npm:2.2.0" @@ -10706,9 +11665,9 @@ __metadata: linkType: hard "cli-spinners@npm:^2.0.0, cli-spinners@npm:^2.5.0": - version: 2.9.0 - resolution: "cli-spinners@npm:2.9.0" - checksum: 10/457497ccef70eec3f1d0825e4a3396ba43f6833a4900c2047c0efe2beecb1c0df476949ea378bcb6595754f7508e28ae943eeb30bbda807f59f547b270ec334c + version: 2.9.2 + resolution: "cli-spinners@npm:2.9.2" + checksum: 10/a0a863f442df35ed7294424f5491fa1756bd8d2e4ff0c8736531d886cec0ece4d85e8663b77a5afaf1d296e3cbbebff92e2e99f52bbea89b667cbe789b994794 languageName: node linkType: hard @@ -10899,7 +11858,7 @@ __metadata: languageName: node linkType: hard -"command-exists@npm:^1.2.4, command-exists@npm:^1.2.8": +"command-exists@npm:^1.2.8": version: 1.2.9 resolution: "command-exists@npm:1.2.9" checksum: 10/46fb3c4d626ca5a9d274f8fe241230817496abc34d12911505370b7411999e183c11adff7078dd8a03ec4cf1391290facda40c6a4faac8203ae38c985eaedd63 @@ -11271,11 +12230,11 @@ __metadata: linkType: hard "core-js-compat@npm:^3.38.0, core-js-compat@npm:^3.43.0": - version: 3.45.1 - resolution: "core-js-compat@npm:3.45.1" + version: 3.49.0 + resolution: "core-js-compat@npm:3.49.0" dependencies: - browserslist: "npm:^4.25.3" - checksum: 10/a6eb757ccf5091ee4cf7756c4f2ddefb506b049d89526e8150221e6d9150dc2685c34cbed42f4b15a27a92dd300fd56f75c9502cd57cfe928c1bd7a8ed961a42 + browserslist: "npm:^4.28.1" + checksum: 10/eb35ad9b31a613092d32e5eb0c9fecb695e680bb29509fe04ae297ef790cea47d06864ef8939c8f5f189cce0bd2807fef8b2d6450f7eeb917ffaaf38a775dece languageName: node linkType: hard @@ -11365,13 +12324,6 @@ __metadata: languageName: node linkType: hard -"crypto-random-string@npm:^2.0.0": - version: 2.0.0 - resolution: "crypto-random-string@npm:2.0.0" - checksum: 10/0283879f55e7c16fdceacc181f87a0a65c53bc16ffe1d58b9d19a6277adcd71900d02bb2c4843dd55e78c51e30e89b0fec618a7f170ebcc95b33182c28f05fd6 - languageName: node - linkType: hard - "css-in-js-utils@npm:^3.1.0": version: 3.1.0 resolution: "css-in-js-utils@npm:3.1.0" @@ -11450,14 +12402,7 @@ __metadata: languageName: node linkType: hard -"csstype@npm:^3.0.2, csstype@npm:^3.1.3": - version: 3.1.3 - resolution: "csstype@npm:3.1.3" - checksum: 10/f593cce41ff5ade23f44e77521e3a1bcc2c64107041e1bf6c3c32adc5187d0d60983292fda326154d20b01079e24931aa5b08e4467cc488b60bb1e7f6d478ade - languageName: node - linkType: hard - -"csstype@npm:^3.2.2": +"csstype@npm:^3.0.2, csstype@npm:^3.1.3, csstype@npm:^3.2.2": version: 3.2.3 resolution: "csstype@npm:3.2.3" checksum: 10/ad41baf7e2ffac65ab544d79107bf7cd1a4bb9bab9ac3302f59ab4ba655d5e30942a8ae46e10ba160c6f4ecea464cc95b975ca2fefbdeeacd6ac63f12f99fe1f @@ -11465,12 +12410,12 @@ __metadata: linkType: hard "d@npm:1, d@npm:^1.0.1": - version: 1.0.1 - resolution: "d@npm:1.0.1" + version: 1.0.2 + resolution: "d@npm:1.0.2" dependencies: - es5-ext: "npm:^0.10.50" - type: "npm:^1.0.1" - checksum: 10/1296e3f92e646895681c1cb564abd0eb23c29db7d62c5120a279e84e98915499a477808e9580760f09e3744c0ed7ac8f7cff98d096ba9770754f6ef0f1c97983 + es5-ext: "npm:^0.10.64" + type: "npm:^2.7.2" + checksum: 10/a3f45ef964622f683f6a1cb9b8dcbd75ce490cd2f4ac9794099db3d8f0e2814d412d84cd3fe522e58feb1f273117bb480f29c5381f6225f0abca82517caaa77a languageName: node linkType: hard @@ -11532,13 +12477,20 @@ __metadata: languageName: node linkType: hard -"dayjs@npm:1.11.13, dayjs@npm:^1.10.4, dayjs@npm:^1.11.6, dayjs@npm:^1.8.15": +"dayjs@npm:1.11.13": version: 1.11.13 resolution: "dayjs@npm:1.11.13" checksum: 10/7374d63ab179b8d909a95e74790def25c8986e329ae989840bacb8b1888be116d20e1c4eee75a69ea0dfbae13172efc50ef85619d304ee7ca3c01d5878b704f5 languageName: node linkType: hard +"dayjs@npm:^1.10.4, dayjs@npm:^1.11.6, dayjs@npm:^1.8.15": + version: 1.11.20 + resolution: "dayjs@npm:1.11.20" + checksum: 10/5347533f21a55b8bb1b1ef559be9b805514c3a8fb7e68b75fb7e73808131c59e70909c073aa44ce8a0d159195cd110cdd4081cf87ab96cb06fee3edacae791c6 + languageName: node + linkType: hard + "debug@npm:2.6.9, debug@npm:^2.2.0, debug@npm:^2.6.9": version: 2.6.9 resolution: "debug@npm:2.6.9" @@ -11548,7 +12500,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.4.0, debug@npm:^4.4.1": +"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.4.0, debug@npm:^4.4.1, debug@npm:^4.4.3": version: 4.4.3 resolution: "debug@npm:4.4.3" dependencies: @@ -11707,6 +12659,19 @@ __metadata: languageName: node linkType: hard +"del-cli@npm:^6.0.0": + version: 6.0.0 + resolution: "del-cli@npm:6.0.0" + dependencies: + del: "npm:^8.0.0" + meow: "npm:^13.2.0" + bin: + del: cli.js + del-cli: cli.js + checksum: 10/5441e55c9181f364e84a1d211e53a03476a806e795f6b7d576673f164a9f76e0dc846413a47730f6224913974ba54dabb1884f854a9cf4d418084872bdaaa229 + languageName: node + linkType: hard + "del@npm:^6.1.1": version: 6.1.1 resolution: "del@npm:6.1.1" @@ -11723,6 +12688,21 @@ __metadata: languageName: node linkType: hard +"del@npm:^8.0.0": + version: 8.0.1 + resolution: "del@npm:8.0.1" + dependencies: + globby: "npm:^14.0.2" + is-glob: "npm:^4.0.3" + is-path-cwd: "npm:^3.0.0" + is-path-inside: "npm:^4.0.0" + p-map: "npm:^7.0.2" + presentable-error: "npm:^0.0.1" + slash: "npm:^5.1.0" + checksum: 10/53ed4a379a68c90e7d6d3bcce09c49229e77de9a946d0a5fc25f45b16c950cb8665986b7d0d0423416c03bfd43e0f31e528c5a19c558fe47449be9d6fae7f846 + languageName: node + linkType: hard + "delayed-stream@npm:~1.0.0": version: 1.0.0 resolution: "delayed-stream@npm:1.0.0" @@ -11744,7 +12724,7 @@ __metadata: languageName: node linkType: hard -"depd@npm:2.0.0, depd@npm:^2.0.0": +"depd@npm:2.0.0, depd@npm:^2.0.0, depd@npm:~2.0.0": version: 2.0.0 resolution: "depd@npm:2.0.0" checksum: 10/c0c8ff36079ce5ada64f46cc9d6fd47ebcf38241105b6e0c98f412e8ad91f084bcf906ff644cc3a4bd876ca27a62accb8b0fff72ea6ed1a414b89d8506f4a5ca @@ -11841,6 +12821,13 @@ __metadata: languageName: node linkType: hard +"dnssd-advertise@npm:^1.1.3": + version: 1.1.4 + resolution: "dnssd-advertise@npm:1.1.4" + checksum: 10/b8a50bac99bc96d79a42bec68fe7ffcf233d5d6accf2be762dfdb2e11ce2cc1be462ab56e7162b60a34a9b90d7222c5e97d2e5d0f44983ab00956328e999d55f + languageName: node + linkType: hard + "doctrine@npm:^2.1.0": version: 2.1.0 resolution: "doctrine@npm:2.1.0" @@ -12003,10 +12990,10 @@ __metadata: languageName: node linkType: hard -"electron-to-chromium@npm:^1.5.227": - version: 1.5.233 - resolution: "electron-to-chromium@npm:1.5.233" - checksum: 10/df166e27705f99a02fc0c6a94c53c3d771939a9de909acd65cbc974ed30ed8301bb08d848cffb9dc8832a194a3e0c99e02d0cb72371f7c7723b9e77c2d0d3867 +"electron-to-chromium@npm:^1.5.328": + version: 1.5.330 + resolution: "electron-to-chromium@npm:1.5.330" + checksum: 10/d77d951fe88c075ea4b8ea90c4c485cab4c316377c13369e1c068a9d02ff3dc37d261e0e1c3c2b618a33a5deb0487addbbb5cee940cb697c8700a0d745948f8e languageName: node linkType: hard @@ -12126,13 +13113,6 @@ __metadata: languageName: node linkType: hard -"eol@npm:^0.9.1": - version: 0.9.1 - resolution: "eol@npm:0.9.1" - checksum: 10/9d3fd93bb2bb5c69c7fe8dfb97b62213ed95857a2e90f5db3110415993e8a989d87fb011755ce22fdb92ca36fbe4e111b395a6f4ce00b9b51d3f00f19c2acf52 - languageName: node - linkType: hard - "err-code@npm:^2.0.2": version: 2.0.3 resolution: "err-code@npm:2.0.3" @@ -12169,8 +13149,8 @@ __metadata: linkType: hard "es-abstract@npm:^1.17.5, es-abstract@npm:^1.23.2, es-abstract@npm:^1.23.3, es-abstract@npm:^1.23.5, es-abstract@npm:^1.23.6, es-abstract@npm:^1.23.9, es-abstract@npm:^1.24.0": - version: 1.24.0 - resolution: "es-abstract@npm:1.24.0" + version: 1.24.1 + resolution: "es-abstract@npm:1.24.1" dependencies: array-buffer-byte-length: "npm:^1.0.2" arraybuffer.prototype.slice: "npm:^1.0.4" @@ -12226,7 +13206,7 @@ __metadata: typed-array-length: "npm:^1.0.7" unbox-primitive: "npm:^1.1.0" which-typed-array: "npm:^1.1.19" - checksum: 10/64e07a886f7439cf5ccfc100f9716e6173e10af6071a50a5031afbdde474a3dbc9619d5965da54e55f8908746a9134a46be02af8c732d574b7b81ed3124e2daf + checksum: 10/c84cb69ebae36781309a3ed70ff40b4767a921d3b3518060fac4e08f14ede04491b68e9f318aedf186e349d4af4a40f5d0e4111e46513800e8368551fd09de8c languageName: node linkType: hard @@ -12316,14 +13296,15 @@ __metadata: languageName: node linkType: hard -"es5-ext@npm:^0.10.35, es5-ext@npm:^0.10.50": - version: 0.10.62 - resolution: "es5-ext@npm:0.10.62" +"es5-ext@npm:^0.10.35, es5-ext@npm:^0.10.62, es5-ext@npm:^0.10.64, es5-ext@npm:~0.10.14": + version: 0.10.64 + resolution: "es5-ext@npm:0.10.64" dependencies: es6-iterator: "npm:^2.0.3" es6-symbol: "npm:^3.1.3" + esniff: "npm:^2.0.1" next-tick: "npm:^1.1.0" - checksum: 10/3f6a3bcdb7ff82aaf65265799729828023c687a2645da04005b8f1dc6676a0c41fd06571b2517f89dcf143e0268d3d9ef0fdfd536ab74580083204c688d6fb45 + checksum: 10/0c5d8657708b1695ddc4b06f4e0b9fbdda4d2fe46d037b6bedb49a7d1931e542ec9eecf4824d59e1d357e93229deab014bb4b86485db2d41b1d68e54439689ce languageName: node linkType: hard @@ -12783,6 +13764,13 @@ __metadata: languageName: node linkType: hard +"eslint-visitor-keys@npm:^5.0.0": + version: 5.0.1 + resolution: "eslint-visitor-keys@npm:5.0.1" + checksum: 10/f9cc1a57b75e0ef949545cac33d01e8367e302de4c1483266ed4d8646ee5c306376660196bbb38b004e767b7043d1e661cb4336b49eff634a1bbe75c1db709ec + languageName: node + linkType: hard + "eslint@npm:^9.37.0": version: 9.37.0 resolution: "eslint@npm:9.37.0" @@ -12833,6 +13821,18 @@ __metadata: languageName: node linkType: hard +"esniff@npm:^2.0.1": + version: 2.0.1 + resolution: "esniff@npm:2.0.1" + dependencies: + d: "npm:^1.0.1" + es5-ext: "npm:^0.10.62" + event-emitter: "npm:^0.3.5" + type: "npm:^2.7.2" + checksum: 10/f6a2abd2f8c5fe57c5fcf53e5407c278023313d0f6c3a92688e7122ab9ac233029fd424508a196ae5bc561aa1f67d23f4e2435b1a0d378030f476596129056ac + languageName: node + linkType: hard + "espree@npm:^10.0.1, espree@npm:^10.4.0": version: 10.4.0 resolution: "espree@npm:10.4.0" @@ -12916,6 +13916,16 @@ __metadata: languageName: node linkType: hard +"event-emitter@npm:^0.3.5": + version: 0.3.5 + resolution: "event-emitter@npm:0.3.5" + dependencies: + d: "npm:1" + es5-ext: "npm:~0.10.14" + checksum: 10/a7f5ea80029193f4869782d34ef7eb43baa49cd397013add1953491b24588468efbe7e3cc9eb87d53f33397e7aab690fd74c079ec440bf8b12856f6bdb6e9396 + languageName: node + linkType: hard + "event-target-shim@npm:6.0.2": version: 6.0.2 resolution: "event-target-shim@npm:6.0.2" @@ -12937,13 +13947,6 @@ __metadata: languageName: node linkType: hard -"exec-async@npm:^2.2.0": - version: 2.2.0 - resolution: "exec-async@npm:2.2.0" - checksum: 10/35932a49c825245e1fe022848a3ffef71717955149a3af8d56bf15b04a21c8f098581ffe2e4916a9dbd7736ce559365ccd55327e72422136adb9f4af867e1203 - languageName: node - linkType: hard - "execa@npm:^4.0.3": version: 4.1.0 resolution: "execa@npm:4.1.0" @@ -13028,14 +14031,28 @@ __metadata: languageName: node linkType: hard -"expo-blur@npm:~15.0.7": - version: 15.0.7 - resolution: "expo-blur@npm:15.0.7" +"expo-asset@npm:~55.0.10": + version: 55.0.10 + resolution: "expo-asset@npm:55.0.10" + dependencies: + "@expo/image-utils": "npm:^0.8.12" + expo-constants: "npm:~55.0.9" + peerDependencies: + expo: "*" + react: "*" + react-native: "*" + checksum: 10/1f23da4f0bb6e7df52c93fbc38a76d00e80a78efea8ece11d9b8ac01e12a8f4f9a56f99937ef4cc17b5d285b97cada35fdaf0945a848504814c9127154b283d9 + languageName: node + linkType: hard + +"expo-blur@npm:~55.0.10": + version: 55.0.10 + resolution: "expo-blur@npm:55.0.10" peerDependencies: expo: "*" react: "*" react-native: "*" - checksum: 10/1f4805ecf4e0bb7e2a6d89295dcfb1c2157658d55a43d4ce1a0b3700b9bf969cefd5a2b484b072c8dbe8ef460f8c0855eb5badad3eaec88ea0259a7b051e2cfc + checksum: 10/38924be289277e80c57ecbdbe6239400943489bf60378ef7c869cd6f2511144b4eb22fe619fd147653218c9df515e8bcf7ff7171666ebfed297e1d45590d4093 languageName: node linkType: hard @@ -13051,6 +14068,19 @@ __metadata: languageName: node linkType: hard +"expo-build-properties@npm:~55.0.10": + version: 55.0.10 + resolution: "expo-build-properties@npm:55.0.10" + dependencies: + "@expo/schema-utils": "npm:^55.0.2" + resolve-from: "npm:^5.0.0" + semver: "npm:^7.6.0" + peerDependencies: + expo: "*" + checksum: 10/fa1d2a679f8c4277a6b2acc1355bc0254b0b060c5d84fc703a65d796e5db0d8e7f250b19149a428950f52af56d193793f5777336fa71cc27b5adebd0d2187bc2 + languageName: node + linkType: hard + "expo-constants@npm:~18.0.8, expo-constants@npm:~18.0.9": version: 18.0.9 resolution: "expo-constants@npm:18.0.9" @@ -13064,6 +14094,34 @@ __metadata: languageName: node linkType: hard +"expo-constants@npm:~55.0.9": + version: 55.0.9 + resolution: "expo-constants@npm:55.0.9" + dependencies: + "@expo/config": "npm:~55.0.10" + "@expo/env": "npm:~2.1.1" + peerDependencies: + expo: "*" + react-native: "*" + checksum: 10/e28d6f94b97dc096b1f6ca326c967a1e95cf2df6170726f3ea22bb0b8c52041279af17d4432b6b4237d7d9e481c53dd18f07a1433705b00c91f4d180366cf16d + languageName: node + linkType: hard + +"expo-dev-client@npm:~55.0.19": + version: 55.0.19 + resolution: "expo-dev-client@npm:55.0.19" + dependencies: + expo-dev-launcher: "npm:55.0.20" + expo-dev-menu: "npm:55.0.17" + expo-dev-menu-interface: "npm:55.0.1" + expo-manifests: "npm:~55.0.11" + expo-updates-interface: "npm:~55.1.3" + peerDependencies: + expo: "*" + checksum: 10/5d23ee4fcf2216c9dd728a751b4d6e128561bf2927c8d51ea433f7238e40d0d783b40f58ad422bd08f7ce40f9c1f4cd16f7f3603d0356504148b2c9a648a5e1f + languageName: node + linkType: hard + "expo-dev-client@npm:~6.0.13": version: 6.0.13 resolution: "expo-dev-client@npm:6.0.13" @@ -13079,6 +14137,19 @@ __metadata: languageName: node linkType: hard +"expo-dev-launcher@npm:55.0.20": + version: 55.0.20 + resolution: "expo-dev-launcher@npm:55.0.20" + dependencies: + "@expo/schema-utils": "npm:^55.0.2" + expo-dev-menu: "npm:55.0.17" + expo-manifests: "npm:~55.0.11" + peerDependencies: + expo: "*" + checksum: 10/649f849522d6509e2e3785bd66766ae7fb3b2d71b4775e59da72ce35ece6395d3c2d61bff076fcb35abdb45943f50553706f3a7c52500b933a78165baffc6e4a + languageName: node + linkType: hard + "expo-dev-launcher@npm:6.0.13": version: 6.0.13 resolution: "expo-dev-launcher@npm:6.0.13" @@ -13100,6 +14171,26 @@ __metadata: languageName: node linkType: hard +"expo-dev-menu-interface@npm:55.0.1": + version: 55.0.1 + resolution: "expo-dev-menu-interface@npm:55.0.1" + peerDependencies: + expo: "*" + checksum: 10/848c557f906aecd0aceeacfcbcd9f299c37386e64e1fe07fe0ae3a214975f4eac4df230b8cdb4582bd4f7cd4adc107e83def024304f2444369f78a92bc14f977 + languageName: node + linkType: hard + +"expo-dev-menu@npm:55.0.17": + version: 55.0.17 + resolution: "expo-dev-menu@npm:55.0.17" + dependencies: + expo-dev-menu-interface: "npm:55.0.1" + peerDependencies: + expo: "*" + checksum: 10/84de5422fcb80d03845e926f27aa3326bccb08aaeda035e1b38ddaab7287ecbdff27742504c68d969e25c0a838d3d5aca8eb766163521f1d9d8a036c3da332e4 + languageName: node + linkType: hard + "expo-dev-menu@npm:7.0.13": version: 7.0.13 resolution: "expo-dev-menu@npm:7.0.13" @@ -13111,35 +14202,86 @@ __metadata: languageName: node linkType: hard -"expo-file-system@npm:~19.0.16": - version: 19.0.16 - resolution: "expo-file-system@npm:19.0.16" +"expo-file-system@npm:~19.0.16": + version: 19.0.16 + resolution: "expo-file-system@npm:19.0.16" + peerDependencies: + expo: "*" + react-native: "*" + checksum: 10/9a47544d8e8ad1bd5d591125abb7d7643f2fc16be543c5e7f148bb6385361fffda75e441ab279a6639eb7e3d4002c290a06dabc242bbad4ce88f6dbeaee978d5 + languageName: node + linkType: hard + +"expo-file-system@npm:~55.0.12": + version: 55.0.12 + resolution: "expo-file-system@npm:55.0.12" + peerDependencies: + expo: "*" + react-native: "*" + checksum: 10/71d962d4e2156c4074d765b8bf33d0db37e19231d9a67af491ffe9ba3101813a800a21cc641dbed24c81e5c1181aa47f2482b9bb2eb7295e42f9a0501692e778 + languageName: node + linkType: hard + +"expo-font@npm:~14.0.8": + version: 14.0.8 + resolution: "expo-font@npm:14.0.8" + dependencies: + fontfaceobserver: "npm:^2.1.0" + peerDependencies: + expo: "*" + react: "*" + react-native: "*" + checksum: 10/70112e94925e1f6035302dcd20ec101f321d679e2157bc8162441f1d9d34cda0b6d24ad4f0cf23957e78a8be26514ec70b84da4e089ce23782cb26d2bcd14285 + languageName: node + linkType: hard + +"expo-font@npm:~55.0.4": + version: 55.0.4 + resolution: "expo-font@npm:55.0.4" + dependencies: + fontfaceobserver: "npm:^2.1.0" + peerDependencies: + expo: "*" + react: "*" + react-native: "*" + checksum: 10/d590354e45c5a4a7a801ab04e4eec6b2a0bf42a2dde7e618f13cdf799d6ce86ccbfabb124de6b1f9ec0a077e74532429169d737948a1bdb0496b762fbf503c31 + languageName: node + linkType: hard + +"expo-glass-effect@npm:^55.0.8": + version: 55.0.8 + resolution: "expo-glass-effect@npm:55.0.8" peerDependencies: expo: "*" + react: "*" react-native: "*" - checksum: 10/9a47544d8e8ad1bd5d591125abb7d7643f2fc16be543c5e7f148bb6385361fffda75e441ab279a6639eb7e3d4002c290a06dabc242bbad4ce88f6dbeaee978d5 + checksum: 10/b3bcd5a4e65fda96a417f9033bb895104f8142cce6bba8656fdbfa60ab7da39bdbb235c8d527593fea4115b4065455f12657afdfb7cacda3b5cf7f3c22b49f36 languageName: node linkType: hard -"expo-font@npm:~14.0.8": - version: 14.0.8 - resolution: "expo-font@npm:14.0.8" - dependencies: - fontfaceobserver: "npm:^2.1.0" +"expo-haptics@npm:~55.0.9": + version: 55.0.9 + resolution: "expo-haptics@npm:55.0.9" peerDependencies: expo: "*" - react: "*" - react-native: "*" - checksum: 10/70112e94925e1f6035302dcd20ec101f321d679e2157bc8162441f1d9d34cda0b6d24ad4f0cf23957e78a8be26514ec70b84da4e089ce23782cb26d2bcd14285 + checksum: 10/4d0a39f6e2abd687681370408c9e3ad8b43516d603563849b950aa852730a2fd096910ddc6fdaf65554c40d61f12243f9ac48f1be3cc4319e8096c21085fd936 languageName: node linkType: hard -"expo-haptics@npm:~15.0.7": - version: 15.0.7 - resolution: "expo-haptics@npm:15.0.7" +"expo-image@npm:^55.0.6": + version: 55.0.6 + resolution: "expo-image@npm:55.0.6" + dependencies: + sf-symbols-typescript: "npm:^2.2.0" peerDependencies: expo: "*" - checksum: 10/5484f21a270ecd4251766e6e3bf2062ed4af2f5c1c743d270221eca30d1238b60d78b47dbe90564c6ef5d350beeb025d4d8c50411d015f92f37cf8bbc6113186 + react: "*" + react-native: "*" + react-native-web: "*" + peerDependenciesMeta: + react-native-web: + optional: true + checksum: 10/69da92574bf4971e7be7350a3b874fa70ee8795c038f1fea54fe2f96d3a1345329cc36117fc362f11f9d8f77bbecf4161abe221cfa6cd0c70e8759160e7d1d6f languageName: node linkType: hard @@ -13150,6 +14292,13 @@ __metadata: languageName: node linkType: hard +"expo-json-utils@npm:~55.0.0": + version: 55.0.0 + resolution: "expo-json-utils@npm:55.0.0" + checksum: 10/7aa53d4d8706705259757c1be73a50888b42df053af8f147c53afcd65ef154bd895ee51c7f0b24631b1f8f77c42d8ccdf69ca8950538cbdfb475520a1f3ffff8 + languageName: node + linkType: hard + "expo-keep-awake@npm:~15.0.7": version: 15.0.7 resolution: "expo-keep-awake@npm:15.0.7" @@ -13160,6 +14309,29 @@ __metadata: languageName: node linkType: hard +"expo-keep-awake@npm:~55.0.4": + version: 55.0.4 + resolution: "expo-keep-awake@npm:55.0.4" + peerDependencies: + expo: "*" + react: "*" + checksum: 10/02c47078b3600be15a59574f4840ba7b9a65c491bd8436e7147d3e02d61993fc14f2ade5897301c18652fb206203001416a1b66242a239ebb9f29519560eae3a + languageName: node + linkType: hard + +"expo-linking@npm:~55.0.9": + version: 55.0.9 + resolution: "expo-linking@npm:55.0.9" + dependencies: + expo-constants: "npm:~55.0.9" + invariant: "npm:^2.2.4" + peerDependencies: + react: "*" + react-native: "*" + checksum: 10/873d226789d3d1cc145a625b9e7ec16dba13c770bee86d21782cb09d3e948bcb852e34e47efb4aa01a248e384c9226ffafa07e414e9e47a1a7e28336cd57ae6f + languageName: node + linkType: hard + "expo-linking@npm:~8.0.8": version: 8.0.8 resolution: "expo-linking@npm:8.0.8" @@ -13185,6 +14357,18 @@ __metadata: languageName: node linkType: hard +"expo-manifests@npm:~55.0.11": + version: 55.0.11 + resolution: "expo-manifests@npm:55.0.11" + dependencies: + "@expo/config": "npm:~55.0.10" + expo-json-utils: "npm:~55.0.0" + peerDependencies: + expo: "*" + checksum: 10/2d823c136fb8fcf334612a7db1a6e1eda8dead881d21e06e6f9b244fd4af47fd31d514663fa76b220a0c3cec0bda5652261013b143ffefb1cebc7fe76a83ff35 + languageName: node + linkType: hard + "expo-module-scripts@npm:^5.0.7": version: 5.0.7 resolution: "expo-module-scripts@npm:5.0.7" @@ -13230,6 +14414,20 @@ __metadata: languageName: node linkType: hard +"expo-modules-autolinking@npm:55.0.12": + version: 55.0.12 + resolution: "expo-modules-autolinking@npm:55.0.12" + dependencies: + "@expo/require-utils": "npm:^55.0.3" + "@expo/spawn-async": "npm:^1.7.2" + chalk: "npm:^4.1.0" + commander: "npm:^7.2.0" + bin: + expo-modules-autolinking: bin/expo-modules-autolinking.js + checksum: 10/ba5297d901be989fadc47737acda2e499555d6b8f27b9640adc2c06c677d6d3470c0e22377b6f19e533c601990ddd01c28c55d70a375f543b3b77ce699096faa + languageName: node + linkType: hard + "expo-modules-core@npm:3.0.20": version: 3.0.20 resolution: "expo-modules-core@npm:3.0.20" @@ -13242,6 +14440,18 @@ __metadata: languageName: node linkType: hard +"expo-modules-core@npm:55.0.18": + version: 55.0.18 + resolution: "expo-modules-core@npm:55.0.18" + dependencies: + invariant: "npm:^2.2.4" + peerDependencies: + react: "*" + react-native: "*" + checksum: 10/2341cff5a97d55a5fcf192eb8df53d2a2ce0c57e3143b12179e6443f5ae5a4d794a963407e184ec22af8bbcf1815123fa78f5c6882c68c95406e6796bca83a79 + languageName: node + linkType: hard + "expo-notifications@npm:~0.32.12": version: 0.32.12 resolution: "expo-notifications@npm:0.32.12" @@ -13261,6 +14471,72 @@ __metadata: languageName: node linkType: hard +"expo-router@npm:~55.0.8": + version: 55.0.8 + resolution: "expo-router@npm:55.0.8" + dependencies: + "@expo/metro-runtime": "npm:^55.0.7" + "@expo/schema-utils": "npm:^55.0.2" + "@radix-ui/react-slot": "npm:^1.2.0" + "@radix-ui/react-tabs": "npm:^1.1.12" + "@react-navigation/bottom-tabs": "npm:^7.15.5" + "@react-navigation/native": "npm:^7.1.33" + "@react-navigation/native-stack": "npm:^7.14.5" + client-only: "npm:^0.0.1" + debug: "npm:^4.3.4" + escape-string-regexp: "npm:^4.0.0" + expo-glass-effect: "npm:^55.0.8" + expo-image: "npm:^55.0.6" + expo-server: "npm:^55.0.6" + expo-symbols: "npm:^55.0.5" + fast-deep-equal: "npm:^3.1.3" + invariant: "npm:^2.2.4" + nanoid: "npm:^3.3.8" + query-string: "npm:^7.1.3" + react-fast-compare: "npm:^3.2.2" + react-native-is-edge-to-edge: "npm:^1.2.1" + semver: "npm:~7.6.3" + server-only: "npm:^0.0.1" + sf-symbols-typescript: "npm:^2.1.0" + shallowequal: "npm:^1.1.0" + use-latest-callback: "npm:^0.2.1" + vaul: "npm:^1.1.2" + peerDependencies: + "@expo/log-box": 55.0.8 + "@expo/metro-runtime": ^55.0.7 + "@react-navigation/drawer": ^7.9.4 + "@testing-library/react-native": ">= 13.2.0" + expo: "*" + expo-constants: ^55.0.9 + expo-linking: ^55.0.9 + react: "*" + react-dom: "*" + react-native: "*" + react-native-gesture-handler: "*" + react-native-reanimated: "*" + react-native-safe-area-context: ">= 5.4.0" + react-native-screens: "*" + react-native-web: "*" + react-server-dom-webpack: ~19.0.4 || ~19.1.5 || ~19.2.4 + peerDependenciesMeta: + "@react-navigation/drawer": + optional: true + "@testing-library/react-native": + optional: true + react-dom: + optional: true + react-native-gesture-handler: + optional: true + react-native-reanimated: + optional: true + react-native-web: + optional: true + react-server-dom-webpack: + optional: true + checksum: 10/69f2b64a8c160e07f609ed30c367f867e2f2048b1ae9ed4258a61c73e0efa313867bba65b61e2b7e87f46cf49e4e470b83f9d808c8f9f4520085fe9c0051e539 + languageName: node + linkType: hard + "expo-router@npm:~6.0.10": version: 6.0.10 resolution: "expo-router@npm:6.0.10" @@ -13330,6 +14606,13 @@ __metadata: languageName: node linkType: hard +"expo-server@npm:^55.0.6": + version: 55.0.6 + resolution: "expo-server@npm:55.0.6" + checksum: 10/966ce7100313ed7ba2f9298eee14e828f8eac420636d0a1b59f60fd6aecfee205a5eea53883c9fa107b6bda15ab00cf37349c4fabfa7993c3f7b8039978c3318 + languageName: node + linkType: hard + "expo-splash-screen@npm:~31.0.10": version: 31.0.10 resolution: "expo-splash-screen@npm:31.0.10" @@ -13341,6 +14624,17 @@ __metadata: languageName: node linkType: hard +"expo-splash-screen@npm:~55.0.13": + version: 55.0.13 + resolution: "expo-splash-screen@npm:55.0.13" + dependencies: + "@expo/prebuild-config": "npm:^55.0.11" + peerDependencies: + expo: "*" + checksum: 10/26b5dba6e5a4dcf69a3e90e9b57e2e02aa1638b124aa4624dc847a56b9fe482b7287c1b778faf95099c54ca54e776b7e48d1064036d88077545a4dc6785039ea + languageName: node + linkType: hard + "expo-status-bar@npm:~3.0.8": version: 3.0.8 resolution: "expo-status-bar@npm:3.0.8" @@ -13353,15 +14647,47 @@ __metadata: languageName: node linkType: hard -"expo-symbols@npm:~1.0.7": - version: 1.0.7 - resolution: "expo-symbols@npm:1.0.7" +"expo-status-bar@npm:~55.0.4": + version: 55.0.4 + resolution: "expo-status-bar@npm:55.0.4" + dependencies: + react-native-is-edge-to-edge: "npm:^1.2.1" + peerDependencies: + react: "*" + react-native: "*" + checksum: 10/2f53930386ede3c46c78cb678d1ef7d66a330a703e3c24afd835e8a301477dba697c00e1deae25c1dd8406f17742e3a94e33d57a8c1b1f4365bcfe0084b0567c + languageName: node + linkType: hard + +"expo-symbols@npm:^55.0.5, expo-symbols@npm:~55.0.5": + version: 55.0.5 + resolution: "expo-symbols@npm:55.0.5" dependencies: + "@expo-google-fonts/material-symbols": "npm:^0.4.1" sf-symbols-typescript: "npm:^2.0.0" + peerDependencies: + expo: "*" + expo-font: "*" + react: "*" + react-native: "*" + checksum: 10/992834df221bd792ff251fc622bd5c6ecb3f1c1862a58273c9edef5e9934870c8f53693871a53dff20bc37792cd16e886fc5475e2e98de8955f95bcd825cc61d + languageName: node + linkType: hard + +"expo-system-ui@npm:~55.0.11": + version: 55.0.11 + resolution: "expo-system-ui@npm:55.0.11" + dependencies: + "@react-native/normalize-colors": "npm:0.83.4" + debug: "npm:^4.3.2" peerDependencies: expo: "*" react-native: "*" - checksum: 10/b1c636db969d1f6c5e1934f4856ecc6cf0bfb05988464495a24ec7605ec704b3386df1be835983aa91ae3e539a27d5d1bc1c3c09f1afa9cafc7daf3eeec00995 + react-native-web: "*" + peerDependenciesMeta: + react-native-web: + optional: true + checksum: 10/4d85cedfbee1ea01b206079567205b9b0bdcf676012e3df871336eeb1511b524fe8740aba66b995b323df8c1aa3dd2ff7484f193e6ede38dcd82b99ede31e05a languageName: node linkType: hard @@ -13391,13 +14717,22 @@ __metadata: languageName: node linkType: hard -"expo-web-browser@npm:~15.0.8": - version: 15.0.8 - resolution: "expo-web-browser@npm:15.0.8" +"expo-updates-interface@npm:~55.1.3": + version: 55.1.3 + resolution: "expo-updates-interface@npm:55.1.3" + peerDependencies: + expo: "*" + checksum: 10/a5ce27ac54d521978e3992e824c83f1f7819337f15c5cd9c052b689b3efb61e8c7c8387ece1a5ccadd0e2594bbd271df7f89e0c9db288c470fca04f12583106e + languageName: node + linkType: hard + +"expo-web-browser@npm:~55.0.10": + version: 55.0.10 + resolution: "expo-web-browser@npm:55.0.10" peerDependencies: expo: "*" react-native: "*" - checksum: 10/a170b9f173b236ef70f499c39ecaf5699e79fe8fd49aa9f24daea13eb1444d42928ae704a8884a55839391b786486c7a81e6f0974064701a25937132f49b7a96 + checksum: 10/f2e5506b3f1f2d6e99c8a23d9b23161df4c5d543044b22f341bf15593aad92e41fe0968148f0bcd80c638801ee6cf8c8dddf9a846fd77e45785dce8786a7ced4 languageName: node linkType: hard @@ -13447,6 +14782,54 @@ __metadata: languageName: node linkType: hard +"expo@npm:^55.0.0": + version: 55.0.9 + resolution: "expo@npm:55.0.9" + dependencies: + "@babel/runtime": "npm:^7.20.0" + "@expo/cli": "npm:55.0.19" + "@expo/config": "npm:~55.0.11" + "@expo/config-plugins": "npm:~55.0.7" + "@expo/devtools": "npm:55.0.2" + "@expo/fingerprint": "npm:0.16.6" + "@expo/local-build-cache-provider": "npm:55.0.7" + "@expo/log-box": "npm:55.0.8" + "@expo/metro": "npm:~54.2.0" + "@expo/metro-config": "npm:55.0.11" + "@expo/vector-icons": "npm:^15.0.2" + "@ungap/structured-clone": "npm:^1.3.0" + babel-preset-expo: "npm:~55.0.13" + expo-asset: "npm:~55.0.10" + expo-constants: "npm:~55.0.9" + expo-file-system: "npm:~55.0.12" + expo-font: "npm:~55.0.4" + expo-keep-awake: "npm:~55.0.4" + expo-modules-autolinking: "npm:55.0.12" + expo-modules-core: "npm:55.0.18" + pretty-format: "npm:^29.7.0" + react-refresh: "npm:^0.14.2" + whatwg-url-minimum: "npm:^0.1.1" + peerDependencies: + "@expo/dom-webview": "*" + "@expo/metro-runtime": "*" + react: "*" + react-native: "*" + react-native-webview: "*" + peerDependenciesMeta: + "@expo/dom-webview": + optional: true + "@expo/metro-runtime": + optional: true + react-native-webview: + optional: true + bin: + expo: bin/cli + expo-modules-autolinking: bin/autolinking + fingerprint: bin/fingerprint + checksum: 10/52ad55ec754ddd123ae05c3f8bbc495357f12a6207110a51862f6b1049fddc26e9e6f4761b1eac9cbf24831820517d06ef92a8d4b1b35e7e0d0902b29c8a1c1e + languageName: node + linkType: hard + "exponential-backoff@npm:^3.1.1": version: 3.1.1 resolution: "exponential-backoff@npm:3.1.1" @@ -13495,16 +14878,16 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:^3.2.7, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.2": - version: 3.3.2 - resolution: "fast-glob@npm:3.3.2" +"fast-glob@npm:^3.2.7, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.2, fast-glob@npm:^3.3.3": + version: 3.3.3 + resolution: "fast-glob@npm:3.3.3" dependencies: "@nodelib/fs.stat": "npm:^2.0.2" "@nodelib/fs.walk": "npm:^1.2.3" glob-parent: "npm:^5.1.2" merge2: "npm:^1.3.0" - micromatch: "npm:^4.0.4" - checksum: 10/222512e9315a0efca1276af9adb2127f02105d7288fa746145bf45e2716383fb79eb983c89601a72a399a56b7c18d38ce70457c5466218c5f13fad957cee16df + micromatch: "npm:^4.0.8" + checksum: 10/dcc6432b269762dd47381d8b8358bf964d8f4f60286ac6aa41c01ade70bda459ff2001b516690b96d5365f68a49242966112b5d5cc9cd82395fa8f9d017c90ad languageName: node linkType: hard @@ -13610,6 +14993,13 @@ __metadata: languageName: node linkType: hard +"fetch-nodeshim@npm:^0.4.6": + version: 0.4.10 + resolution: "fetch-nodeshim@npm:0.4.10" + checksum: 10/4abc48fe6bb2c44493f4d781a8d746e99133b18e456f44626fde36852e9f63fe7a3f71c1b0316e49725398c19b46d503389b4622e6e62b81f70add6da4b43cd7 + languageName: node + linkType: hard + "fflate@npm:^0.8.2": version: 0.8.2 resolution: "fflate@npm:0.8.2" @@ -13762,25 +15152,25 @@ __metadata: languageName: node linkType: hard -"firebase@npm:12.2.1": - version: 12.2.1 - resolution: "firebase@npm:12.2.1" +"firebase@npm:12.6.0": + version: 12.6.0 + resolution: "firebase@npm:12.6.0" dependencies: - "@firebase/ai": "npm:2.2.1" - "@firebase/analytics": "npm:0.10.18" - "@firebase/analytics-compat": "npm:0.2.24" - "@firebase/app": "npm:0.14.2" + "@firebase/ai": "npm:2.6.0" + "@firebase/analytics": "npm:0.10.19" + "@firebase/analytics-compat": "npm:0.2.25" + "@firebase/app": "npm:0.14.6" "@firebase/app-check": "npm:0.11.0" "@firebase/app-check-compat": "npm:0.4.0" - "@firebase/app-compat": "npm:0.5.2" + "@firebase/app-compat": "npm:0.5.6" "@firebase/app-types": "npm:0.9.3" - "@firebase/auth": "npm:1.11.0" - "@firebase/auth-compat": "npm:0.6.0" - "@firebase/data-connect": "npm:0.3.11" + "@firebase/auth": "npm:1.11.1" + "@firebase/auth-compat": "npm:0.6.1" + "@firebase/data-connect": "npm:0.3.12" "@firebase/database": "npm:1.1.0" "@firebase/database-compat": "npm:2.1.0" - "@firebase/firestore": "npm:4.9.1" - "@firebase/firestore-compat": "npm:0.4.1" + "@firebase/firestore": "npm:4.9.2" + "@firebase/firestore-compat": "npm:0.4.2" "@firebase/functions": "npm:0.13.1" "@firebase/functions-compat": "npm:0.4.1" "@firebase/installations": "npm:0.6.19" @@ -13789,12 +15179,12 @@ __metadata: "@firebase/messaging-compat": "npm:0.2.23" "@firebase/performance": "npm:0.7.9" "@firebase/performance-compat": "npm:0.2.22" - "@firebase/remote-config": "npm:0.6.6" - "@firebase/remote-config-compat": "npm:0.2.19" + "@firebase/remote-config": "npm:0.7.0" + "@firebase/remote-config-compat": "npm:0.2.20" "@firebase/storage": "npm:0.14.0" "@firebase/storage-compat": "npm:0.4.0" "@firebase/util": "npm:1.13.0" - checksum: 10/5fd586cecc4621fbd04fc0975b167ebe4ba1abf912e8ea7b0cae000a01380ff1ea5153e76de521fa3d245febddf0b3a9e1e251059a19b19602a1b977052a7617 + checksum: 10/b6f12864832dd6c80b37222466b927b8049888d93a50a22539dedf9a2923ef86b763c35467fca1537400dba61e96a7e693ead652ef2d18b2ab6b06418c8addaa languageName: node linkType: hard @@ -13845,13 +15235,13 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.15.6": - version: 1.15.6 - resolution: "follow-redirects@npm:1.15.6" +"follow-redirects@npm:^1.15.11, follow-redirects@npm:^1.15.6": + version: 1.15.11 + resolution: "follow-redirects@npm:1.15.11" peerDependenciesMeta: debug: optional: true - checksum: 10/70c7612c4cab18e546e36b991bbf8009a1a41cf85354afe04b113d1117569abf760269409cb3eb842d9f7b03d62826687086b081c566ea7b1e6613cf29030bf7 + checksum: 10/07372fd74b98c78cf4d417d68d41fdaa0be4dcacafffb9e67b1e3cf090bc4771515e65020651528faab238f10f9b9c0d9707d6c1574a6c0387c5de1042cde9ba languageName: node linkType: hard @@ -13881,16 +15271,16 @@ __metadata: languageName: node linkType: hard -"form-data@npm:^4.0.0, form-data@npm:^4.0.4": - version: 4.0.4 - resolution: "form-data@npm:4.0.4" +"form-data@npm:^4.0.0, form-data@npm:^4.0.4, form-data@npm:^4.0.5": + version: 4.0.5 + resolution: "form-data@npm:4.0.5" dependencies: asynckit: "npm:^0.4.0" combined-stream: "npm:^1.0.8" es-set-tostringtag: "npm:^2.1.0" hasown: "npm:^2.0.2" mime-types: "npm:^2.1.12" - checksum: 10/a4b62e21932f48702bc468cc26fb276d186e6b07b557e3dd7cc455872bdbb82db7db066844a64ad3cf40eaf3a753c830538183570462d3649fdfd705601cbcfb + checksum: 10/52ecd6e927c8c4e215e68a7ad5e0f7c1031397439672fd9741654b4a94722c4182e74cc815b225dcb5be3f4180f36428f67c6dd39eaa98af0dcfdd26c00c19cd languageName: node linkType: hard @@ -13937,7 +15327,7 @@ __metadata: languageName: node linkType: hard -"fresh@npm:0.5.2": +"fresh@npm:0.5.2, fresh@npm:~0.5.2": version: 0.5.2 resolution: "fresh@npm:0.5.2" checksum: 10/64c88e489b5d08e2f29664eb3c79c705ff9a8eb15d3e597198ef76546d4ade295897a44abb0abd2700e7ef784b2e3cbf1161e4fbf16f59129193fd1030d16da1 @@ -14126,10 +15516,10 @@ __metadata: languageName: node linkType: hard -"get-east-asian-width@npm:^1.0.0, get-east-asian-width@npm:^1.3.0": - version: 1.4.0 - resolution: "get-east-asian-width@npm:1.4.0" - checksum: 10/c9ae85bfc2feaf4cc71cdb236e60f1757ae82281964c206c6aa89a25f1987d326ddd8b0de9f9ccd56e37711b9fcd988f7f5137118b49b0b45e19df93c3be8f45 +"get-east-asian-width@npm:^1.0.0, get-east-asian-width@npm:^1.3.0, get-east-asian-width@npm:^1.3.1": + version: 1.5.0 + resolution: "get-east-asian-width@npm:1.5.0" + checksum: 10/60bc34cd1e975055ab99f0f177e31bed3e516ff7cee9c536474383954a976abaa6b94a51d99ad158ef1e372790fa096cab7d07f166bb0778f6587954c0fbe946 languageName: node linkType: hard @@ -14165,13 +15555,6 @@ __metadata: languageName: node linkType: hard -"get-port@npm:^3.2.0": - version: 3.2.0 - resolution: "get-port@npm:3.2.0" - checksum: 10/577b6ae47dcac1cb64f9bad28c9aa9e4cd8e8f2166c4224485dcdd1dede64154517a57a0eb55bfb557ad3d48f9a1b400415ed047f04002e936f96ddb247f645d - languageName: node - linkType: hard - "get-proto@npm:^1.0.0, get-proto@npm:^1.0.1": version: 1.0.1 resolution: "get-proto@npm:1.0.1" @@ -14329,6 +15712,17 @@ __metadata: languageName: node linkType: hard +"glob@npm:^13.0.0": + version: 13.0.6 + resolution: "glob@npm:13.0.6" + dependencies: + minimatch: "npm:^10.2.2" + minipass: "npm:^7.1.3" + path-scurry: "npm:^2.0.2" + checksum: 10/201ad69e5f0aa74e1d8c00a481581f8b8c804b6a4fbfabeeb8541f5d756932800331daeba99b58fb9e4cd67e12ba5a7eba5b82fb476691588418060b84353214 + languageName: node + linkType: hard + "glob@npm:^7.1.1, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.2.0": version: 7.2.3 resolution: "glob@npm:7.2.3" @@ -14368,22 +15762,6 @@ __metadata: languageName: node linkType: hard -"global-dirs@npm:^0.1.1": - version: 0.1.1 - resolution: "global-dirs@npm:0.1.1" - dependencies: - ini: "npm:^1.3.4" - checksum: 10/10624f5a8ddb8634c22804c6b24f93fb591c3639a6bc78e3584e01a238fc6f7b7965824184e57d63f6df36980b6c191484ad7bc6c35a1599b8f1d64be64c2a4a - languageName: node - linkType: hard - -"globals@npm:^11.1.0": - version: 11.12.0 - resolution: "globals@npm:11.12.0" - checksum: 10/9f054fa38ff8de8fa356502eb9d2dae0c928217b8b5c8de1f09f5c9b6c8a96d8b9bd3afc49acbcd384a98a81fea713c859e1b09e214c60509517bb8fc2bc13c2 - languageName: node - linkType: hard - "globals@npm:^14.0.0": version: 14.0.0 resolution: "globals@npm:14.0.0" @@ -14429,6 +15807,20 @@ __metadata: languageName: node linkType: hard +"globby@npm:^14.0.2": + version: 14.1.0 + resolution: "globby@npm:14.1.0" + dependencies: + "@sindresorhus/merge-streams": "npm:^2.1.0" + fast-glob: "npm:^3.3.3" + ignore: "npm:^7.0.3" + path-type: "npm:^6.0.0" + slash: "npm:^5.1.0" + unicorn-magic: "npm:^0.3.0" + checksum: 10/e527ff54f0dddf60abfabd0d9e799768619d957feecd8b13ef60481f270bfdce0d28f6b09267c60f8064798fb3003b8ec991375f7fe0233fbce5304e1741368c + languageName: node + linkType: hard + "globrex@npm:^0.1.2": version: 0.1.2 resolution: "globrex@npm:0.1.2" @@ -14638,6 +16030,13 @@ __metadata: languageName: node linkType: hard +"hermes-estree@npm:0.28.1": + version: 0.28.1 + resolution: "hermes-estree@npm:0.28.1" + checksum: 10/3195a1aa7035d96b77839e6bfd6832b51830518aaf8dabfca11248b84d6fb6abd27e21c8caa84229954a76b4f8a1e346b65d421a4daecd3053bd2ea08fe6abc9 + languageName: node + linkType: hard + "hermes-estree@npm:0.29.1": version: 0.29.1 resolution: "hermes-estree@npm:0.29.1" @@ -14652,6 +16051,20 @@ __metadata: languageName: node linkType: hard +"hermes-estree@npm:0.32.1": + version: 0.32.1 + resolution: "hermes-estree@npm:0.32.1" + checksum: 10/6d0c03216c69fcabe6a534ffcffd4bc21b54de1e7ae3c81f1cafce36c33c4acafe334ee27e865f65549b78971dbdb3d78be9b40281365a162c6a23a6b8f1e06b + languageName: node + linkType: hard + +"hermes-estree@npm:0.33.3": + version: 0.33.3 + resolution: "hermes-estree@npm:0.33.3" + checksum: 10/dfaac7eb91e282cf04f26c8f557fcadbfb78f630062c7abc1e75b9765918103ebee1359dffbe6c5e42a52c7cee0b14420affda984d534f76ba3d7e8d9ba98215 + languageName: node + linkType: hard + "hermes-parser@npm:0.23.1": version: 0.23.1 resolution: "hermes-parser@npm:0.23.1" @@ -14661,6 +16074,15 @@ __metadata: languageName: node linkType: hard +"hermes-parser@npm:0.28.1": + version: 0.28.1 + resolution: "hermes-parser@npm:0.28.1" + dependencies: + hermes-estree: "npm:0.28.1" + checksum: 10/cb2aa4d386929825c3bd8184eeb4e3dcf34892c1f850624d09a80aee0674bc2eb135eccaeb7ac33675552130229ee6160025c4e4f351d6a61b503bd8bfdf63f5 + languageName: node + linkType: hard + "hermes-parser@npm:0.29.1, hermes-parser@npm:^0.29.1": version: 0.29.1 resolution: "hermes-parser@npm:0.29.1" @@ -14679,6 +16101,24 @@ __metadata: languageName: node linkType: hard +"hermes-parser@npm:0.32.1, hermes-parser@npm:^0.32.0": + version: 0.32.1 + resolution: "hermes-parser@npm:0.32.1" + dependencies: + hermes-estree: "npm:0.32.1" + checksum: 10/f392d309e3e9d01a01fd71bda83a488906b1182ebf4073768a6528b28c7a1b54f099a4170593dcfad886c434927dbedf93eff985ec6cf78af4c6eded10e26f03 + languageName: node + linkType: hard + +"hermes-parser@npm:0.33.3": + version: 0.33.3 + resolution: "hermes-parser@npm:0.33.3" + dependencies: + hermes-estree: "npm:0.33.3" + checksum: 10/709dac7283a9eab706f3fff5c6f09deee5197a1a38751da66fdf499a307120ba3ef14ce734715430a838145531973a8c0b69874bf5bc615cca10059ee87f5ff3 + languageName: node + linkType: hard + "hermes-parser@npm:^0.25.1": version: 0.25.1 resolution: "hermes-parser@npm:0.25.1" @@ -14763,6 +16203,19 @@ __metadata: languageName: node linkType: hard +"http-errors@npm:~2.0.1": + version: 2.0.1 + resolution: "http-errors@npm:2.0.1" + dependencies: + depd: "npm:~2.0.0" + inherits: "npm:~2.0.4" + setprototypeof: "npm:~1.2.0" + statuses: "npm:~2.0.2" + toidentifier: "npm:~1.0.1" + checksum: 10/9fe31bc0edf36566c87048aed1d3d0cbe03552564adc3541626a0613f542d753fbcb13bdfcec0a3a530dbe1714bb566c89d46244616b66bddd26ac413b06a207 + languageName: node + linkType: hard + "http-parser-js@npm:>=0.5.1": version: 0.5.8 resolution: "http-parser-js@npm:0.5.8" @@ -14882,7 +16335,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:0.7.0, iconv-lite@npm:^0.7.0": +"iconv-lite@npm:0.7.0": version: 0.7.0 resolution: "iconv-lite@npm:0.7.0" dependencies: @@ -14891,6 +16344,15 @@ __metadata: languageName: node linkType: hard +"iconv-lite@npm:^0.7.0": + version: 0.7.2 + resolution: "iconv-lite@npm:0.7.2" + dependencies: + safer-buffer: "npm:>= 2.1.2 < 3.0.0" + checksum: 10/24c937b532f868e938386b62410b303b7c767ce3d08dc2829cbe59464d5a26ef86ae5ad1af6b34eec43ddfea39e7d101638644b0178d67262fa87015d59f983a + languageName: node + linkType: hard + "idb@npm:7.1.1": version: 7.1.1 resolution: "idb@npm:7.1.1" @@ -14912,7 +16374,7 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^7.0.0": +"ignore@npm:^7.0.0, ignore@npm:^7.0.3, ignore@npm:^7.0.5": version: 7.0.5 resolution: "ignore@npm:7.0.5" checksum: 10/f134b96a4de0af419196f52c529d5c6120c4456ff8a6b5a14ceaaa399f883e15d58d2ce651c9b69b9388491d4669dda47285d307e827de9304a53a1824801bc6 @@ -14965,14 +16427,14 @@ __metadata: linkType: hard "import-in-the-middle@npm:^2, import-in-the-middle@npm:^2.0.0": - version: 2.0.0 - resolution: "import-in-the-middle@npm:2.0.0" + version: 2.0.6 + resolution: "import-in-the-middle@npm:2.0.6" dependencies: - acorn: "npm:^8.14.0" + acorn: "npm:^8.15.0" acorn-import-attributes: "npm:^1.9.5" - cjs-module-lexer: "npm:^1.2.2" - module-details-from-path: "npm:^1.0.3" - checksum: 10/badb8359552f1e9fedc8569299dd1937e802256ce0fe6aa9cb348bca6f217f06e16a3ca46f889bfcb66028a096a1956674d257de9e809db4271ca0e508521c30 + cjs-module-lexer: "npm:^2.2.0" + module-details-from-path: "npm:^1.0.4" + checksum: 10/8be80d7f2d4ad34e5eb1082925ee2e90844edb65359cad0f5d8e934a09fafeca10e66f50d0b07570bd6b877ff678755d3c2d36d05258cc3541e39fa6aae6ae56 languageName: node linkType: hard @@ -15019,7 +16481,7 @@ __metadata: languageName: node linkType: hard -"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.3": +"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.3, inherits@npm:~2.0.4": version: 2.0.4 resolution: "inherits@npm:2.0.4" checksum: 10/cd45e923bee15186c07fa4c89db0aace24824c482fb887b528304694b2aa6ff8a898da8657046a5dcf3e46cd6db6c61629551f9215f208d7c3f157cf9b290521 @@ -15033,7 +16495,7 @@ __metadata: languageName: node linkType: hard -"ini@npm:^1.3.4, ini@npm:~1.3.0": +"ini@npm:~1.3.0": version: 1.3.8 resolution: "ini@npm:1.3.8" checksum: 10/314ae176e8d4deb3def56106da8002b462221c174ddb7ce0c49ee72c8cd1f9044f7b10cc555a7d8850982c3b9ca96fc212122749f5234bc2b6fb05fb942ed566 @@ -15246,7 +16708,7 @@ __metadata: languageName: node linkType: hard -"is-core-module@npm:^2.13.0, is-core-module@npm:^2.16.0, is-core-module@npm:^2.16.1": +"is-core-module@npm:^2.13.0, is-core-module@npm:^2.16.1": version: 2.16.1 resolution: "is-core-module@npm:2.16.1" dependencies: @@ -15330,11 +16792,11 @@ __metadata: linkType: hard "is-fullwidth-code-point@npm:^5.0.0": - version: 5.0.0 - resolution: "is-fullwidth-code-point@npm:5.0.0" + version: 5.1.0 + resolution: "is-fullwidth-code-point@npm:5.1.0" dependencies: - get-east-asian-width: "npm:^1.0.0" - checksum: 10/8dfb2d2831b9e87983c136f5c335cd9d14c1402973e357a8ff057904612ed84b8cba196319fabedf9aefe4639e14fe3afe9d9966d1d006ebeb40fe1fed4babe5 + get-east-asian-width: "npm:^1.3.1" + checksum: 10/4700d8a82cb71bd2a2955587b2823c36dc4660eadd4047bfbd070821ddbce8504fc5f9b28725567ecddf405b1e06c6692c9b719f65df6af9ec5262bc11393a6a languageName: node linkType: hard @@ -15462,6 +16924,13 @@ __metadata: languageName: node linkType: hard +"is-path-cwd@npm:^3.0.0": + version: 3.0.0 + resolution: "is-path-cwd@npm:3.0.0" + checksum: 10/bc34d13b6a03dfca4a3ab6a8a5ba78ae4b24f4f1db4b2b031d2760c60d0913bd16a4b980dcb4e590adfc906649d5f5132684079a3972bd219da49deebb9adea8 + languageName: node + linkType: hard + "is-path-inside@npm:^3.0.2": version: 3.0.3 resolution: "is-path-inside@npm:3.0.3" @@ -15469,6 +16938,13 @@ __metadata: languageName: node linkType: hard +"is-path-inside@npm:^4.0.0": + version: 4.0.0 + resolution: "is-path-inside@npm:4.0.0" + checksum: 10/8810fa11c58e6360b82c3e0d6cd7d9c7d0392d3ac9eb10f980b81f9839f40ac6d1d6d6f05d069db0d227759801228f0b072e1b6c343e4469b065ab5fe0b68fe5 + languageName: node + linkType: hard + "is-plain-obj@npm:^2.1.0": version: 2.1.0 resolution: "is-plain-obj@npm:2.1.0" @@ -15929,7 +17405,7 @@ __metadata: languageName: node linkType: hard -"jest-diff@npm:30.2.0, jest-diff@npm:^30.0.2": +"jest-diff@npm:30.2.0": version: 30.2.0 resolution: "jest-diff@npm:30.2.0" dependencies: @@ -15953,6 +17429,18 @@ __metadata: languageName: node linkType: hard +"jest-diff@npm:^30.0.2": + version: 30.3.0 + resolution: "jest-diff@npm:30.3.0" + dependencies: + "@jest/diff-sequences": "npm:30.3.0" + "@jest/get-type": "npm:30.1.0" + chalk: "npm:^4.1.2" + pretty-format: "npm:30.3.0" + checksum: 10/9f566259085e6badd525dc48ee6de3792cfae080abd66e170ac230359cf32c4334d92f0f48b577a31ad2a6aed4aefde81f5f4366ab44a96f78bcde975e5cc26e + languageName: node + linkType: hard + "jest-docblock@npm:^29.7.0": version: 29.7.0 resolution: "jest-docblock@npm:29.7.0" @@ -16405,9 +17893,9 @@ __metadata: linkType: hard "jose@npm:^4.10.0, jose@npm:^4.15.5": - version: 4.15.5 - resolution: "jose@npm:4.15.5" - checksum: 10/17944fcc0d9afa07387eef23127c30ecfcc77eafddc4b4f1a349a8eee0536bee9b08ecd745406eaa0af65d531f738b94d2467976479cbfe8b3b60f8fc8082b8d + version: 4.15.9 + resolution: "jose@npm:4.15.9" + checksum: 10/256234b6f85cdc080b1331f2d475bd58c8ccf459cb20f70ac5e4200b271bce10002b1c2f8e5b96dd975d83065ae5a586d52cdf89d28471d56de5d297992f9905 languageName: node linkType: hard @@ -16445,13 +17933,13 @@ __metadata: linkType: hard "js-yaml@npm:^4.0.0, js-yaml@npm:^4.1.0": - version: 4.1.0 - resolution: "js-yaml@npm:4.1.0" + version: 4.1.1 + resolution: "js-yaml@npm:4.1.1" dependencies: argparse: "npm:^2.0.1" bin: js-yaml: bin/js-yaml.js - checksum: 10/c138a34a3fd0d08ebaf71273ad4465569a483b8a639e0b118ff65698d257c2791d3199e3f303631f2cb98213fa7b5f5d6a4621fd0fff819421b990d30d967140 + checksum: 10/a52d0519f0f4ef5b4adc1cde466cb54c50d56e2b4a983b9d5c9c0f2f99462047007a6274d7e95617a21d3c91fde3ee6115536ed70991cd645ba8521058b78f77 languageName: node linkType: hard @@ -16534,12 +18022,12 @@ __metadata: languageName: node linkType: hard -"jsesc@npm:^3.0.2, jsesc@npm:~3.0.2": - version: 3.0.2 - resolution: "jsesc@npm:3.0.2" +"jsesc@npm:^3.0.2, jsesc@npm:~3.1.0": + version: 3.1.0 + resolution: "jsesc@npm:3.1.0" bin: jsesc: bin/jsesc - checksum: 10/8e5a7de6b70a8bd71f9cb0b5a7ade6a73ae6ab55e697c74cc997cede97417a3a65ed86c36f7dd6125fe49766e8386c845023d9e213916ca92c9dfdd56e2babf3 + checksum: 10/20bd37a142eca5d1794f354db8f1c9aeb54d85e1f5c247b371de05d23a9751ecd7bd3a9c4fc5298ea6fa09a100dafb4190fa5c98c6610b75952c3487f3ce7967 languageName: node linkType: hard @@ -16753,6 +18241,15 @@ __metadata: languageName: node linkType: hard +"lan-network@npm:^0.2.0": + version: 0.2.0 + resolution: "lan-network@npm:0.2.0" + bin: + lan-network: dist/lan-network-cli.js + checksum: 10/221291b52503454b37b0f51670f4b4a2844b727e73a706ce6b5167813ac00d06be333e2a8c6be3dc645222b99cc246d68f59642dd892c80d76bd294802b28f94 + languageName: node + linkType: hard + "launch-editor@npm:^2.9.1": version: 2.10.0 resolution: "launch-editor@npm:2.10.0" @@ -17221,9 +18718,9 @@ __metadata: linkType: hard "lru-cache@npm:^11.0.0": - version: 11.0.2 - resolution: "lru-cache@npm:11.0.2" - checksum: 10/25fcb66e9d91eaf17227c6abfe526a7bed5903de74f93bfde380eb8a13410c5e8d3f14fe447293f3f322a7493adf6f9f015c6f1df7a235ff24ec30f366e1c058 + version: 11.2.7 + resolution: "lru-cache@npm:11.2.7" + checksum: 10/fbff4b8dee8189dde9b52cdfb3ea89b4c9cec094c1538cd30d1f47299477ff312efdb35f7994477ec72328f8e754e232b26a143feda1bd1f79ff22da6664d2c5 languageName: node linkType: hard @@ -17262,11 +18759,11 @@ __metadata: linkType: hard "magic-string@npm:^0.30.17, magic-string@npm:^0.30.3": - version: 0.30.17 - resolution: "magic-string@npm:0.30.17" + version: 0.30.21 + resolution: "magic-string@npm:0.30.21" dependencies: - "@jridgewell/sourcemap-codec": "npm:^1.5.0" - checksum: 10/2f71af2b0afd78c2e9012a29b066d2c8ba45a9cd0c8070f7fd72de982fb1c403b4e3afdb1dae00691d56885ede66b772ef6bedf765e02e3a7066208fe2fec4aa + "@jridgewell/sourcemap-codec": "npm:^1.5.5" + checksum: 10/57d5691f41ed40d962d8bd300148114f53db67fadbff336207db10a99f2bdf4a1be9cac3a68ee85dba575912ee1d4402e4396408196ec2d3afd043b076156221 languageName: node linkType: hard @@ -17636,6 +19133,13 @@ __metadata: languageName: node linkType: hard +"meow@npm:^13.2.0": + version: 13.2.0 + resolution: "meow@npm:13.2.0" + checksum: 10/4eff5bc921fed0b8a471ad79069d741a0210036d717547d0c7f36fdaf84ef7a3036225f38b6a53830d84dc9cbf8b944b097fde62381b8b5b215119e735ce1063 + languageName: node + linkType: hard + "merge-options@npm:^3.0.4": version: 3.0.4 resolution: "merge-options@npm:3.0.4" @@ -17683,27 +19187,27 @@ __metadata: languageName: node linkType: hard -"metro-babel-transformer@npm:0.83.2": - version: 0.83.2 - resolution: "metro-babel-transformer@npm:0.83.2" +"metro-babel-transformer@npm:0.83.3": + version: 0.83.3 + resolution: "metro-babel-transformer@npm:0.83.3" dependencies: "@babel/core": "npm:^7.25.2" flow-enums-runtime: "npm:^0.0.6" hermes-parser: "npm:0.32.0" nullthrows: "npm:^1.1.1" - checksum: 10/8ca98216c3fc32757cbb445d2e42042617b5a2399d3d409759b168fbd3d52aadf8bb2b8471e4b204ddf5c654b7b146397edb7693f48a0582e7e4e169cf3bbfbb + checksum: 10/dd178409d1718dae12dfffb6572ebc5bb78f1e0d7e93dce829c945957f8a686cb1b4c466c69585d7b982b3937fbea28d5c53a80691f2fc66717a0bcc800bc5b8 languageName: node linkType: hard -"metro-babel-transformer@npm:0.83.3": - version: 0.83.3 - resolution: "metro-babel-transformer@npm:0.83.3" +"metro-babel-transformer@npm:0.83.5": + version: 0.83.5 + resolution: "metro-babel-transformer@npm:0.83.5" dependencies: "@babel/core": "npm:^7.25.2" flow-enums-runtime: "npm:^0.0.6" - hermes-parser: "npm:0.32.0" + hermes-parser: "npm:0.33.3" nullthrows: "npm:^1.1.1" - checksum: 10/dd178409d1718dae12dfffb6572ebc5bb78f1e0d7e93dce829c945957f8a686cb1b4c466c69585d7b982b3937fbea28d5c53a80691f2fc66717a0bcc800bc5b8 + checksum: 10/2a7664a55a5c3f276c884288978bf2fb4d5f5a5137f3769d5fdfd79d6a2f0027475b0d8a19ff1d8b3d39b91f4bb7c54dbd191f7d671d776ccd4a84183f69aee2 languageName: node linkType: hard @@ -17725,21 +19229,21 @@ __metadata: languageName: node linkType: hard -"metro-cache-key@npm:0.83.2": - version: 0.83.2 - resolution: "metro-cache-key@npm:0.83.2" +"metro-cache-key@npm:0.83.3": + version: 0.83.3 + resolution: "metro-cache-key@npm:0.83.3" dependencies: flow-enums-runtime: "npm:^0.0.6" - checksum: 10/ad60492b1db35b7d4eb1f9ed6f8aa79a051dcb1be3183fcd5b0a810e7c4ba5dba5e9f02e131ccd271d6db2efaa9893ef0e316ef26ebb3ab49cb074fada4de1b5 + checksum: 10/a6f9d2bf8b810f57d330d6f8f1ebf029e1224f426c5895f73d9bc1007482684048bfc7513a855626ee7f3ae72ca46e1b08cf983aefbfa84321bb7c0cef4ba4ae languageName: node linkType: hard -"metro-cache-key@npm:0.83.3": - version: 0.83.3 - resolution: "metro-cache-key@npm:0.83.3" +"metro-cache-key@npm:0.83.5": + version: 0.83.5 + resolution: "metro-cache-key@npm:0.83.5" dependencies: flow-enums-runtime: "npm:^0.0.6" - checksum: 10/a6f9d2bf8b810f57d330d6f8f1ebf029e1224f426c5895f73d9bc1007482684048bfc7513a855626ee7f3ae72ca46e1b08cf983aefbfa84321bb7c0cef4ba4ae + checksum: 10/704d0d8e06e8477d20c700cd5f729356aaa704999d4b80882b85aa21ccf7da13959dcd0760f9a456931466bf77dffe688f2a11f468aae5c074f74667957c6608 languageName: node linkType: hard @@ -17766,27 +19270,27 @@ __metadata: languageName: node linkType: hard -"metro-cache@npm:0.83.2": - version: 0.83.2 - resolution: "metro-cache@npm:0.83.2" +"metro-cache@npm:0.83.3": + version: 0.83.3 + resolution: "metro-cache@npm:0.83.3" dependencies: exponential-backoff: "npm:^3.1.1" flow-enums-runtime: "npm:^0.0.6" https-proxy-agent: "npm:^7.0.5" - metro-core: "npm:0.83.2" - checksum: 10/3183bcd8e0590ab4630f344f9dd4daa3b2371450e7f4546f2b1128b1386ecece204a74a7e3df49a8f3776b5a4a746fe4aa05f952a97e6f4f61deda80be5c55cf + metro-core: "npm:0.83.3" + checksum: 10/4bc263ac92f176451710ebd330d156675e40f028be02eb9659a9b024db9897f3ad8510809d699969cb6f06dc0f06d85c38ca7162fb9a70be44510fa03270e089 languageName: node linkType: hard -"metro-cache@npm:0.83.3": - version: 0.83.3 - resolution: "metro-cache@npm:0.83.3" +"metro-cache@npm:0.83.5": + version: 0.83.5 + resolution: "metro-cache@npm:0.83.5" dependencies: exponential-backoff: "npm:^3.1.1" flow-enums-runtime: "npm:^0.0.6" https-proxy-agent: "npm:^7.0.5" - metro-core: "npm:0.83.3" - checksum: 10/4bc263ac92f176451710ebd330d156675e40f028be02eb9659a9b024db9897f3ad8510809d699969cb6f06dc0f06d85c38ca7162fb9a70be44510fa03270e089 + metro-core: "npm:0.83.5" + checksum: 10/f2b3b9e85e46f262b0adeb36dcbd2e14692199ba834757013bc7fca200f66573ca1d3925090597326764f4efe57da3a1416b8b611cf83b6c965541a3c51af4f2 languageName: node linkType: hard @@ -17822,35 +19326,35 @@ __metadata: languageName: node linkType: hard -"metro-config@npm:0.83.2, metro-config@npm:^0.83.1": - version: 0.83.2 - resolution: "metro-config@npm:0.83.2" +"metro-config@npm:0.83.3": + version: 0.83.3 + resolution: "metro-config@npm:0.83.3" dependencies: connect: "npm:^3.6.5" flow-enums-runtime: "npm:^0.0.6" jest-validate: "npm:^29.7.0" - metro: "npm:0.83.2" - metro-cache: "npm:0.83.2" - metro-core: "npm:0.83.2" - metro-runtime: "npm:0.83.2" + metro: "npm:0.83.3" + metro-cache: "npm:0.83.3" + metro-core: "npm:0.83.3" + metro-runtime: "npm:0.83.3" yaml: "npm:^2.6.1" - checksum: 10/830696bb515ad421f1a25003d64c01bca580b2485c69266e03faf0c8f36f55283388fda5505f53ae400f8298502f712aab6c76655e45996907588288d2586c6b + checksum: 10/e377c375a48afc85a4d742f80a17fc178f9af7f5b007375e65bb49472ad78bc8e1f0ba4399411310ee8b856fb767bd81bd6dae19bec6ef6a44f0ece4d8457b30 languageName: node linkType: hard -"metro-config@npm:0.83.3, metro-config@npm:^0.83.3": - version: 0.83.3 - resolution: "metro-config@npm:0.83.3" +"metro-config@npm:0.83.5, metro-config@npm:^0.83.1, metro-config@npm:^0.83.3": + version: 0.83.5 + resolution: "metro-config@npm:0.83.5" dependencies: connect: "npm:^3.6.5" flow-enums-runtime: "npm:^0.0.6" jest-validate: "npm:^29.7.0" - metro: "npm:0.83.3" - metro-cache: "npm:0.83.3" - metro-core: "npm:0.83.3" - metro-runtime: "npm:0.83.3" + metro: "npm:0.83.5" + metro-cache: "npm:0.83.5" + metro-core: "npm:0.83.5" + metro-runtime: "npm:0.83.5" yaml: "npm:^2.6.1" - checksum: 10/e377c375a48afc85a4d742f80a17fc178f9af7f5b007375e65bb49472ad78bc8e1f0ba4399411310ee8b856fb767bd81bd6dae19bec6ef6a44f0ece4d8457b30 + checksum: 10/d085f7cd50b7c8557bd5b105fb23551ac3915ef162b62443fb9c44d9e25d450e37a729177c1267063167b5445e779c136b9a123c2c968d9ddfe6f979fb3f9ae2 languageName: node linkType: hard @@ -17876,25 +19380,25 @@ __metadata: languageName: node linkType: hard -"metro-core@npm:0.83.2, metro-core@npm:^0.83.1": - version: 0.83.2 - resolution: "metro-core@npm:0.83.2" +"metro-core@npm:0.83.3": + version: 0.83.3 + resolution: "metro-core@npm:0.83.3" dependencies: flow-enums-runtime: "npm:^0.0.6" lodash.throttle: "npm:^4.1.1" - metro-resolver: "npm:0.83.2" - checksum: 10/dbbef6b6d0cdb76ff808928cda59086aa4fc04a50ff76be8e19bd181d9cf270f4fe0a6b60883d0230aeeba2ba65a68875af549c83c2cfee5a1f0988ed1b4fccd + metro-resolver: "npm:0.83.3" + checksum: 10/6ef06214faa1d727396d986f989a8150f699d73c5764c66e06e61b08017e462141a7b4c9ca63f67becee58ea1394b41aabfff441e644fc1e945c715e07c60612 languageName: node linkType: hard -"metro-core@npm:0.83.3, metro-core@npm:^0.83.3": - version: 0.83.3 - resolution: "metro-core@npm:0.83.3" +"metro-core@npm:0.83.5, metro-core@npm:^0.83.1, metro-core@npm:^0.83.3": + version: 0.83.5 + resolution: "metro-core@npm:0.83.5" dependencies: flow-enums-runtime: "npm:^0.0.6" lodash.throttle: "npm:^4.1.1" - metro-resolver: "npm:0.83.3" - checksum: 10/6ef06214faa1d727396d986f989a8150f699d73c5764c66e06e61b08017e462141a7b4c9ca63f67becee58ea1394b41aabfff441e644fc1e945c715e07c60612 + metro-resolver: "npm:0.83.5" + checksum: 10/a65e83fc73f2cc42f9ea72f9d6c976b2272c9c3477f17c6a1288497995a5572d2a89c2ebf29b8ff45195bde29b2ae90fa58b7238dfcfe07928289f58049c2842 languageName: node linkType: hard @@ -17938,9 +19442,9 @@ __metadata: languageName: node linkType: hard -"metro-file-map@npm:0.83.2": - version: 0.83.2 - resolution: "metro-file-map@npm:0.83.2" +"metro-file-map@npm:0.83.3": + version: 0.83.3 + resolution: "metro-file-map@npm:0.83.3" dependencies: debug: "npm:^4.4.0" fb-watchman: "npm:^2.0.0" @@ -17951,13 +19455,13 @@ __metadata: micromatch: "npm:^4.0.4" nullthrows: "npm:^1.1.1" walker: "npm:^1.0.7" - checksum: 10/349a52c74cd02a1db75d0677c82e31750098e74a67bd1e10b2241e296897bfb20de2d8a2f27d7c292e2b3f492a36a191eb3c1bd5d09d5758b8febd36db86e58f + checksum: 10/be621b144168b6a35567d4313557596df68ee61c1b9a067fbf8272ec3db7c2d9d76849c9b8d2331716d6839c3f8e243e2b715ca2551d7ffebbd206a34c19591a languageName: node linkType: hard -"metro-file-map@npm:0.83.3": - version: 0.83.3 - resolution: "metro-file-map@npm:0.83.3" +"metro-file-map@npm:0.83.5": + version: 0.83.5 + resolution: "metro-file-map@npm:0.83.5" dependencies: debug: "npm:^4.4.0" fb-watchman: "npm:^2.0.0" @@ -17968,7 +19472,7 @@ __metadata: micromatch: "npm:^4.0.4" nullthrows: "npm:^1.1.1" walker: "npm:^1.0.7" - checksum: 10/be621b144168b6a35567d4313557596df68ee61c1b9a067fbf8272ec3db7c2d9d76849c9b8d2331716d6839c3f8e243e2b715ca2551d7ffebbd206a34c19591a + checksum: 10/0cce73c75bbf9b248628285554ddd73fce6f4e86ee4776c9f6b65fcf2cfd1f75b15e3f4cf2dc44ad91e5c78fc61a6eb7d3daaee09b61af2b55d82558a2b0423c languageName: node linkType: hard @@ -17992,23 +19496,23 @@ __metadata: languageName: node linkType: hard -"metro-minify-terser@npm:0.83.2": - version: 0.83.2 - resolution: "metro-minify-terser@npm:0.83.2" +"metro-minify-terser@npm:0.83.3": + version: 0.83.3 + resolution: "metro-minify-terser@npm:0.83.3" dependencies: flow-enums-runtime: "npm:^0.0.6" terser: "npm:^5.15.0" - checksum: 10/ee164bdd3ddf797e1b0f9fd71960b662b40fc3abead77521b1e1435291d38cc151442348362d6afee0596d52fcff48cc6a055a04a7928905e9557968e05293ac + checksum: 10/1de88b70b7c903147807baa46497491a87600594fd0868b6538bbb9d7785242cabfbe8bccf36cc2285d0e17be72445b512d00c496952a159572545f3e6bcb199 languageName: node linkType: hard -"metro-minify-terser@npm:0.83.3": - version: 0.83.3 - resolution: "metro-minify-terser@npm:0.83.3" +"metro-minify-terser@npm:0.83.5": + version: 0.83.5 + resolution: "metro-minify-terser@npm:0.83.5" dependencies: flow-enums-runtime: "npm:^0.0.6" terser: "npm:^5.15.0" - checksum: 10/1de88b70b7c903147807baa46497491a87600594fd0868b6538bbb9d7785242cabfbe8bccf36cc2285d0e17be72445b512d00c496952a159572545f3e6bcb199 + checksum: 10/b9e257b5a74343a271e89603479775ed76b9c5e7b28015bafbce2afb4d7507acf36e897fc78c2ee571ad89951ba0ca708188ecb33fff0b947d1cee0ea8fd7837 languageName: node linkType: hard @@ -18030,21 +19534,21 @@ __metadata: languageName: node linkType: hard -"metro-resolver@npm:0.83.2": - version: 0.83.2 - resolution: "metro-resolver@npm:0.83.2" +"metro-resolver@npm:0.83.3": + version: 0.83.3 + resolution: "metro-resolver@npm:0.83.3" dependencies: flow-enums-runtime: "npm:^0.0.6" - checksum: 10/2ba0cdda5c5a3ddac72fd486a310892638ba7d67a736246ec128674dfa6217d6169bdd0f811874435eae37f0201d72735fe7dddfc0c83a9e1439f05994bc293a + checksum: 10/a425376447505a088a365fc1fbe2753d452c0353a189f2c74833f2b30d6401de7ed90e36a927d355fa454d6c439a156eb66bcfcedfbbe8a78d313cf49acfbb4c languageName: node linkType: hard -"metro-resolver@npm:0.83.3": - version: 0.83.3 - resolution: "metro-resolver@npm:0.83.3" +"metro-resolver@npm:0.83.5": + version: 0.83.5 + resolution: "metro-resolver@npm:0.83.5" dependencies: flow-enums-runtime: "npm:^0.0.6" - checksum: 10/a425376447505a088a365fc1fbe2753d452c0353a189f2c74833f2b30d6401de7ed90e36a927d355fa454d6c439a156eb66bcfcedfbbe8a78d313cf49acfbb4c + checksum: 10/0ad900735aa3446d8e5b341ff921b990895bb26517be96530b2a7c21504a617fa079299447b5ea4e3014894c94bcab7da54d37cbdc00bcc0c54f5c645c1d42cd languageName: node linkType: hard @@ -18068,23 +19572,23 @@ __metadata: languageName: node linkType: hard -"metro-runtime@npm:0.83.2, metro-runtime@npm:^0.83.1": - version: 0.83.2 - resolution: "metro-runtime@npm:0.83.2" +"metro-runtime@npm:0.83.3": + version: 0.83.3 + resolution: "metro-runtime@npm:0.83.3" dependencies: "@babel/runtime": "npm:^7.25.0" flow-enums-runtime: "npm:^0.0.6" - checksum: 10/1666e0e5c51d39f916642ed3918cf1996f76e82366ba9ca3132d6c11c5c62a1ab1115e4aa325f0fc9b8cefbe62d6ca8d1948cfde2ee78963491deafcbc79adba + checksum: 10/bf916759a7178e1d12e131c64ac67d6015ba35ead7a178e6efedd23f12ec65de99f450fe7da0ffb6c6edbfeb3cd186d2006b979a1c1c588377ae54f5f5d7921d languageName: node linkType: hard -"metro-runtime@npm:0.83.3, metro-runtime@npm:^0.83.3": - version: 0.83.3 - resolution: "metro-runtime@npm:0.83.3" +"metro-runtime@npm:0.83.5, metro-runtime@npm:^0.83.1, metro-runtime@npm:^0.83.3": + version: 0.83.5 + resolution: "metro-runtime@npm:0.83.5" dependencies: "@babel/runtime": "npm:^7.25.0" flow-enums-runtime: "npm:^0.0.6" - checksum: 10/bf916759a7178e1d12e131c64ac67d6015ba35ead7a178e6efedd23f12ec65de99f450fe7da0ffb6c6edbfeb3cd186d2006b979a1c1c588377ae54f5f5d7921d + checksum: 10/95a5f670fb2b230eea86e29833d0353c0fc845905fdae65c2f8a63c272ea095bf94976db7e28908bc6213ca22dffc21438eb18360321d92d8fb5aeb12a8d7520 languageName: node linkType: hard @@ -18123,39 +19627,38 @@ __metadata: languageName: node linkType: hard -"metro-source-map@npm:0.83.2, metro-source-map@npm:^0.83.1": - version: 0.83.2 - resolution: "metro-source-map@npm:0.83.2" +"metro-source-map@npm:0.83.3": + version: 0.83.3 + resolution: "metro-source-map@npm:0.83.3" dependencies: "@babel/traverse": "npm:^7.25.3" "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3" "@babel/types": "npm:^7.25.2" flow-enums-runtime: "npm:^0.0.6" invariant: "npm:^2.2.4" - metro-symbolicate: "npm:0.83.2" + metro-symbolicate: "npm:0.83.3" nullthrows: "npm:^1.1.1" - ob1: "npm:0.83.2" + ob1: "npm:0.83.3" source-map: "npm:^0.5.6" vlq: "npm:^1.0.0" - checksum: 10/6253f6aa9a19ff35d70a08e1a434b9641874392e3cccec6abc8dcbac1c3e9289e348fa37960f16581c386e8f9ba743631ecc8ed5bf42817a5d5c54b6784c63b5 + checksum: 10/1dcfce503628275f97dd85945ca575c71e5654fd8872b7d86449f3352cfc84ea7a59889b2aad012361245b5497e1e097db73390245952dcfb63258ba32fa90bf languageName: node linkType: hard -"metro-source-map@npm:0.83.3, metro-source-map@npm:^0.83.3": - version: 0.83.3 - resolution: "metro-source-map@npm:0.83.3" +"metro-source-map@npm:0.83.5, metro-source-map@npm:^0.83.1, metro-source-map@npm:^0.83.3": + version: 0.83.5 + resolution: "metro-source-map@npm:0.83.5" dependencies: - "@babel/traverse": "npm:^7.25.3" - "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3" - "@babel/types": "npm:^7.25.2" + "@babel/traverse": "npm:^7.29.0" + "@babel/types": "npm:^7.29.0" flow-enums-runtime: "npm:^0.0.6" invariant: "npm:^2.2.4" - metro-symbolicate: "npm:0.83.3" + metro-symbolicate: "npm:0.83.5" nullthrows: "npm:^1.1.1" - ob1: "npm:0.83.3" + ob1: "npm:0.83.5" source-map: "npm:^0.5.6" vlq: "npm:^1.0.0" - checksum: 10/1dcfce503628275f97dd85945ca575c71e5654fd8872b7d86449f3352cfc84ea7a59889b2aad012361245b5497e1e097db73390245952dcfb63258ba32fa90bf + checksum: 10/55e9562f95e1056b48bd4b705a8ff01998c0bb9da2166638141ce7404f8800caa5c7ba077ead999809245400e38bbff1e175c2feefd044ac78a69f9a69c73d3d languageName: node linkType: hard @@ -18192,35 +19695,35 @@ __metadata: languageName: node linkType: hard -"metro-symbolicate@npm:0.83.2": - version: 0.83.2 - resolution: "metro-symbolicate@npm:0.83.2" +"metro-symbolicate@npm:0.83.3": + version: 0.83.3 + resolution: "metro-symbolicate@npm:0.83.3" dependencies: flow-enums-runtime: "npm:^0.0.6" invariant: "npm:^2.2.4" - metro-source-map: "npm:0.83.2" + metro-source-map: "npm:0.83.3" nullthrows: "npm:^1.1.1" source-map: "npm:^0.5.6" vlq: "npm:^1.0.0" bin: metro-symbolicate: src/index.js - checksum: 10/1ddd82d0f1e236f4eb69c49b319a5446f364aaa421b4301554898abe86d23a452a5fb5113bfef6b6c68c2a697ad3a68fb00919a2f7b9b73a040c92689002a8d4 + checksum: 10/f3be0740655732044e92728a3bccd5f4a73ab2f9e4423ca05faee02446e9b2efd9400cc7bcd761fad9bc2a1b92855ce5b03bf13e0421a203fe179be40dcc9381 languageName: node linkType: hard -"metro-symbolicate@npm:0.83.3": - version: 0.83.3 - resolution: "metro-symbolicate@npm:0.83.3" +"metro-symbolicate@npm:0.83.5": + version: 0.83.5 + resolution: "metro-symbolicate@npm:0.83.5" dependencies: flow-enums-runtime: "npm:^0.0.6" invariant: "npm:^2.2.4" - metro-source-map: "npm:0.83.3" + metro-source-map: "npm:0.83.5" nullthrows: "npm:^1.1.1" source-map: "npm:^0.5.6" vlq: "npm:^1.0.0" bin: metro-symbolicate: src/index.js - checksum: 10/f3be0740655732044e92728a3bccd5f4a73ab2f9e4423ca05faee02446e9b2efd9400cc7bcd761fad9bc2a1b92855ce5b03bf13e0421a203fe179be40dcc9381 + checksum: 10/56cab184eff91d13f6122342f6564dd1b9bba97a32017c21ca1b0dade69a9020a53ef6971668a02ac0d4c457a05941162f3e6052a5854d124a30a63ee611d59b languageName: node linkType: hard @@ -18252,9 +19755,9 @@ __metadata: languageName: node linkType: hard -"metro-transform-plugins@npm:0.83.2": - version: 0.83.2 - resolution: "metro-transform-plugins@npm:0.83.2" +"metro-transform-plugins@npm:0.83.3": + version: 0.83.3 + resolution: "metro-transform-plugins@npm:0.83.3" dependencies: "@babel/core": "npm:^7.25.2" "@babel/generator": "npm:^7.25.0" @@ -18262,21 +19765,21 @@ __metadata: "@babel/traverse": "npm:^7.25.3" flow-enums-runtime: "npm:^0.0.6" nullthrows: "npm:^1.1.1" - checksum: 10/e3ebef11d64e5e568fde3fe2edc5d7f1e9508b28c7607c14dd711bc29058cbfc97e53edbfee79bd60f58c189e4d74869d87a30488534024fe88503296a7d095a + checksum: 10/fa7efe6ab4f2ce5f66e1cb302f71341cf7fd55319cf360a269b187d2f507cecce8db8069f92585cf43517aee63e18cf6e66dd124db95c293902ab27c68ac43b1 languageName: node linkType: hard -"metro-transform-plugins@npm:0.83.3": - version: 0.83.3 - resolution: "metro-transform-plugins@npm:0.83.3" +"metro-transform-plugins@npm:0.83.5": + version: 0.83.5 + resolution: "metro-transform-plugins@npm:0.83.5" dependencies: "@babel/core": "npm:^7.25.2" - "@babel/generator": "npm:^7.25.0" - "@babel/template": "npm:^7.25.0" - "@babel/traverse": "npm:^7.25.3" + "@babel/generator": "npm:^7.29.1" + "@babel/template": "npm:^7.28.6" + "@babel/traverse": "npm:^7.29.0" flow-enums-runtime: "npm:^0.0.6" nullthrows: "npm:^1.1.1" - checksum: 10/fa7efe6ab4f2ce5f66e1cb302f71341cf7fd55319cf360a269b187d2f507cecce8db8069f92585cf43517aee63e18cf6e66dd124db95c293902ab27c68ac43b1 + checksum: 10/227da814239803d8c8288a403fe166e4d99b4d070426c57dc4a02e82c117cf9398b40a82b5e1060f1ebdb65a882dab840dbbea7d3f09a97ef3d3e4f6297fc2af languageName: node linkType: hard @@ -18322,27 +19825,6 @@ __metadata: languageName: node linkType: hard -"metro-transform-worker@npm:0.83.2": - version: 0.83.2 - resolution: "metro-transform-worker@npm:0.83.2" - dependencies: - "@babel/core": "npm:^7.25.2" - "@babel/generator": "npm:^7.25.0" - "@babel/parser": "npm:^7.25.3" - "@babel/types": "npm:^7.25.2" - flow-enums-runtime: "npm:^0.0.6" - metro: "npm:0.83.2" - metro-babel-transformer: "npm:0.83.2" - metro-cache: "npm:0.83.2" - metro-cache-key: "npm:0.83.2" - metro-minify-terser: "npm:0.83.2" - metro-source-map: "npm:0.83.2" - metro-transform-plugins: "npm:0.83.2" - nullthrows: "npm:^1.1.1" - checksum: 10/b4286b1b0511e46e2ec265e24138d03d8a794687260beae297de3d378285cce0e06132280dac62d447dfaf55627432c28463939a63136f3a84c2cf6b880d3865 - languageName: node - linkType: hard - "metro-transform-worker@npm:0.83.3": version: 0.83.3 resolution: "metro-transform-worker@npm:0.83.3" @@ -18364,6 +19846,27 @@ __metadata: languageName: node linkType: hard +"metro-transform-worker@npm:0.83.5": + version: 0.83.5 + resolution: "metro-transform-worker@npm:0.83.5" + dependencies: + "@babel/core": "npm:^7.25.2" + "@babel/generator": "npm:^7.29.1" + "@babel/parser": "npm:^7.29.0" + "@babel/types": "npm:^7.29.0" + flow-enums-runtime: "npm:^0.0.6" + metro: "npm:0.83.5" + metro-babel-transformer: "npm:0.83.5" + metro-cache: "npm:0.83.5" + metro-cache-key: "npm:0.83.5" + metro-minify-terser: "npm:0.83.5" + metro-source-map: "npm:0.83.5" + metro-transform-plugins: "npm:0.83.5" + nullthrows: "npm:^1.1.1" + checksum: 10/6f3201cde7af9cb063ce0dd40b695dbcc658856e8db1d03d3b0c6854dab692477c33885c7891cb2f829ca6c682e7842f9a1801ac4c62db711183d2f7dd33a10d + languageName: node + linkType: hard + "metro@npm:0.80.12": version: 0.80.12 resolution: "metro@npm:0.80.12" @@ -18466,9 +19969,9 @@ __metadata: languageName: node linkType: hard -"metro@npm:0.83.2, metro@npm:^0.83.1": - version: 0.83.2 - resolution: "metro@npm:0.83.2" +"metro@npm:0.83.3": + version: 0.83.3 + resolution: "metro@npm:0.83.3" dependencies: "@babel/code-frame": "npm:^7.24.7" "@babel/core": "npm:^7.25.2" @@ -18491,18 +19994,18 @@ __metadata: jest-worker: "npm:^29.7.0" jsc-safe-url: "npm:^0.2.2" lodash.throttle: "npm:^4.1.1" - metro-babel-transformer: "npm:0.83.2" - metro-cache: "npm:0.83.2" - metro-cache-key: "npm:0.83.2" - metro-config: "npm:0.83.2" - metro-core: "npm:0.83.2" - metro-file-map: "npm:0.83.2" - metro-resolver: "npm:0.83.2" - metro-runtime: "npm:0.83.2" - metro-source-map: "npm:0.83.2" - metro-symbolicate: "npm:0.83.2" - metro-transform-plugins: "npm:0.83.2" - metro-transform-worker: "npm:0.83.2" + metro-babel-transformer: "npm:0.83.3" + metro-cache: "npm:0.83.3" + metro-cache-key: "npm:0.83.3" + metro-config: "npm:0.83.3" + metro-core: "npm:0.83.3" + metro-file-map: "npm:0.83.3" + metro-resolver: "npm:0.83.3" + metro-runtime: "npm:0.83.3" + metro-source-map: "npm:0.83.3" + metro-symbolicate: "npm:0.83.3" + metro-transform-plugins: "npm:0.83.3" + metro-transform-worker: "npm:0.83.3" mime-types: "npm:^2.1.27" nullthrows: "npm:^1.1.1" serialize-error: "npm:^2.1.0" @@ -18512,22 +20015,22 @@ __metadata: yargs: "npm:^17.6.2" bin: metro: src/cli.js - checksum: 10/524c0f98ce8be619a345f58c39d19e6d0e5745dfd156c9b0a06201e6d9ad59e4405922f09f56fe92a86df9e06b0e89b173a3136640f1ec69c395b9ca34c1b042 + checksum: 10/c989031710f02e51d3030660f1913870885647c5a216068333f7b4c43363f9ede03a9efb3b068b6750c6decab40f541376c3d81b32389d24932a46e10d19ebe1 languageName: node linkType: hard -"metro@npm:0.83.3, metro@npm:^0.83.3": - version: 0.83.3 - resolution: "metro@npm:0.83.3" +"metro@npm:0.83.5, metro@npm:^0.83.1, metro@npm:^0.83.3": + version: 0.83.5 + resolution: "metro@npm:0.83.5" dependencies: - "@babel/code-frame": "npm:^7.24.7" + "@babel/code-frame": "npm:^7.29.0" "@babel/core": "npm:^7.25.2" - "@babel/generator": "npm:^7.25.0" - "@babel/parser": "npm:^7.25.3" - "@babel/template": "npm:^7.25.0" - "@babel/traverse": "npm:^7.25.3" - "@babel/types": "npm:^7.25.2" - accepts: "npm:^1.3.7" + "@babel/generator": "npm:^7.29.1" + "@babel/parser": "npm:^7.29.0" + "@babel/template": "npm:^7.28.6" + "@babel/traverse": "npm:^7.29.0" + "@babel/types": "npm:^7.29.0" + accepts: "npm:^2.0.0" chalk: "npm:^4.0.0" ci-info: "npm:^2.0.0" connect: "npm:^3.6.5" @@ -18535,25 +20038,25 @@ __metadata: error-stack-parser: "npm:^2.0.6" flow-enums-runtime: "npm:^0.0.6" graceful-fs: "npm:^4.2.4" - hermes-parser: "npm:0.32.0" + hermes-parser: "npm:0.33.3" image-size: "npm:^1.0.2" invariant: "npm:^2.2.4" jest-worker: "npm:^29.7.0" jsc-safe-url: "npm:^0.2.2" lodash.throttle: "npm:^4.1.1" - metro-babel-transformer: "npm:0.83.3" - metro-cache: "npm:0.83.3" - metro-cache-key: "npm:0.83.3" - metro-config: "npm:0.83.3" - metro-core: "npm:0.83.3" - metro-file-map: "npm:0.83.3" - metro-resolver: "npm:0.83.3" - metro-runtime: "npm:0.83.3" - metro-source-map: "npm:0.83.3" - metro-symbolicate: "npm:0.83.3" - metro-transform-plugins: "npm:0.83.3" - metro-transform-worker: "npm:0.83.3" - mime-types: "npm:^2.1.27" + metro-babel-transformer: "npm:0.83.5" + metro-cache: "npm:0.83.5" + metro-cache-key: "npm:0.83.5" + metro-config: "npm:0.83.5" + metro-core: "npm:0.83.5" + metro-file-map: "npm:0.83.5" + metro-resolver: "npm:0.83.5" + metro-runtime: "npm:0.83.5" + metro-source-map: "npm:0.83.5" + metro-symbolicate: "npm:0.83.5" + metro-transform-plugins: "npm:0.83.5" + metro-transform-worker: "npm:0.83.5" + mime-types: "npm:^3.0.1" nullthrows: "npm:^1.1.1" serialize-error: "npm:^2.1.0" source-map: "npm:^0.5.6" @@ -18562,7 +20065,7 @@ __metadata: yargs: "npm:^17.6.2" bin: metro: src/cli.js - checksum: 10/c989031710f02e51d3030660f1913870885647c5a216068333f7b4c43363f9ede03a9efb3b068b6750c6decab40f541376c3d81b32389d24932a46e10d19ebe1 + checksum: 10/3c4643121335cf157696531829448b2c86ec653d5a7a11aa9cd005a1b9ad7a3f87f5e6ba8b997fc87e7b9f679a212d74db16739b4526a42425c6fb83e86283dc languageName: node linkType: hard @@ -18912,7 +20415,7 @@ __metadata: languageName: node linkType: hard -"mime-db@npm:>= 1.43.0 < 2": +"mime-db@npm:>= 1.43.0 < 2, mime-db@npm:^1.54.0": version: 1.54.0 resolution: "mime-db@npm:1.54.0" checksum: 10/9e7834be3d66ae7f10eaa69215732c6d389692b194f876198dca79b2b90cbf96688d9d5d05ef7987b20f749b769b11c01766564264ea5f919c88b32a29011311 @@ -18928,6 +20431,15 @@ __metadata: languageName: node linkType: hard +"mime-types@npm:^3.0.0, mime-types@npm:^3.0.1": + version: 3.0.2 + resolution: "mime-types@npm:3.0.2" + dependencies: + mime-db: "npm:^1.54.0" + checksum: 10/9db0ad31f5eff10ee8f848130779b7f2d056ddfdb6bda696cb69be68d486d33a3457b4f3f9bdeb60d0736edb471bd5a7c0a384375c011c51c889fd0d5c3b893e + languageName: node + linkType: hard + "mime@npm:1.6.0": version: 1.6.0 resolution: "mime@npm:1.6.0" @@ -18992,21 +20504,21 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^10.0.3": - version: 10.0.3 - resolution: "minimatch@npm:10.0.3" +"minimatch@npm:^10.0.3, minimatch@npm:^10.2.2": + version: 10.2.5 + resolution: "minimatch@npm:10.2.5" dependencies: - "@isaacs/brace-expansion": "npm:^5.0.0" - checksum: 10/d5b8b2538b367f2cfd4aeef27539fddeee58d1efb692102b848e4a968a09780a302c530eb5aacfa8c57f7299155fb4b4e85219ad82664dcef5c66f657111d9b8 + brace-expansion: "npm:^5.0.5" + checksum: 10/19e87a931aff60ee7b9d80f39f817b8bfc54f61f8356ee3549fbf636dbccacacfec8d803eac73293955c4527cd085247dfc064bce4a5e349f8f3b85e2bf5da0f languageName: node linkType: hard "minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": - version: 3.1.2 - resolution: "minimatch@npm:3.1.2" + version: 3.1.5 + resolution: "minimatch@npm:3.1.5" dependencies: brace-expansion: "npm:^1.1.7" - checksum: 10/e0b25b04cd4ec6732830344e5739b13f8690f8a012d73445a4a19fbc623f5dd481ef7a5827fde25954cd6026fede7574cc54dc4643c99d6c6b653d6203f94634 + checksum: 10/b11a7ee5773cd34c1a0c8436cdbe910901018fb4b6cb47aa508a18d567f6efd2148507959e35fba798389b161b8604a2d704ccef751ea36bd4582f9852b7d63f languageName: node linkType: hard @@ -19118,10 +20630,10 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.4, minipass@npm:^7.1.2": - version: 7.1.2 - resolution: "minipass@npm:7.1.2" - checksum: 10/c25f0ee8196d8e6036661104bacd743785b2599a21de5c516b32b3fa2b83113ac89a2358465bc04956baab37ffb956ae43be679b2262bf7be15fce467ccd7950 +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.4, minipass@npm:^7.1.2, minipass@npm:^7.1.3": + version: 7.1.3 + resolution: "minipass@npm:7.1.3" + checksum: 10/175e4d5e20980c3cd316ae82d2c031c42f6c746467d8b1905b51060a0ba4461441a0c25bb67c025fd9617f9a3873e152c7b543c6b5ac83a1846be8ade80dffd6 languageName: node linkType: hard @@ -19136,11 +20648,11 @@ __metadata: linkType: hard "minizlib@npm:^3.0.1": - version: 3.0.2 - resolution: "minizlib@npm:3.0.2" + version: 3.1.0 + resolution: "minizlib@npm:3.1.0" dependencies: minipass: "npm:^7.1.2" - checksum: 10/c075bed1594f68dcc8c35122333520112daefd4d070e5d0a228bd4cf5580e9eed3981b96c0ae1d62488e204e80fd27b2b9d0068ca9a5ef3993e9565faf63ca41 + checksum: 10/f47365cc2cb7f078cbe7e046eb52655e2e7e97f8c0a9a674f4da60d94fb0624edfcec9b5db32e8ba5a99a5f036f595680ae6fe02a262beaa73026e505cc52f99 languageName: node linkType: hard @@ -19151,17 +20663,6 @@ __metadata: languageName: node linkType: hard -"mkdirp@npm:^0.5.1": - version: 0.5.6 - resolution: "mkdirp@npm:0.5.6" - dependencies: - minimist: "npm:^1.2.6" - bin: - mkdirp: bin/cmd.js - checksum: 10/0c91b721bb12c3f9af4b77ebf73604baf350e64d80df91754dc509491ae93bf238581e59c7188360cec7cb62fc4100959245a42cfe01834efedc5e9d068376c2 - languageName: node - linkType: hard - "mkdirp@npm:^1.0.3, mkdirp@npm:^1.0.4": version: 1.0.4 resolution: "mkdirp@npm:1.0.4" @@ -19231,6 +20732,13 @@ __metadata: languageName: node linkType: hard +"multitars@npm:^0.2.3": + version: 0.2.4 + resolution: "multitars@npm:0.2.4" + checksum: 10/20a9f234e8789bd9456f2133fd770642708c016428e8953e9f5ea62e1c8fa00b505e6d8ff1d7b9d8e44bf93163da6ec239e1b30bbab065a2100f61e72b8313b5 + languageName: node + linkType: hard + "murmurhash-js@npm:^1.0.0": version: 1.0.0 resolution: "murmurhash-js@npm:1.0.0" @@ -19302,6 +20810,13 @@ __metadata: languageName: node linkType: hard +"negotiator@npm:^1.0.0": + version: 1.0.0 + resolution: "negotiator@npm:1.0.0" + checksum: 10/b5734e87295324fabf868e36fb97c84b7d7f3156ec5f4ee5bf6e488079c11054f818290fc33804cef7b1ee21f55eeb14caea83e7dafae6492a409b3e573153e5 + languageName: node + linkType: hard + "neo-async@npm:^2.5.0, neo-async@npm:^2.6.0, neo-async@npm:^2.6.1": version: 2.6.2 resolution: "neo-async@npm:2.6.2" @@ -19441,8 +20956,8 @@ __metadata: linkType: hard "node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.12, node-fetch@npm:^2.6.7": - version: 2.6.13 - resolution: "node-fetch@npm:2.6.13" + version: 2.7.0 + resolution: "node-fetch@npm:2.7.0" dependencies: whatwg-url: "npm:^5.0.0" peerDependencies: @@ -19450,14 +20965,14 @@ __metadata: peerDependenciesMeta: encoding: optional: true - checksum: 10/72f94498d547e322207575b2778e7b3d969b0d73f35a19f258c7fb982bf0ae96e5a3a518477300ba1dd2bd22e6b05be074648ed88448c5cf1a9b7d23c6529d1a + checksum: 10/b24f8a3dc937f388192e59bcf9d0857d7b6940a2496f328381641cb616efccc9866e89ec43f2ec956bbd6c3d3ee05524ce77fe7b29ccd34692b3a16f237d6676 languageName: node linkType: hard -"node-forge@npm:^1.2.1, node-forge@npm:^1.3.1": - version: 1.3.1 - resolution: "node-forge@npm:1.3.1" - checksum: 10/05bab6868633bf9ad4c3b1dd50ec501c22ffd69f556cdf169a00998ca1d03e8107a6032ba013852f202035372021b845603aeccd7dfcb58cdb7430013b3daa8d +"node-forge@npm:^1.2.1, node-forge@npm:^1.3.1, node-forge@npm:^1.3.3": + version: 1.4.0 + resolution: "node-forge@npm:1.4.0" + checksum: 10/d70fd769768e646eda73343d4d4105ccb6869315d975905a22117431c04ae5b6df6c488e34ed275b1a66b50195a09b84b5c8aeca3b8605c20605fcb8e9f109d9 languageName: node linkType: hard @@ -19495,10 +21010,10 @@ __metadata: languageName: node linkType: hard -"node-releases@npm:^2.0.21": - version: 2.0.21 - resolution: "node-releases@npm:2.0.21" - checksum: 10/5344d634b39d20f47c0d85a1c64567fdb9cf46f7b27ed3d141f752642faab47dae326835c2109636f823758afb16ffbed7b0c0fe6f800ef91cec9f2beb4f2b4a +"node-releases@npm:^2.0.36": + version: 2.0.36 + resolution: "node-releases@npm:2.0.36" + checksum: 10/b31ead96e328b1775f07cad80c17b0601d0ee2894650b737e7ab5cbeb14e284e82dbc37ef38f1d915fa46dd7909781bd933d19b79cfe31b352573fac6da377aa languageName: node linkType: hard @@ -19711,21 +21226,21 @@ __metadata: languageName: node linkType: hard -"ob1@npm:0.83.2": - version: 0.83.2 - resolution: "ob1@npm:0.83.2" +"ob1@npm:0.83.3": + version: 0.83.3 + resolution: "ob1@npm:0.83.3" dependencies: flow-enums-runtime: "npm:^0.0.6" - checksum: 10/8eb482589b66cf46600d1231c2ea50a365f47ee5db0274795d1d3f5c43112e255b931a41ce1ef8a220f31b4fb985fb269c6a54bf7e9719f90dac3f4001a89a6c + checksum: 10/20dfe91d48d0cadd97159cfd53f5abdca435b55d58b1f562e0687485e8f44f8a95e8ab3c835badd13d0d8c01e3d7b14d639a316aa4bf82841ac78b49611d4e5c languageName: node linkType: hard -"ob1@npm:0.83.3": - version: 0.83.3 - resolution: "ob1@npm:0.83.3" +"ob1@npm:0.83.5": + version: 0.83.5 + resolution: "ob1@npm:0.83.5" dependencies: flow-enums-runtime: "npm:^0.0.6" - checksum: 10/20dfe91d48d0cadd97159cfd53f5abdca435b55d58b1f562e0687485e8f44f8a95e8ab3c835badd13d0d8c01e3d7b14d639a316aa4bf82841ac78b49611d4e5c + checksum: 10/7a3ed43344d3d10c76060218fc35c652d12e20c0e520cf4bdb3c86c2817f0622b78a3d8c81fd52a05c29d7d2113b65514ee721e61adb352dd547d14a74b6015a languageName: node linkType: hard @@ -19842,7 +21357,7 @@ __metadata: languageName: node linkType: hard -"on-finished@npm:2.4.1": +"on-finished@npm:2.4.1, on-finished@npm:~2.4.1": version: 2.4.1 resolution: "on-finished@npm:2.4.1" dependencies: @@ -20096,6 +21611,13 @@ __metadata: languageName: node linkType: hard +"p-map@npm:^7.0.2": + version: 7.0.4 + resolution: "p-map@npm:7.0.4" + checksum: 10/ef48c3b2e488f31c693c9fcc0df0ef76518cf6426a495cf9486ebbb0fd7f31aef7f90e96f72e0070c0ff6e3177c9318f644b512e2c29e3feee8d7153fcb6782e + languageName: node + linkType: hard + "p-try@npm:^2.0.0": version: 2.2.0 resolution: "p-try@npm:2.2.0" @@ -20130,9 +21652,9 @@ __metadata: linkType: hard "package-json-from-dist@npm:^1.0.0": - version: 1.0.0 - resolution: "package-json-from-dist@npm:1.0.0" - checksum: 10/ac706ec856a5a03f5261e4e48fa974f24feb044d51f84f8332e2af0af04fbdbdd5bbbfb9cbbe354190409bc8307c83a9e38c6672c3c8855f709afb0006a009ea + version: 1.0.1 + resolution: "package-json-from-dist@npm:1.0.1" + checksum: 10/58ee9538f2f762988433da00e26acc788036914d57c71c246bf0be1b60cdbd77dd60b6a3e1a30465f0b248aeb80079e0b34cb6050b1dfa18c06953bb1cbc7602 languageName: node linkType: hard @@ -20220,16 +21742,6 @@ __metadata: languageName: node linkType: hard -"password-prompt@npm:^1.0.4": - version: 1.1.3 - resolution: "password-prompt@npm:1.1.3" - dependencies: - ansi-escapes: "npm:^4.3.2" - cross-spawn: "npm:^7.0.3" - checksum: 10/1cf7001e66868b2ed7a03e036bc2f1dd45eb6dc8fee7e3e2056370057c484be25e7468fee00a1378e1ee8eca77ba79f48bee5ce15dcb464413987ace63c68b35 - languageName: node - linkType: hard - "path-exists@npm:^3.0.0": version: 3.0.0 resolution: "path-exists@npm:3.0.0" @@ -20282,13 +21794,13 @@ __metadata: languageName: node linkType: hard -"path-scurry@npm:^2.0.0": - version: 2.0.0 - resolution: "path-scurry@npm:2.0.0" +"path-scurry@npm:^2.0.0, path-scurry@npm:^2.0.2": + version: 2.0.2 + resolution: "path-scurry@npm:2.0.2" dependencies: lru-cache: "npm:^11.0.0" minipass: "npm:^7.1.2" - checksum: 10/285ae0c2d6c34ae91dc1d5378ede21981c9a2f6de1ea9ca5a88b5a270ce9763b83dbadc7a324d512211d8d36b0c540427d3d0817030849d97a60fa840a2c59ec + checksum: 10/2b4257422bcb870a4c2d205b3acdbb213a72f5e2250f61c80f79c9d014d010f82bdf8584441612c8e1fa4eb098678f5704a66fa8377d72646bad4be38e57a2c3 languageName: node linkType: hard @@ -20306,6 +21818,13 @@ __metadata: languageName: node linkType: hard +"path-type@npm:^6.0.0": + version: 6.0.0 + resolution: "path-type@npm:6.0.0" + checksum: 10/b9f6eaf7795c48d5c9bc4c6bc3ac61315b8d36975a73497ab2e02b764c0836b71fb267ea541863153f633a069a1c2ed3c247cb781633842fc571c655ac57c00e + languageName: node + linkType: hard + "path@npm:0.12.7": version: 0.12.7 resolution: "path@npm:0.12.7" @@ -20673,6 +22192,13 @@ __metadata: languageName: node linkType: hard +"presentable-error@npm:^0.0.1": + version: 0.0.1 + resolution: "presentable-error@npm:0.0.1" + checksum: 10/013809ee7a47ced847a8d860e9b89a56cdd8c4f1ad04ad8da1e58fd60843f77f497d204146bb15aaa9793d3b94ad8626eed01256fc9eb5839a545af2000a5fa4 + languageName: node + linkType: hard + "prettier-linter-helpers@npm:^1.0.0": version: 1.0.0 resolution: "prettier-linter-helpers@npm:1.0.0" @@ -20698,7 +22224,7 @@ __metadata: languageName: node linkType: hard -"pretty-format@npm:30.2.0, pretty-format@npm:^30.0.5": +"pretty-format@npm:30.2.0": version: 30.2.0 resolution: "pretty-format@npm:30.2.0" dependencies: @@ -20709,6 +22235,17 @@ __metadata: languageName: node linkType: hard +"pretty-format@npm:30.3.0, pretty-format@npm:^30.0.5": + version: 30.3.0 + resolution: "pretty-format@npm:30.3.0" + dependencies: + "@jest/schemas": "npm:30.0.5" + ansi-styles: "npm:^5.2.0" + react-is: "npm:^18.3.1" + checksum: 10/b288db630841f2464554c5cfa7d7faf519ad7b5c05c3818e764c7cb486bcf59f240ea5576c748f8ca6625623c5856a8906642255bbe89d6cfa1a9090b0fbc6b9 + languageName: node + linkType: hard + "pretty-format@npm:^29.0.0, pretty-format@npm:^29.0.3, pretty-format@npm:^29.7.0": version: 29.7.0 resolution: "pretty-format@npm:29.7.0" @@ -20868,6 +22405,13 @@ __metadata: languageName: node linkType: hard +"proxy-from-env@npm:^2.1.0": + version: 2.1.0 + resolution: "proxy-from-env@npm:2.1.0" + checksum: 10/fbbaf4dab2a6231dc9e394903a5f66f20475e36b734335790b46feb9da07c37d6b32e2c02e3e2ea4d4b23774c53d8562e5b7cc73282cb43f4a597b7eacaee2ee + languageName: node + linkType: hard + "psl@npm:^1.1.33": version: 1.15.0 resolution: "psl@npm:1.15.0" @@ -21046,6 +22590,17 @@ __metadata: languageName: node linkType: hard +"react-dom@npm:19.2.0": + version: 19.2.0 + resolution: "react-dom@npm:19.2.0" + dependencies: + scheduler: "npm:^0.27.0" + peerDependencies: + react: ^19.2.0 + checksum: 10/3dbba071b9b1e7a19eae55f05c100f6b44f88c0aee72397d719ae338248ca66ed5028e6964c1c14870cc3e1abcecc91b22baba6dc2072f819dea81a9fd72f2fd + languageName: node + linkType: hard + "react-dropzone@npm:^14.2.3": version: 14.3.8 resolution: "react-dropzone@npm:14.3.8" @@ -21172,6 +22727,38 @@ __metadata: languageName: node linkType: hard +"react-native-builder-bob@npm:^0.40.15": + version: 0.40.18 + resolution: "react-native-builder-bob@npm:0.40.18" + dependencies: + "@babel/core": "npm:^7.25.2" + "@babel/plugin-transform-flow-strip-types": "npm:^7.26.5" + "@babel/plugin-transform-strict-mode": "npm:^7.24.7" + "@babel/preset-env": "npm:^7.25.2" + "@babel/preset-react": "npm:^7.24.7" + "@babel/preset-typescript": "npm:^7.24.7" + arktype: "npm:^2.1.15" + babel-plugin-syntax-hermes-parser: "npm:^0.28.0" + browserslist: "npm:^4.20.4" + cross-spawn: "npm:^7.0.3" + dedent: "npm:^0.7.0" + del: "npm:^6.1.1" + escape-string-regexp: "npm:^4.0.0" + fs-extra: "npm:^10.1.0" + glob: "npm:^10.5.0" + is-git-dirty: "npm:^2.0.1" + json5: "npm:^2.2.1" + kleur: "npm:^4.1.4" + prompts: "npm:^2.4.2" + react-native-monorepo-config: "npm:^0.3.3" + which: "npm:^2.0.2" + yargs: "npm:^17.5.1" + bin: + bob: bin/bob + checksum: 10/06eddba046a508dff0aa322c823fa57a280e2ed6a6f586de2114ebc11b6bb7863714eaaff8e93d681bb851fb4f9e792b102541211847f0e546f4e9546df6c3de + languageName: node + linkType: hard + "react-native-builder-bob@npm:~0.23": version: 0.23.2 resolution: "react-native-builder-bob@npm:0.23.2" @@ -21201,15 +22788,6 @@ __metadata: languageName: node linkType: hard -"react-native-callkeep@npm:^4.3.16": - version: 4.3.16 - resolution: "react-native-callkeep@npm:4.3.16" - peerDependencies: - react-native: ">=0.40.0" - checksum: 10/db1ece74c2272b72625fc6240464452aca6d8541a3682e051f72de1f1c52ee5b8295b247e30fe9a325f9ecc60d728b18ae4b46e6ac590151b4b33c351b67f4e7 - languageName: node - linkType: hard - "react-native-device-info@npm:^14.1.1": version: 14.1.1 resolution: "react-native-device-info@npm:14.1.1" @@ -21230,9 +22808,9 @@ __metadata: languageName: node linkType: hard -"react-native-gesture-handler@npm:^2.28.0": - version: 2.28.0 - resolution: "react-native-gesture-handler@npm:2.28.0" +"react-native-gesture-handler@npm:^2.28.0, react-native-gesture-handler@npm:~2.30.0": + version: 2.30.1 + resolution: "react-native-gesture-handler@npm:2.30.1" dependencies: "@egjs/hammerjs": "npm:^2.0.17" hoist-non-react-statics: "npm:^3.3.0" @@ -21240,7 +22818,7 @@ __metadata: peerDependencies: react: "*" react-native: "*" - checksum: 10/856a9cb50b467e5e21cdd50930be68fee20f1c8ea13caa3cabb0bebd1345d0a847cd7b761a39b2d42b986b9d8e82e9419ccaf481b17373233c7ece7fed08dc70 + checksum: 10/5ee536ff45623381c576025852d48d42e082589b46095606652f17b74b86216ebcea26fbdea5af7f4f0a257f1500ad63cb9348c51a54bf0e45d8ce472919c43e languageName: node linkType: hard @@ -21263,7 +22841,7 @@ __metadata: languageName: node linkType: hard -"react-native-is-edge-to-edge@npm:1.2.1, react-native-is-edge-to-edge@npm:^1.1.6, react-native-is-edge-to-edge@npm:^1.2.1": +"react-native-is-edge-to-edge@npm:1.2.1": version: 1.2.1 resolution: "react-native-is-edge-to-edge@npm:1.2.1" peerDependencies: @@ -21273,6 +22851,16 @@ __metadata: languageName: node linkType: hard +"react-native-is-edge-to-edge@npm:^1.1.6, react-native-is-edge-to-edge@npm:^1.2.1": + version: 1.3.1 + resolution: "react-native-is-edge-to-edge@npm:1.3.1" + peerDependencies: + react: "*" + react-native: "*" + checksum: 10/dc82d54e0bf8f89208a538bb0d14e4891af6efae27ed5b7b21be683a72c38c5219ab9be1ea9bd40aa1c905d481174e649d0b71aeceaa9946e6c707f251568282 + languageName: node + linkType: hard + "react-native-lightbox@npm:^0.7.0": version: 0.7.0 resolution: "react-native-lightbox@npm:0.7.0" @@ -21303,6 +22891,16 @@ __metadata: languageName: node linkType: hard +"react-native-monorepo-config@npm:^0.3.3": + version: 0.3.3 + resolution: "react-native-monorepo-config@npm:0.3.3" + dependencies: + escape-string-regexp: "npm:^5.0.0" + fast-glob: "npm:^3.3.3" + checksum: 10/d301020b38f80010bce38108a9e1b72deee3eb37f1ba5e2f0471dc0737584b8d25158a2e649c38ddbe890b653c29a69ef82d73c522473cfdb2396239ee84fcd8 + languageName: node + linkType: hard + "react-native-permissions@npm:^5.4.2": version: 5.4.2 resolution: "react-native-permissions@npm:5.4.2" @@ -21317,6 +22915,20 @@ __metadata: languageName: node linkType: hard +"react-native-reanimated@npm:4.2.1": + version: 4.2.1 + resolution: "react-native-reanimated@npm:4.2.1" + dependencies: + react-native-is-edge-to-edge: "npm:1.2.1" + semver: "npm:7.7.3" + peerDependencies: + react: "*" + react-native: "*" + react-native-worklets: ">=0.7.0" + checksum: 10/869eb4c90b5464ac616bf048f77a309fd2c793020a662b80b874022f90256b7c955f43d7e7c4751c15f2f767c306e52c624cc37f3edff4fe0bbf26c2c3f955b7 + languageName: node + linkType: hard + "react-native-reanimated@npm:~4.1.2": version: 4.1.2 resolution: "react-native-reanimated@npm:4.1.2" @@ -21333,30 +22945,43 @@ __metadata: linkType: hard "react-native-reanimated@npm:~4.2.1": - version: 4.2.1 - resolution: "react-native-reanimated@npm:4.2.1" + version: 4.2.3 + resolution: "react-native-reanimated@npm:4.2.3" dependencies: - react-native-is-edge-to-edge: "npm:1.2.1" - semver: "npm:7.7.3" + react-native-is-edge-to-edge: "npm:^1.2.1" + semver: "npm:^7.7.3" + peerDependencies: + react: "*" + react-native: 0.80 - 0.84 + react-native-worklets: 0.7 - 0.8 + checksum: 10/fe47e39385895b200541649bfb85175b215c08bd4f9194736896768b1e445c2153a49921aaec7db9e2e3d694d263e33cac8ff25ff650a2b2d4d9cbd56c70cabd + languageName: node + linkType: hard + +"react-native-safe-area-context@npm:^5.6.1, react-native-safe-area-context@npm:~5.6.1, react-native-safe-area-context@npm:~5.6.2": + version: 5.6.2 + resolution: "react-native-safe-area-context@npm:5.6.2" peerDependencies: react: "*" react-native: "*" - react-native-worklets: ">=0.7.0" - checksum: 10/869eb4c90b5464ac616bf048f77a309fd2c793020a662b80b874022f90256b7c955f43d7e7c4751c15f2f767c306e52c624cc37f3edff4fe0bbf26c2c3f955b7 + checksum: 10/880d87ee60119321b366eef2c151ecefe14f5bc0d39cf5cfbfb167684e571d3dae2600ee19b9bc8521f5726eb285abecaa7aafb1a3b213529dafbac24703d302 languageName: node linkType: hard -"react-native-safe-area-context@npm:^5.6.1, react-native-safe-area-context@npm:~5.6.1": - version: 5.6.1 - resolution: "react-native-safe-area-context@npm:5.6.1" +"react-native-screens@npm:^4.16.0, react-native-screens@npm:~4.23.0": + version: 4.23.0 + resolution: "react-native-screens@npm:4.23.0" + dependencies: + react-freeze: "npm:^1.0.0" + warn-once: "npm:^0.1.0" peerDependencies: react: "*" react-native: "*" - checksum: 10/2fc93cf46a6cbad28e5850bef009905c6db44066fb7e6f7bbce52c2ae4b0467c6718e4f572a42f8387c6b37f6d61ebe79980d0c2b5899e23dc19482a7db8417b + checksum: 10/cb8cc1c18c8d340f53a34a15e84ad6a3bd0ee43384d712a9e4c2a8257428c129c9bae0900ab86f64a4ebdc27684e6b12be9064a410e8f54c7a649534f12a9d76 languageName: node linkType: hard -"react-native-screens@npm:^4.16.0, react-native-screens@npm:~4.16.0": +"react-native-screens@npm:~4.16.0": version: 4.16.0 resolution: "react-native-screens@npm:4.16.0" dependencies: @@ -21370,9 +22995,9 @@ __metadata: languageName: node linkType: hard -"react-native-svg@npm:^15.14.0": - version: 15.14.0 - resolution: "react-native-svg@npm:15.14.0" +"react-native-svg@npm:15.15.3, react-native-svg@npm:^15.14.0": + version: 15.15.3 + resolution: "react-native-svg@npm:15.15.3" dependencies: css-select: "npm:^5.1.0" css-tree: "npm:^1.1.3" @@ -21380,7 +23005,7 @@ __metadata: peerDependencies: react: "*" react-native: "*" - checksum: 10/8f067e265cd2749a7f0a3a09eccd6a297f1e99d0306e2c630d354e7093c3cb8e47cd054988f832b05ed80509ebdda7422cb407d8dcf851f1d4e8706f14f38c21 + checksum: 10/32254d53ac6d43af1e38011e899ae23ee8a272f1bd8e24fb34f355326cace369cd260331e58a53af3aec67ec8ec40ce6a60e57655259ebd0c32fb156649a4a23 languageName: node linkType: hard @@ -21446,15 +23071,6 @@ __metadata: languageName: node linkType: hard -"react-native-voip-push-notification@npm:3.3.3, react-native-voip-push-notification@npm:^3.3.3, react-native-voip-push-notification@npm:~3.3.3": - version: 3.3.3 - resolution: "react-native-voip-push-notification@npm:3.3.3" - peerDependencies: - react-native: ">=0.60.0" - checksum: 10/5b0791bc34be2a2bf8cc10558ffd68e9eac9cee4804eeeb78e8cc29fe7d6188794f9b3a62f0474b4706c4a10ddd0620de4723d53bdbe1efc256728b15a5ab1d7 - languageName: node - linkType: hard - "react-native-web@npm:^0.21.1": version: 0.21.1 resolution: "react-native-web@npm:0.21.1" @@ -21487,6 +23103,29 @@ __metadata: languageName: node linkType: hard +"react-native-worklets@npm:0.7.2": + version: 0.7.2 + resolution: "react-native-worklets@npm:0.7.2" + dependencies: + "@babel/plugin-transform-arrow-functions": "npm:7.27.1" + "@babel/plugin-transform-class-properties": "npm:7.27.1" + "@babel/plugin-transform-classes": "npm:7.28.4" + "@babel/plugin-transform-nullish-coalescing-operator": "npm:7.27.1" + "@babel/plugin-transform-optional-chaining": "npm:7.27.1" + "@babel/plugin-transform-shorthand-properties": "npm:7.27.1" + "@babel/plugin-transform-template-literals": "npm:7.27.1" + "@babel/plugin-transform-unicode-regex": "npm:7.27.1" + "@babel/preset-typescript": "npm:7.27.1" + convert-source-map: "npm:2.0.0" + semver: "npm:7.7.3" + peerDependencies: + "@babel/core": "*" + react: "*" + react-native: "*" + checksum: 10/4bc9b71a3a63f589da6f9bc7d889218f4a0e8d80737fb79e9e78298924774a99db254089428a877b0f1e434acdd00099a86a2424f33bf908283eb1b60adaea0e + languageName: node + linkType: hard + "react-native-worklets@npm:^0.5.0": version: 0.5.1 resolution: "react-native-worklets@npm:0.5.1" @@ -21533,104 +23172,104 @@ __metadata: languageName: node linkType: hard -"react-native@npm:^0.81.5": - version: 0.81.5 - resolution: "react-native@npm:0.81.5" +"react-native@npm:0.83.4, react-native@npm:^0.83.2": + version: 0.83.4 + resolution: "react-native@npm:0.83.4" dependencies: "@jest/create-cache-key-function": "npm:^29.7.0" - "@react-native/assets-registry": "npm:0.81.5" - "@react-native/codegen": "npm:0.81.5" - "@react-native/community-cli-plugin": "npm:0.81.5" - "@react-native/gradle-plugin": "npm:0.81.5" - "@react-native/js-polyfills": "npm:0.81.5" - "@react-native/normalize-colors": "npm:0.81.5" - "@react-native/virtualized-lists": "npm:0.81.5" + "@react-native/assets-registry": "npm:0.83.4" + "@react-native/codegen": "npm:0.83.4" + "@react-native/community-cli-plugin": "npm:0.83.4" + "@react-native/gradle-plugin": "npm:0.83.4" + "@react-native/js-polyfills": "npm:0.83.4" + "@react-native/normalize-colors": "npm:0.83.4" + "@react-native/virtualized-lists": "npm:0.83.4" abort-controller: "npm:^3.0.0" anser: "npm:^1.4.9" ansi-regex: "npm:^5.0.0" babel-jest: "npm:^29.7.0" - babel-plugin-syntax-hermes-parser: "npm:0.29.1" + babel-plugin-syntax-hermes-parser: "npm:0.32.0" base64-js: "npm:^1.5.1" commander: "npm:^12.0.0" flow-enums-runtime: "npm:^0.0.6" glob: "npm:^7.1.1" + hermes-compiler: "npm:0.14.1" invariant: "npm:^2.2.4" jest-environment-node: "npm:^29.7.0" memoize-one: "npm:^5.0.0" - metro-runtime: "npm:^0.83.1" - metro-source-map: "npm:^0.83.1" + metro-runtime: "npm:^0.83.3" + metro-source-map: "npm:^0.83.3" nullthrows: "npm:^1.1.1" pretty-format: "npm:^29.7.0" promise: "npm:^8.3.0" react-devtools-core: "npm:^6.1.5" react-refresh: "npm:^0.14.0" regenerator-runtime: "npm:^0.13.2" - scheduler: "npm:0.26.0" + scheduler: "npm:0.27.0" semver: "npm:^7.1.3" stacktrace-parser: "npm:^0.1.10" whatwg-fetch: "npm:^3.0.0" - ws: "npm:^6.2.3" + ws: "npm:^7.5.10" yargs: "npm:^17.6.2" peerDependencies: - "@types/react": ^19.1.0 - react: ^19.1.0 + "@types/react": ^19.1.1 + react: ^19.2.0 peerDependenciesMeta: "@types/react": optional: true bin: react-native: cli.js - checksum: 10/ee472f6cb3a86d9e154e3ac43830424403c8b5d23c6f613f0ac39953b7123352bdb6056270078d98ebbf77ac26fb00d86e50a9c4521a2db93072281937a8d9b0 + checksum: 10/ef79e818bccc17dffac6810270902cd2d6bce8e6ccec3d6c6b71ff3f6da1e32f66ea0f296624c295d5febd4396d3d38ccf923daefb0c943fef3a8aa3c4f554c3 languageName: node linkType: hard -"react-native@npm:^0.83.2": - version: 0.83.2 - resolution: "react-native@npm:0.83.2" +"react-native@npm:^0.81.5": + version: 0.81.5 + resolution: "react-native@npm:0.81.5" dependencies: "@jest/create-cache-key-function": "npm:^29.7.0" - "@react-native/assets-registry": "npm:0.83.2" - "@react-native/codegen": "npm:0.83.2" - "@react-native/community-cli-plugin": "npm:0.83.2" - "@react-native/gradle-plugin": "npm:0.83.2" - "@react-native/js-polyfills": "npm:0.83.2" - "@react-native/normalize-colors": "npm:0.83.2" - "@react-native/virtualized-lists": "npm:0.83.2" + "@react-native/assets-registry": "npm:0.81.5" + "@react-native/codegen": "npm:0.81.5" + "@react-native/community-cli-plugin": "npm:0.81.5" + "@react-native/gradle-plugin": "npm:0.81.5" + "@react-native/js-polyfills": "npm:0.81.5" + "@react-native/normalize-colors": "npm:0.81.5" + "@react-native/virtualized-lists": "npm:0.81.5" abort-controller: "npm:^3.0.0" anser: "npm:^1.4.9" ansi-regex: "npm:^5.0.0" babel-jest: "npm:^29.7.0" - babel-plugin-syntax-hermes-parser: "npm:0.32.0" + babel-plugin-syntax-hermes-parser: "npm:0.29.1" base64-js: "npm:^1.5.1" commander: "npm:^12.0.0" flow-enums-runtime: "npm:^0.0.6" glob: "npm:^7.1.1" - hermes-compiler: "npm:0.14.1" invariant: "npm:^2.2.4" jest-environment-node: "npm:^29.7.0" memoize-one: "npm:^5.0.0" - metro-runtime: "npm:^0.83.3" - metro-source-map: "npm:^0.83.3" + metro-runtime: "npm:^0.83.1" + metro-source-map: "npm:^0.83.1" nullthrows: "npm:^1.1.1" pretty-format: "npm:^29.7.0" promise: "npm:^8.3.0" react-devtools-core: "npm:^6.1.5" react-refresh: "npm:^0.14.0" regenerator-runtime: "npm:^0.13.2" - scheduler: "npm:0.27.0" + scheduler: "npm:0.26.0" semver: "npm:^7.1.3" stacktrace-parser: "npm:^0.1.10" whatwg-fetch: "npm:^3.0.0" - ws: "npm:^7.5.10" + ws: "npm:^6.2.3" yargs: "npm:^17.6.2" peerDependencies: - "@types/react": ^19.1.1 - react: ^19.2.0 + "@types/react": ^19.1.0 + react: ^19.1.0 peerDependenciesMeta: "@types/react": optional: true bin: react-native: cli.js - checksum: 10/415d10079de4b21608b303809c938154fcd55731204827c34334d331acd8edb0162b72ee146ee61202ecdd2a0c6a5d4e9b35dc82e39a22cd8ff870a2f6b4ac1c + checksum: 10/ee472f6cb3a86d9e154e3ac43830424403c8b5d23c6f613f0ac39953b7123352bdb6056270078d98ebbf77ac26fb00d86e50a9c4521a2db93072281937a8d9b0 languageName: node linkType: hard @@ -21950,12 +23589,12 @@ __metadata: languageName: node linkType: hard -"regenerate-unicode-properties@npm:^10.2.0": - version: 10.2.0 - resolution: "regenerate-unicode-properties@npm:10.2.0" +"regenerate-unicode-properties@npm:^10.2.2": + version: 10.2.2 + resolution: "regenerate-unicode-properties@npm:10.2.2" dependencies: regenerate: "npm:^1.4.2" - checksum: 10/9150eae6fe04a8c4f2ff06077396a86a98e224c8afad8344b1b656448e89e84edcd527e4b03aa5476774129eb6ad328ed684f9c1459794a935ec0cc17ce14329 + checksum: 10/5041ee31185c4700de9dd76783fab9def51c412751190d523d621db5b8e35a6c2d91f1642c12247e7d94f84b8ae388d044baac1e88fc2ba0ac215ca8dc7bed38 languageName: node linkType: hard @@ -21994,17 +23633,17 @@ __metadata: languageName: node linkType: hard -"regexpu-core@npm:^6.2.0": - version: 6.2.0 - resolution: "regexpu-core@npm:6.2.0" +"regexpu-core@npm:^6.3.1": + version: 6.4.0 + resolution: "regexpu-core@npm:6.4.0" dependencies: regenerate: "npm:^1.4.2" - regenerate-unicode-properties: "npm:^10.2.0" + regenerate-unicode-properties: "npm:^10.2.2" regjsgen: "npm:^0.8.0" - regjsparser: "npm:^0.12.0" + regjsparser: "npm:^0.13.0" unicode-match-property-ecmascript: "npm:^2.0.0" - unicode-match-property-value-ecmascript: "npm:^2.1.0" - checksum: 10/4d054ffcd98ca4f6ca7bf0df6598ed5e4a124264602553308add41d4fa714a0c5bcfb5bc868ac91f7060a9c09889cc21d3180a3a14c5f9c5838442806129ced3 + unicode-match-property-value-ecmascript: "npm:^2.2.1" + checksum: 10/bf5f85a502a17f127a1f922270e2ecc1f0dd071ff76a3ec9afcd6b1c2bf7eae1486d1e3b1a6d621aee8960c8b15139e6b5058a84a68e518e1a92b52e9322faf9 languageName: node linkType: hard @@ -22015,14 +23654,14 @@ __metadata: languageName: node linkType: hard -"regjsparser@npm:^0.12.0": - version: 0.12.0 - resolution: "regjsparser@npm:0.12.0" +"regjsparser@npm:^0.13.0": + version: 0.13.0 + resolution: "regjsparser@npm:0.13.0" dependencies: - jsesc: "npm:~3.0.2" + jsesc: "npm:~3.1.0" bin: regjsparser: bin/parser - checksum: 10/c2d6506b3308679de5223a8916984198e0493649a67b477c66bdb875357e3785abbf3bedf7c5c2cf8967d3b3a7bdf08b7cbd39e65a70f9e1ffad584aecf5f06a + checksum: 10/eeaabd3454f59394cbb3bfeb15fd789e638040f37d0bee9071a9b0b85524ddc52b5f7aaaaa4847304c36fa37429e53d109c4dbf6b878cb5ffa4f4198c1042fb7 languageName: node linkType: hard @@ -22162,15 +23801,6 @@ __metadata: languageName: node linkType: hard -"resolve-global@npm:^1.0.0": - version: 1.0.0 - resolution: "resolve-global@npm:1.0.0" - dependencies: - global-dirs: "npm:^0.1.1" - checksum: 10/c4e11d33e84bde7516b824503ffbe4b6cce863d5ce485680fd3db997b7c64da1df98321b1fd0703b58be8bc9bc83bc96bd83043f96194386b45eb47229efb6b6 - languageName: node - linkType: hard - "resolve-pkg-maps@npm:^1.0.0": version: 1.0.0 resolution: "resolve-pkg-maps@npm:1.0.0" @@ -22214,16 +23844,16 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.1.7, resolve@npm:^1.10.1, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.22.1, resolve@npm:^1.22.10, resolve@npm:^1.22.2, resolve@npm:^1.22.4, resolve@npm:^1.22.8": - version: 1.22.10 - resolution: "resolve@npm:1.22.10" +"resolve@npm:^1.1.7, resolve@npm:^1.10.1, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.22.1, resolve@npm:^1.22.11, resolve@npm:^1.22.2, resolve@npm:^1.22.4, resolve@npm:^1.22.8": + version: 1.22.11 + resolution: "resolve@npm:1.22.11" dependencies: - is-core-module: "npm:^2.16.0" + is-core-module: "npm:^2.16.1" path-parse: "npm:^1.0.7" supports-preserve-symlinks-flag: "npm:^1.0.0" bin: resolve: bin/resolve - checksum: 10/0a398b44da5c05e6e421d70108822c327675febb880eebe905587628de401854c61d5df02866ff34fc4cb1173a51c9f0e84a94702738df3611a62e2acdc68181 + checksum: 10/e1b2e738884a08de03f97ee71494335eba8c2b0feb1de9ae065e82c48997f349f77a2b10e8817e147cf610bfabc4b1cb7891ee8eaf5bf80d4ad514a34c4fab0a languageName: node linkType: hard @@ -22262,16 +23892,16 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@npm%3A^1.1.7#optional!builtin, resolve@patch:resolve@npm%3A^1.10.1#optional!builtin, resolve@patch:resolve@npm%3A^1.19.0#optional!builtin, resolve@patch:resolve@npm%3A^1.20.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.1#optional!builtin, resolve@patch:resolve@npm%3A^1.22.10#optional!builtin, resolve@patch:resolve@npm%3A^1.22.2#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin, resolve@patch:resolve@npm%3A^1.22.8#optional!builtin": - version: 1.22.10 - resolution: "resolve@patch:resolve@npm%3A1.22.10#optional!builtin::version=1.22.10&hash=c3c19d" +"resolve@patch:resolve@npm%3A^1.1.7#optional!builtin, resolve@patch:resolve@npm%3A^1.10.1#optional!builtin, resolve@patch:resolve@npm%3A^1.19.0#optional!builtin, resolve@patch:resolve@npm%3A^1.20.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.1#optional!builtin, resolve@patch:resolve@npm%3A^1.22.11#optional!builtin, resolve@patch:resolve@npm%3A^1.22.2#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin, resolve@patch:resolve@npm%3A^1.22.8#optional!builtin": + version: 1.22.11 + resolution: "resolve@patch:resolve@npm%3A1.22.11#optional!builtin::version=1.22.11&hash=c3c19d" dependencies: - is-core-module: "npm:^2.16.0" + is-core-module: "npm:^2.16.1" path-parse: "npm:^1.0.7" supports-preserve-symlinks-flag: "npm:^1.0.0" bin: resolve: bin/resolve - checksum: 10/d4d878bfe3702d215ea23e75e0e9caf99468e3db76f5ca100d27ebdc527366fee3877e54bce7d47cc72ca8952fc2782a070d238bfa79a550eeb0082384c3b81a + checksum: 10/fd342cad25e52cd6f4f3d1716e189717f2522bfd6641109fe7aa372f32b5714a296ed7c238ddbe7ebb0c1ddfe0b7f71c9984171024c97cf1b2073e3e40ff71a8 languageName: node linkType: hard @@ -22613,7 +24243,7 @@ __metadata: languageName: node linkType: hard -"scheduler@npm:0.27.0": +"scheduler@npm:0.27.0, scheduler@npm:^0.27.0": version: 0.27.0 resolution: "scheduler@npm:0.27.0" checksum: 10/eab3c3a8373195173e59c147224fc30dabe6dd453f248f5e610e8458512a5a2ee3a06465dc400ebfe6d35c9f5b7f3bb6b2e41c88c86fd177c25a73e7286a1e06 @@ -22645,7 +24275,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:7.7.3, semver@npm:7.x, semver@npm:^7.1.3, semver@npm:^7.3.5, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.3, semver@npm:^7.7.3": +"semver@npm:7.7.3": version: 7.7.3 resolution: "semver@npm:7.7.3" bin: @@ -22654,6 +24284,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:7.x, semver@npm:^7.1.3, semver@npm:^7.3.5, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.3, semver@npm:^7.7.3": + version: 7.7.4 + resolution: "semver@npm:7.7.4" + bin: + semver: bin/semver.js + checksum: 10/26bdc6d58b29528f4142d29afb8526bc335f4fc04c4a10f2b98b217f277a031c66736bf82d3d3bb354a2f6a3ae50f18fd62b053c4ac3f294a3d10a61f5075b75 + languageName: node + linkType: hard + "semver@npm:^5.3.0, semver@npm:^5.6.0": version: 5.7.2 resolution: "semver@npm:5.7.2" @@ -22703,8 +24342,8 @@ __metadata: linkType: hard "send@npm:^0.19.0": - version: 0.19.1 - resolution: "send@npm:0.19.1" + version: 0.19.2 + resolution: "send@npm:0.19.2" dependencies: debug: "npm:2.6.9" depd: "npm:2.0.0" @@ -22712,14 +24351,14 @@ __metadata: encodeurl: "npm:~2.0.0" escape-html: "npm:~1.0.3" etag: "npm:~1.8.1" - fresh: "npm:0.5.2" - http-errors: "npm:2.0.0" + fresh: "npm:~0.5.2" + http-errors: "npm:~2.0.1" mime: "npm:1.6.0" ms: "npm:2.1.3" - on-finished: "npm:2.4.1" + on-finished: "npm:~2.4.1" range-parser: "npm:~1.2.1" - statuses: "npm:2.0.1" - checksum: 10/360bf50a839c7bbc181f67c3a0f3424a7ad8016dfebcd9eb90891f4b762b4377da14414c32250d67b53872e884171c27469110626f6c22765caa7c38c207ee1d + statuses: "npm:~2.0.2" + checksum: 10/e932a592f62c58560b608a402d52333a8ae98a5ada076feb5db1d03adaa77c3ca32a7befa1c4fd6dedc186e88f342725b0cb4b3d86835eaf834688b259bef18d languageName: node linkType: hard @@ -22800,17 +24439,17 @@ __metadata: languageName: node linkType: hard -"setprototypeof@npm:1.2.0": +"setprototypeof@npm:1.2.0, setprototypeof@npm:~1.2.0": version: 1.2.0 resolution: "setprototypeof@npm:1.2.0" checksum: 10/fde1630422502fbbc19e6844346778f99d449986b2f9cdcceb8326730d2f3d9964dbcb03c02aaadaefffecd0f2c063315ebea8b3ad895914bf1afc1747fc172e languageName: node linkType: hard -"sf-symbols-typescript@npm:^2.0.0, sf-symbols-typescript@npm:^2.1.0": - version: 2.1.0 - resolution: "sf-symbols-typescript@npm:2.1.0" - checksum: 10/1fe80afa6a8275bf0665a5ed30fc82b25abd706162235cf77d9422c9cf707aee9bac0e7f3a3effd2177963751e8da9b76e43f1be814660b3a9c18de11274be39 +"sf-symbols-typescript@npm:^2.0.0, sf-symbols-typescript@npm:^2.1.0, sf-symbols-typescript@npm:^2.2.0": + version: 2.2.0 + resolution: "sf-symbols-typescript@npm:2.2.0" + checksum: 10/8623e148bf86151692d3ccb3149122c091b256162a39f4c4f78472811ffe92e3c77a83841a7381a5dc0bc6505ebb73e7ca8901cb077004872d7ee1cb4e5be9d4 languageName: node linkType: hard @@ -23056,7 +24695,7 @@ __metadata: languageName: node linkType: hard -"slash@npm:^5.0.0": +"slash@npm:^5.0.0, slash@npm:^5.1.0": version: 5.1.0 resolution: "slash@npm:5.1.0" checksum: 10/2c41ec6fb1414cd9bba0fa6b1dd00e8be739e3fe85d079c69d4b09ca5f2f86eafd18d9ce611c0c0f686428638a36c272a6ac14799146a8295f259c10cc45cde4 @@ -23303,11 +24942,11 @@ __metadata: linkType: hard "stacktrace-parser@npm:^0.1.10": - version: 0.1.10 - resolution: "stacktrace-parser@npm:0.1.10" + version: 0.1.11 + resolution: "stacktrace-parser@npm:0.1.11" dependencies: type-fest: "npm:^0.7.1" - checksum: 10/f4fbddfc09121d91e587b60de4beb4941108e967d71ad3a171812dc839b010ca374d064ad0a296295fed13acd103609d99a4224a25b4e67de13cae131f1901ee + checksum: 10/1120cf716606ec6a8e25cc9b6ada79d7b91e6a599bba1a6664e6badc8b5f37987d7df7d9ad0344f717a042781fd8e1e999de08614a5afea451b68902421036b5 languageName: node linkType: hard @@ -23325,6 +24964,13 @@ __metadata: languageName: node linkType: hard +"statuses@npm:~2.0.2": + version: 2.0.2 + resolution: "statuses@npm:2.0.2" + checksum: 10/6927feb50c2a75b2a4caab2c565491f7a93ad3d8dbad7b1398d52359e9243a20e2ebe35e33726dee945125ef7a515e9097d8a1b910ba2bbd818265a2f6c39879 + languageName: node + linkType: hard + "std-env@npm:^3.9.0": version: 3.9.0 resolution: "std-env@npm:3.9.0" @@ -23495,8 +25141,8 @@ __metadata: linkType: hard "stream-chat@npm:^9.33.0": - version: 9.33.0 - resolution: "stream-chat@npm:9.33.0" + version: 9.39.0 + resolution: "stream-chat@npm:9.39.0" dependencies: "@types/jsonwebtoken": "npm:^9.0.8" "@types/ws": "npm:^8.5.14" @@ -23507,7 +25153,7 @@ __metadata: jsonwebtoken: "npm:^9.0.3" linkifyjs: "npm:^4.3.2" ws: "npm:^8.18.1" - checksum: 10/51bc7c179651a55feda8beba8da12d72770a9b04283c084570328c6b3b3407be4bd6ede8c54d8aad38f904b85e8244755fe85592660d760c5b9974264b6d3cbf + checksum: 10/123bd6b3a898003feb39631160b88e5734c65d10dbef3a54f463ea2cfc9f90297e894871e6d88140d848ba1976f9d78eb501c637f2901e7a781e73769daeea55 languageName: node linkType: hard @@ -23704,11 +25350,11 @@ __metadata: linkType: hard "strip-ansi@npm:^7.0.1, strip-ansi@npm:^7.1.0": - version: 7.1.0 - resolution: "strip-ansi@npm:7.1.0" + version: 7.2.0 + resolution: "strip-ansi@npm:7.2.0" dependencies: - ansi-regex: "npm:^6.0.1" - checksum: 10/475f53e9c44375d6e72807284024ac5d668ee1d06010740dec0b9744f2ddf47de8d7151f80e5f6190fc8f384e802fdf9504b76a7e9020c9faee7103623338be2 + ansi-regex: "npm:^6.2.2" + checksum: 10/96da3bc6d73cfba1218625a3d66cf7d37a69bf0920d8735b28f9eeaafcdb6c1fe8440e1ae9eb1ba0ca355dbe8702da872e105e2e939fa93e7851b3cb5dd7d316 languageName: node linkType: hard @@ -23836,7 +25482,7 @@ __metadata: languageName: node linkType: hard -"sucrase@npm:3.35.0, sucrase@npm:^3.35.0": +"sucrase@npm:3.35.0": version: 3.35.0 resolution: "sucrase@npm:3.35.0" dependencies: @@ -23854,10 +25500,21 @@ __metadata: languageName: node linkType: hard -"sudo-prompt@npm:^8.2.0": - version: 8.2.5 - resolution: "sudo-prompt@npm:8.2.5" - checksum: 10/5977f72564dc49920a241a08dcae93e110f2e682381ad755b502a6f431548b9aa03169143c9e1a28fe4b430f206c9053128be7993c6d6d2b6d402ed5824ef74a +"sucrase@npm:^3.35.0": + version: 3.35.1 + resolution: "sucrase@npm:3.35.1" + dependencies: + "@jridgewell/gen-mapping": "npm:^0.3.2" + commander: "npm:^4.0.0" + lines-and-columns: "npm:^1.1.6" + mz: "npm:^2.7.0" + pirates: "npm:^4.0.1" + tinyglobby: "npm:^0.2.11" + ts-interface-checker: "npm:^0.1.9" + bin: + sucrase: bin/sucrase + sucrase-node: bin/sucrase-node + checksum: 10/539f5c6ebc1ff8d449a89eb52b8c8944a730b9840ddadbd299a7d89ebcf16c3f4bc9aa59e1f2e112a502e5cf1508f7e02065f0e97c0435eb9a7058e997dfff5a languageName: node linkType: hard @@ -24030,13 +25687,6 @@ __metadata: languageName: node linkType: hard -"temp-dir@npm:~2.0.0": - version: 2.0.0 - resolution: "temp-dir@npm:2.0.0" - checksum: 10/cc4f0404bf8d6ae1a166e0e64f3f409b423f4d1274d8c02814a59a5529f07db6cd070a749664141b992b2c1af337fa9bb451a460a43bb9bcddc49f235d3115aa - languageName: node - linkType: hard - "terminal-link@npm:^2.1.1": version: 2.1.1 resolution: "terminal-link@npm:2.1.1" @@ -24167,7 +25817,7 @@ __metadata: languageName: node linkType: hard -"tinyglobby@npm:^0.2.14, tinyglobby@npm:^0.2.15": +"tinyglobby@npm:^0.2.11, tinyglobby@npm:^0.2.14, tinyglobby@npm:^0.2.15": version: 0.2.15 resolution: "tinyglobby@npm:0.2.15" dependencies: @@ -24237,7 +25887,7 @@ __metadata: languageName: node linkType: hard -"toidentifier@npm:1.0.1": +"toidentifier@npm:1.0.1, toidentifier@npm:~1.0.1": version: 1.0.1 resolution: "toidentifier@npm:1.0.1" checksum: 10/952c29e2a85d7123239b5cfdd889a0dde47ab0497f0913d70588f19c53f7e0b5327c95f4651e413c74b785147f9637b17410ac8c846d5d4a20a5a33eb6dc3a45 @@ -24255,6 +25905,13 @@ __metadata: languageName: node linkType: hard +"toqr@npm:^0.1.1": + version: 0.1.1 + resolution: "toqr@npm:0.1.1" + checksum: 10/b75da11ce8bf645f805c43fc8a2ea6dfe5e7d2da9a751404deb72d48def027abccdf4ea3af5dce771852717f5c2c5d2eb7fdee246566eccbdab9b86a98ba9100 + languageName: node + linkType: hard + "tough-cookie@npm:^4.1.2": version: 4.1.4 resolution: "tough-cookie@npm:4.1.4" @@ -24306,12 +25963,12 @@ __metadata: languageName: node linkType: hard -"ts-api-utils@npm:^2.1.0": - version: 2.1.0 - resolution: "ts-api-utils@npm:2.1.0" +"ts-api-utils@npm:^2.1.0, ts-api-utils@npm:^2.5.0": + version: 2.5.0 + resolution: "ts-api-utils@npm:2.5.0" peerDependencies: typescript: ">=4.8.4" - checksum: 10/02e55b49d9617c6eebf8aadfa08d3ca03ca0cd2f0586ad34117fdfc7aa3cd25d95051843fde9df86665ad907f99baed179e7a117b11021417f379e4d2614eacd + checksum: 10/d5f1936f5618c6ab6942a97b78802217540ced00e7501862ae1f578d9a3aa189fc06050e64cb8951d21f7088e5fd35f53d2bf0d0370a883861c7b05e993ebc44 languageName: node linkType: hard @@ -24462,13 +26119,6 @@ __metadata: languageName: node linkType: hard -"type@npm:^1.0.1": - version: 1.2.0 - resolution: "type@npm:1.2.0" - checksum: 10/b4d4b27d1926028be45fc5baaca205896e2a1fe9e5d24dc892046256efbe88de6acd0149e7353cd24dad596e1483e48ec60b0912aa47ca078d68cdd198b09885 - languageName: node - linkType: hard - "type@npm:^2.7.2": version: 2.7.2 resolution: "type@npm:2.7.2" @@ -24624,13 +26274,6 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~6.19.8": - version: 6.19.8 - resolution: "undici-types@npm:6.19.8" - checksum: 10/cf0b48ed4fc99baf56584afa91aaffa5010c268b8842f62e02f752df209e3dea138b372a60a963b3b2576ed932f32329ce7ddb9cb5f27a6c83040d8cd74b7a70 - languageName: node - linkType: hard - "undici-types@npm:~6.21.0": version: 6.21.0 resolution: "undici-types@npm:6.21.0" @@ -24638,6 +26281,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~7.18.0": + version: 7.18.2 + resolution: "undici-types@npm:7.18.2" + checksum: 10/e61a5918f624d68420c3ca9d301e9f15b61cba6e97be39fe2ce266dd6151e4afe424d679372638826cb506be33952774e0424141200111a9857e464216c009af + languageName: node + linkType: hard + "undici@npm:^6.18.2": version: 6.21.0 resolution: "undici@npm:6.21.0" @@ -24662,10 +26312,10 @@ __metadata: languageName: node linkType: hard -"unicode-match-property-value-ecmascript@npm:^2.1.0": - version: 2.1.0 - resolution: "unicode-match-property-value-ecmascript@npm:2.1.0" - checksum: 10/06661bc8aba2a60c7733a7044f3e13085808939ad17924ffd4f5222a650f88009eb7c09481dc9c15cfc593d4ad99bd1cde8d54042733b335672591a81c52601c +"unicode-match-property-value-ecmascript@npm:^2.2.1": + version: 2.2.1 + resolution: "unicode-match-property-value-ecmascript@npm:2.2.1" + checksum: 10/a42bebebab4c82ea6d8363e487b1fb862f82d1b54af1b67eb3fef43672939b685780f092c4f235266b90225863afa1258d57e7be3578d8986a08d8fc309aabe1 languageName: node linkType: hard @@ -24676,6 +26326,13 @@ __metadata: languageName: node linkType: hard +"unicorn-magic@npm:^0.3.0": + version: 0.3.0 + resolution: "unicorn-magic@npm:0.3.0" + checksum: 10/bdd7d7c522f9456f32a0b77af23f8854f9a7db846088c3868ec213f9550683ab6a2bdf3803577eacbafddb4e06900974385841ccb75338d17346ccef45f9cb01 + languageName: node + linkType: hard + "unified@npm:^11.0.0": version: 11.0.5 resolution: "unified@npm:11.0.5" @@ -24716,15 +26373,6 @@ __metadata: languageName: node linkType: hard -"unique-string@npm:~2.0.0": - version: 2.0.0 - resolution: "unique-string@npm:2.0.0" - dependencies: - crypto-random-string: "npm:^2.0.0" - checksum: 10/107cae65b0b618296c2c663b8e52e4d1df129e9af04ab38d53b4f2189e96da93f599c85f4589b7ffaf1a11c9327cbb8a34f04c71b8d4950d3e385c2da2a93828 - languageName: node - linkType: hard - "unist-builder@npm:^4.0.0": version: 4.0.0 resolution: "unist-builder@npm:4.0.0" @@ -24822,9 +26470,9 @@ __metadata: languageName: node linkType: hard -"update-browserslist-db@npm:^1.1.3": - version: 1.1.3 - resolution: "update-browserslist-db@npm:1.1.3" +"update-browserslist-db@npm:^1.2.3": + version: 1.2.3 + resolution: "update-browserslist-db@npm:1.2.3" dependencies: escalade: "npm:^3.2.0" picocolors: "npm:^1.1.1" @@ -24832,7 +26480,7 @@ __metadata: browserslist: ">= 4.21.0" bin: update-browserslist-db: cli.js - checksum: 10/87af2776054ffb9194cf95e0201547d041f72ee44ce54b144da110e65ea7ca01379367407ba21de5c9edd52c74d95395366790de67f3eb4cc4afa0fe4424e76f + checksum: 10/059f774300efb4b084a49293143c511f3ae946d40397b5c30914e900cd5691a12b8e61b41dd54ed73d3b56c8204165a0333107dd784ccf8f8c81790bcc423175 languageName: node linkType: hard @@ -25298,9 +26946,9 @@ __metadata: linkType: hard "webpack-sources@npm:^3.2.0, webpack-sources@npm:^3.2.3": - version: 3.3.3 - resolution: "webpack-sources@npm:3.3.3" - checksum: 10/ec5d72607e8068467370abccbfff855c596c098baedbe9d198a557ccf198e8546a322836a6f74241492576adba06100286592993a62b63196832cdb53c8bae91 + version: 3.3.4 + resolution: "webpack-sources@npm:3.3.4" + checksum: 10/714427b235b04c2d7cf229f204b9e65145ea3643da3c7b139ebfa8a51056238d1e3a2a47c3cc3fc8eab71ed4300f66405cdc7cff29cd2f7f6b71086252f81cf1 languageName: node linkType: hard @@ -25361,6 +27009,13 @@ __metadata: languageName: node linkType: hard +"whatwg-url-minimum@npm:^0.1.1": + version: 0.1.1 + resolution: "whatwg-url-minimum@npm:0.1.1" + checksum: 10/96d06b1ad60bd8e0eb134a4741e244ee91030edb59fd0bcc01a808daeb0110d84eee92c8bc462a2675be82ecac33ec560a28429bb4fec3587846b58388351bf7 + languageName: node + linkType: hard + "whatwg-url-without-unicode@npm:8.0.0-3": version: 8.0.0-3 resolution: "whatwg-url-without-unicode@npm:8.0.0-3" @@ -25714,11 +27369,11 @@ __metadata: linkType: hard "yaml@npm:^2.2.1, yaml@npm:^2.6.0, yaml@npm:^2.6.1, yaml@npm:^2.8.1": - version: 2.8.1 - resolution: "yaml@npm:2.8.1" + version: 2.8.3 + resolution: "yaml@npm:2.8.3" bin: yaml: bin.mjs - checksum: 10/eae07b3947d405012672ec17ce27348aea7d1fa0534143355d24a43a58f5e05652157ea2182c4fe0604f0540be71f99f1173f9d61018379404507790dff17665 + checksum: 10/ecad41d39d34fae5cc17ea2d4b7f7f55faacd45cbce8983ba22d48d1ed1a92ed242ea49ea813a79ac39a69f75f9c5a03e7b5395fd954d55476f25e21a47c141d languageName: node linkType: hard