Doesn't connect when rejoining a call

I’m currently having an issue where joining a call for the first time works perfectly, however, when a peer who isn’t the host leaves and rejoins, the connection state never changes to connected; it gets stuck at connecting. After several hours of debugging, I realised the first set of handshake has its signalling state as “have-local-offer”, while the rejoining one has its signalling state as “stable”.

To also add, ending the call and restarting as a host works perfectly.

Here’s my code for proper explanation:

 const joinCall = useCallback(async () => {
    const callDoc = doc(collection(firestore, 'calls'), partyId);
    const callData = (await getDoc(callDoc)).data();

    if (callData?.callEnded) {
      Toast.show({
        type: ALERT_TYPE.DANGER,
        title: "Can't join party",
        textBody: 'The party has ended',
        titleStyle: tw`font-poppinsRegular text-xs`,
        textBodyStyle: tw`font-poppinsRegular text-xs`,
      });
      setCallEnded(true);
      return false;
    }

    if (!callData?.callStarted) {
      Toast.show({
        type: ALERT_TYPE.DANGER,
        title: "Can't join party yet",
        textBody: "Host hasn't started the party",
        titleStyle: tw`font-poppinsRegular text-xs`,
        textBodyStyle: tw`font-poppinsRegular text-xs`,
      });
      setCallStarted(false);
      return false;
    }

    setCallStarted(true);

    const pc = createPeerConnection();
    const stream = await setupMediaStream();

    if (stream) {
      stream.getTracks().forEach(track => pc.addTrack(track, stream));
    }

    try {
      console.log('join call signaling state', pc.signalingState);
      if (callData?.offer && pc.signalingState === 'stable') {
        const offerDescription = new RTCSessionDescription(callData.offer);
        await pc.setRemoteDescription(offerDescription);

        const answerDescription = await pc.createAnswer();
        await pc.setLocalDescription(answerDescription);

        await updateDoc(callDoc, {answer: answerDescription});
        addIceCandidatesToPeerConnection();
      }

      unsubscribeSnapshot = onSnapshot(callDoc, snapshot => {
        const data = snapshot.data();
        if (data?.callEnded) {
          setCallEnded(true);
        }
        if (data?.candidates) {
          console.log(
            'ICE candidates received from Firestore:',
            data.candidates,
          );
          data.candidates.forEach((candidateData: any) => {
            const candidate = new RTCIceCandidate(candidateData);
            if (pc.remoteDescription) {
              console.log('Adding ICE candidate:', candidate);
              pc.addIceCandidate(candidate).catch(error => {
                console.error(
                  'Failed to add ICE candidate when joining the call',
                  error,
                );
              });
            } else {
              console.warn(
                'Remote description not set yet, queuing ICE candidate:',
                candidate,
              );
              ICE_CANDIDATES_QUEUE.push(candidate);
            }
          });
        }
      });

      return true;
    } catch (error) {
      console.error('Error in joinCall:', error);
      return false;
    }
  }, [
    createPeerConnection,
    setupMediaStream,
    firestore,
    partyId,
    addIceCandidatesToPeerConnection,
  ]);
  const leaveCall = useCallback(async () => {
    const pc = PEERCONNECTION;
    if (pc) {
      pc.getTransceivers().forEach(transceiver => {
        transceiver.stop();
      });
      pc.close();
      PEERCONNECTION = null;
    }

    if (LOCAL_STREAM) {
      LOCAL_STREAM.getTracks().forEach(track => {
        track.stop();
      });
      LOCAL_STREAM = null;
    }

    if (REMOTE_STREAM) {
      REMOTE_STREAM.getTracks().forEach(track => {
        track.stop();
      });
      REMOTE_STREAM = null;
    }

    if (unsubscribeSnapshot) {
      unsubscribeSnapshot();
    }

    setIsConnected(false);
    setCallEnded(true);
  }, []);
 const beginParty = useCallback(async () => {
    const pc = createPeerConnection();
    const stream = await setupMediaStream();

    if (stream) {
      stream.getTracks().forEach(track => pc.addTrack(track, stream));
    }

    const callDoc = doc(collection(db, 'calls'), partyId);
    const offerOptions = {
      offerToReceiveAudio: true,
      offerToReceiveVideo: false,
      voiceActivityDetection: true,
    };

    try {
      const offer = await pc.createOffer(offerOptions);
      await pc.setLocalDescription(offer);

      await setDoc(callDoc, {
        offer: offer,
        candidates: [],
        callStarted: true,
        callEnded: false,
      });

      unsubscribeSnapshot = onSnapshot(callDoc, snapshot => {
        const data = snapshot.data();
        if (data?.callEnded) {
          setCallEnded(true);
        }
        console.log('signalling', pc.signalingState);
        if (data?.answer && pc.signalingState === 'have-local-offer') {
          const answerDescription = new RTCSessionDescription(data.answer);
          pc.setRemoteDescription(answerDescription)
            .then(
              () => console.log('adding ice candidate'),
              addIceCandidatesToPeerConnection,
            )
            .catch(error => {
              console.error(
                'Failed to set remote description when beginning party:',
                error,
              );
            });
        }
        if (data?.candidates) {
          data.candidates.forEach((candidateData: any) => {
            const candidate = new RTCIceCandidate(candidateData);
            if (pc.remoteDescription) {
              pc.addIceCandidate(candidate).catch(error => {
                console.error(
                  'Failed to add ICE candidate when beginning party:',
                  error,
                );
              });
            } else {
              ICE_CANDIDATES_QUEUE.push(candidate);
            }
          });
        }
      });
    } catch (error) {
      console.error('Error in beginParty:', error);
    }
  }, [
    createPeerConnection,
    setupMediaStream,
    partyId,
    addIceCandidatesToPeerConnection,
  ]);