Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
Comment thread
carocao-msft marked this conversation as resolved.
"comment": "Display correct error text and guidance text for device permission errors on safari browser",
"packageName": "@azure/communication-react",
"email": "carolinecao@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Display correct error text and guidance text for device permission errors on safari browser",
"packageName": "@azure/communication-react",
"email": "carolinecao@microsoft.com",
"dependentChangeType": "patch"
}
33 changes: 27 additions & 6 deletions packages/calling-component-bindings/src/errorBarSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
// Licensed under the MIT license.

import { CallingBaseSelectorProps, getDeviceManager, getDiagnostics, getLatestErrors } from './baseSelectors';
/* @conditional-compile-remove(unsupported-browser) */
import { getEnvironmentInfo } from './baseSelectors';
import { ActiveErrorMessage, ErrorType } from '@internal/react-components';
import { createSelector } from 'reselect';
import { CallClientState, CallErrors, CallErrorTarget } from '@internal/calling-stateful-client';
import { DiagnosticQuality } from '@azure/communication-calling';

/**
* Selector type for {@link ErrorBar} component.
*
Expand All @@ -32,8 +33,18 @@ export type ErrorBarSelector = (
* @public
*/
export const errorBarSelector: ErrorBarSelector = createSelector(
[getLatestErrors, getDiagnostics, getDeviceManager],
(latestErrors: CallErrors, diagnostics, deviceManager): { activeErrorMessages: ActiveErrorMessage[] } => {
[
getLatestErrors,
getDiagnostics,
getDeviceManager,
/* @conditional-compile-remove(unsupported-browser) */ getEnvironmentInfo
],
(
latestErrors: CallErrors,
diagnostics,
deviceManager,
/* @conditional-compile-remove(unsupported-browser) */ environmentInfo
): { activeErrorMessages: ActiveErrorMessage[] } => {
// The order in which the errors are returned is significant: The `ErrorBar` shows errors on the UI in that order.
// There are several options for the ordering:
// - Sorted by when the errors happened (latest first / oldest first).
Expand All @@ -43,6 +54,12 @@ export const errorBarSelector: ErrorBarSelector = createSelector(
// have timestamps for errors.
const activeErrorMessages: ActiveErrorMessage[] = [];

const isSafari = (): boolean => {
/* @conditional-compile-remove(unsupported-browser) */
return environmentInfo?.environment.browser === 'safari';
return /^((?!chrome|android|crios|fxios).)*safari/i.test(navigator.userAgent);
};

// Errors reported via diagnostics are more reliable than from API method failures, so process those first.
if (
diagnostics?.network.latest.networkReceiveQuality?.value === DiagnosticQuality.Bad ||
Expand All @@ -56,7 +73,10 @@ export const errorBarSelector: ErrorBarSelector = createSelector(
if (diagnostics?.media.latest.noMicrophoneDevicesEnumerated?.value === true) {
activeErrorMessages.push({ type: 'callNoMicrophoneFound' });
}
if (deviceManager.deviceAccess?.audio === false) {
if (deviceManager.deviceAccess?.audio === false && isSafari()) {
activeErrorMessages.push({ type: 'callMicrophoneAccessDeniedSafari' });
}
if (deviceManager.deviceAccess?.audio === false && !isSafari()) {
activeErrorMessages.push({ type: 'callMicrophoneAccessDenied' });
}
if (diagnostics?.media.latest.microphonePermissionDenied?.value === true) {
Expand Down Expand Up @@ -85,8 +105,9 @@ export const errorBarSelector: ErrorBarSelector = createSelector(
activeErrorMessages.push({ type: 'callVideoRecoveredBySystem' });
}
}

if (deviceManager.deviceAccess?.video === false) {
if (deviceManager.deviceAccess?.video === false && isSafari()) {
activeErrorMessages.push({ type: 'callCameraAccessDeniedSafari' });
} else if (deviceManager.deviceAccess?.video === false) {
activeErrorMessages.push({ type: 'callCameraAccessDenied' });
} else {
if (diagnostics?.media.latest.cameraFreeze?.value === true) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1419,6 +1419,7 @@ export interface CommonCallingHandlers {
// @beta
export interface CommonDomainPermissionsProps {
appName: string;
browser?: string;
onContinueAnywayClick?: () => void;
onTroubleshootingClick?: () => void;
styles?: DomainPermissionsStyles;
Expand Down Expand Up @@ -1497,10 +1498,12 @@ export interface ComponentStrings {
BrowserPermissionDeniedIOS: BrowserPermissionDeniedIOSStrings;
CameraAndMicrophoneDomainPermissionsCheck: DomainPermissionsStrings;
CameraAndMicrophoneDomainPermissionsDenied: DomainPermissionsStrings;
CameraAndMicrophoneDomainPermissionsDeniedSafari: DomainPermissionsStrings;
CameraAndMicrophoneDomainPermissionsRequest: DomainPermissionsStrings;
cameraButton: CameraButtonStrings;
CameraDomainPermissionsCheck: DomainPermissionsStrings;
CameraDomainPermissionsDenied: DomainPermissionsStrings;
CameraDomainPermissionsDeniedSafari: DomainPermissionsStrings;
CameraDomainPermissionsRequest: DomainPermissionsStrings;
devicesButton: DevicesButtonStrings;
dialpad: DialpadStrings;
Expand All @@ -1512,6 +1515,7 @@ export interface ComponentStrings {
microphoneButton: MicrophoneButtonStrings;
MicrophoneDomainPermissionsCheck: DomainPermissionsStrings;
MicrophoneDomainPermissionsDenied: DomainPermissionsStrings;
MicrophoneDomainPermissionsDeniedSafari: DomainPermissionsStrings;
MicrophoneDomainPermissionsRequest: DomainPermissionsStrings;
participantItem: ParticipantItemStrings;
participantsButton: ParticipantsButtonStrings;
Expand Down Expand Up @@ -2123,12 +2127,14 @@ export interface ErrorBarProps extends IMessageBarProps {
export interface ErrorBarStrings {
accessDenied: string;
callCameraAccessDenied: string;
callCameraAccessDeniedSafari: string;
callCameraAlreadyInUse: string;
callLocalVideoFreeze: string;
callMacOsCameraAccessDenied: string;
callMacOsMicrophoneAccessDenied: string;
callMacOsScreenShareAccessDenied: string;
callMicrophoneAccessDenied: string;
callMicrophoneAccessDeniedSafari: string;
callMicrophoneMutedBySystem: string;
callMicrophoneUnmutedBySystem: string;
callNetworkQualityLow: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1624,12 +1624,14 @@ export interface ErrorBarProps extends IMessageBarProps {
export interface ErrorBarStrings {
accessDenied: string;
callCameraAccessDenied: string;
callCameraAccessDeniedSafari: string;
callCameraAlreadyInUse: string;
callLocalVideoFreeze: string;
callMacOsCameraAccessDenied: string;
callMacOsMicrophoneAccessDenied: string;
callMacOsScreenShareAccessDenied: string;
callMicrophoneAccessDenied: string;
callMicrophoneAccessDeniedSafari: string;
callMicrophoneMutedBySystem: string;
callMicrophoneUnmutedBySystem: string;
callNetworkQualityLow: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ export interface ChatMessage extends MessageCommon {
// @beta
export interface CommonDomainPermissionsProps {
appName: string;
browser?: string;
onContinueAnywayClick?: () => void;
onTroubleshootingClick?: () => void;
styles?: DomainPermissionsStyles;
Expand Down Expand Up @@ -351,10 +352,12 @@ export interface ComponentStrings {
BrowserPermissionDeniedIOS: BrowserPermissionDeniedIOSStrings;
CameraAndMicrophoneDomainPermissionsCheck: DomainPermissionsStrings;
CameraAndMicrophoneDomainPermissionsDenied: DomainPermissionsStrings;
CameraAndMicrophoneDomainPermissionsDeniedSafari: DomainPermissionsStrings;
CameraAndMicrophoneDomainPermissionsRequest: DomainPermissionsStrings;
cameraButton: CameraButtonStrings;
CameraDomainPermissionsCheck: DomainPermissionsStrings;
CameraDomainPermissionsDenied: DomainPermissionsStrings;
CameraDomainPermissionsDeniedSafari: DomainPermissionsStrings;
CameraDomainPermissionsRequest: DomainPermissionsStrings;
devicesButton: DevicesButtonStrings;
dialpad: DialpadStrings;
Expand All @@ -366,6 +369,7 @@ export interface ComponentStrings {
microphoneButton: MicrophoneButtonStrings;
MicrophoneDomainPermissionsCheck: DomainPermissionsStrings;
MicrophoneDomainPermissionsDenied: DomainPermissionsStrings;
MicrophoneDomainPermissionsDeniedSafari: DomainPermissionsStrings;
MicrophoneDomainPermissionsRequest: DomainPermissionsStrings;
participantItem: ParticipantItemStrings;
participantsButton: ParticipantsButtonStrings;
Expand Down Expand Up @@ -733,12 +737,14 @@ export interface ErrorBarProps extends IMessageBarProps {
export interface ErrorBarStrings {
accessDenied: string;
callCameraAccessDenied: string;
callCameraAccessDeniedSafari: string;
callCameraAlreadyInUse: string;
callLocalVideoFreeze: string;
callMacOsCameraAccessDenied: string;
callMacOsMicrophoneAccessDenied: string;
callMacOsScreenShareAccessDenied: string;
callMicrophoneAccessDenied: string;
callMicrophoneAccessDeniedSafari: string;
callMicrophoneMutedBySystem: string;
callMicrophoneUnmutedBySystem: string;
callNetworkQualityLow: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -576,12 +576,14 @@ export interface ErrorBarProps extends IMessageBarProps {
export interface ErrorBarStrings {
accessDenied: string;
callCameraAccessDenied: string;
callCameraAccessDeniedSafari: string;
callCameraAlreadyInUse: string;
callLocalVideoFreeze: string;
callMacOsCameraAccessDenied: string;
callMacOsMicrophoneAccessDenied: string;
callMacOsScreenShareAccessDenied: string;
callMicrophoneAccessDenied: string;
callMicrophoneAccessDeniedSafari: string;
callMicrophoneMutedBySystem: string;
callMicrophoneUnmutedBySystem: string;
callNetworkQualityLow: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export interface CommonDomainPermissionsProps {
* Type of the Domain Permissions component.
*/
type: 'request' | 'denied' | 'check';
/**
* Type of the browser domain permission component is rendered in.
*/
browser?: string;
Comment thread
carocao-msft marked this conversation as resolved.
Outdated
/**
* Action to be taken by the more help link. Possible to send to external page or show other modal.
* If this is not provided the button will not be shown.
Expand Down Expand Up @@ -60,6 +64,13 @@ export interface CameraAndMicrophoneDomainPermissionsProps extends CommonDomainP
strings?: CameraAndMicrophoneDomainPermissionsStrings;
}

/* @conditional-compile-remove(call-readiness) */
const isSafari = (browser: string | undefined): boolean => {
/* @conditional-compile-remove(unsupported-browser) */
return browser === 'safari';
return /^((?!chrome|android|crios|fxios).)*safari/i.test(navigator.userAgent);
Comment thread
JamesBurnside marked this conversation as resolved.
Outdated
Comment thread
JamesBurnside marked this conversation as resolved.
Outdated
};

/**
* @beta
*
Expand All @@ -73,7 +84,9 @@ export const CameraAndMicrophoneDomainPermissions = (props: CameraAndMicrophoneD
/* @conditional-compile-remove(call-readiness) */
const strings = useShallowMerge(
props.type === 'denied'
? locale.CameraAndMicrophoneDomainPermissionsDenied
? isSafari(props.browser)
? locale.CameraAndMicrophoneDomainPermissionsDeniedSafari
: locale.CameraAndMicrophoneDomainPermissionsDenied
: props.type === 'request'
? locale.CameraAndMicrophoneDomainPermissionsRequest
: locale.CameraAndMicrophoneDomainPermissionsCheck,
Expand Down Expand Up @@ -133,7 +146,9 @@ export const MicrophoneDomainPermissions = (props: MicrophoneDomainPermissionsPr
/* @conditional-compile-remove(call-readiness) */
const strings = useShallowMerge(
props.type === 'denied'
? locale.MicrophoneDomainPermissionsDenied
? isSafari(props.browser)
? locale.MicrophoneDomainPermissionsDeniedSafari
: locale.MicrophoneDomainPermissionsDenied
: props.type === 'request'
? locale.MicrophoneDomainPermissionsRequest
: locale.MicrophoneDomainPermissionsCheck,
Expand Down Expand Up @@ -186,7 +201,9 @@ export const CameraDomainPermissions = (props: CameraDomainPermissionsProps): JS
/* @conditional-compile-remove(call-readiness) */
const strings = useShallowMerge(
props.type === 'denied'
? locale.CameraDomainPermissionsDenied
? isSafari(props.browser)
? locale.CameraDomainPermissionsDeniedSafari
: locale.CameraDomainPermissionsDenied
: props.type === 'request'
? locale.CameraDomainPermissionsRequest
: locale.CameraDomainPermissionsCheck,
Expand Down
10 changes: 10 additions & 0 deletions packages/react-components/src/components/ErrorBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ export interface ErrorBarStrings {
*/
callMicrophoneAccessDenied: string;

/**
* Message shown when microphone can be enumerated but access is blocked by the system, for safari browsers
*/
callMicrophoneAccessDeniedSafari: string;

/**
* Message shown when microphone is muted by the system (not by local or remote participants)
*/
Expand Down Expand Up @@ -160,6 +165,11 @@ export interface ErrorBarStrings {
*/
callCameraAccessDenied: string;

/**
* Message shown when camera can be enumerated but access is blocked by the system, for safari browsers
*/
callCameraAccessDeniedSafari: string;

/**
* Message shown when local video fails to start because camera is already in use by
* another applciation.
Expand Down
4 changes: 4 additions & 0 deletions packages/react-components/src/components/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,13 @@ export const messageBarType = (errorType: ErrorType): MessageBarType => {
case 'callNoSpeakerFound':
case 'callNoMicrophoneFound':
case 'callMicrophoneAccessDenied':
case 'callMicrophoneAccessDeniedSafari':
case 'callMicrophoneMutedBySystem':
case 'callMicrophoneUnmutedBySystem':
case 'callMacOsMicrophoneAccessDenied':
case 'callLocalVideoFreeze':
case 'callCameraAccessDenied':
case 'callCameraAccessDeniedSafari':
case 'callCameraAlreadyInUse':
case 'callVideoStoppedBySystem':
case 'callVideoRecoveredBySystem':
Expand Down Expand Up @@ -173,11 +175,13 @@ export const customIconName: Partial<{ [key in ErrorType]: string }> = {
callNoSpeakerFound: 'ErrorBarCallNoSpeakerFound',
callNoMicrophoneFound: 'ErrorBarCallNoMicrophoneFound',
callMicrophoneAccessDenied: 'ErrorBarCallMicrophoneAccessDenied',
callMicrophoneAccessDeniedSafari: 'ErrorBarCallMicrophoneAccessDenied',
callMicrophoneMutedBySystem: 'ErrorBarCallMicrophoneMutedBySystem',
callMicrophoneUnmutedBySystem: 'ErrorBarCallMicrophoneUnmutedBySystem',
callMacOsMicrophoneAccessDenied: 'ErrorBarCallMacOsMicrophoneAccessDenied',
callLocalVideoFreeze: 'ErrorBarCallLocalVideoFreeze',
callCameraAccessDenied: 'ErrorBarCallCameraAccessDenied',
callCameraAccessDeniedSafari: 'ErrorBarCallCameraAccessDenied',
callCameraAlreadyInUse: 'ErrorBarCallCameraAlreadyInUse',
callVideoStoppedBySystem: 'ErrorBarCallVideoStoppedBySystem',
callVideoRecoveredBySystem: 'ErrorBarCallVideoRecoveredBySystem',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,20 @@ export interface ComponentStrings {
/** Strings for a domain permission denied prompt */
CameraAndMicrophoneDomainPermissionsDenied: DomainPermissionsStrings;
/* @conditional-compile-remove(call-readiness) */
/** Strings for a domain permission denied prompt for safari browsers*/
CameraAndMicrophoneDomainPermissionsDeniedSafari: DomainPermissionsStrings;
/* @conditional-compile-remove(call-readiness) */
/** Strings for a domain permission denied prompt */
CameraDomainPermissionsDenied: DomainPermissionsStrings;
/* @conditional-compile-remove(call-readiness) */
/** Strings for a domain permission denied prompt */
MicrophoneDomainPermissionsDenied: DomainPermissionsStrings;
/* @conditional-compile-remove(call-readiness) */
/** Strings for a domain permission denied prompt for safari browsers*/
CameraDomainPermissionsDeniedSafari: DomainPermissionsStrings;
/* @conditional-compile-remove(call-readiness) */
/** Strings for a domain permission denied prompt for safari browsers*/
MicrophoneDomainPermissionsDeniedSafari: DomainPermissionsStrings;
/* @conditional-compile-remove(unsupported-browser) */
/** Strings for unsupported browser UI */
UnsupportedBrowser: UnsupportedBrowserStrings;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,13 @@
"callNoSpeakerFound": "No speakers or headphones found. Connect an audio device to hear the call.",
"callNoMicrophoneFound": "No microphones found. Connect an audio input device.",
"callMicrophoneAccessDenied": "Unable to access microphone. Click the lock in the address bar to grant permission to this webpage.",
"callMicrophoneAccessDeniedSafari": "Unable to access microphone. Refresh the page to allow permissions, or check this browser’s settings and verify permissions are enabled for this website.",
"callMicrophoneMutedBySystem": "You are muted by your system.",
"callMicrophoneUnmutedBySystem": "Your microphone recovered and you were unmuted by your system.",
"callMacOsMicrophoneAccessDenied": "Unable to access microphone. Grant microphone permission in your macOS privacy settings.",
"callLocalVideoFreeze": "Network bandwidth is poor. Your video may appear paused for others on the call.",
"callCameraAccessDenied": "Unable to access camera. Click the lock in the address bar to grant permission to this webpage.",
"callCameraAccessDeniedSafari": "Unable to access camera. Refresh the page to allow permissions, or check this browser’s settings and verify permissions are enabled for this website.",
"callCameraAlreadyInUse": "Unable to access camera. It may already be in use by another application.",
"callVideoStoppedBySystem": "Your video has been stopped by your system.",
"callVideoRecoveredBySystem": "Your video has resumed.",
Expand Down Expand Up @@ -234,6 +236,12 @@
"primaryButtonText": "Continue without camera and microphone",
"linkText": "Need help? Get troubleshooting help"
},
"CameraAndMicrophoneDomainPermissionsDeniedSafari": {
"primaryText": "Unable to access camera and microphone",
"secondaryText": "Refresh the page to allow permissions, or check this browser’s settings and verify permissions are enabled for this website.",
"primaryButtonText": "Continue without camera and microphone",
"linkText": "Need help? Get troubleshooting help"
},
"CameraDomainPermissionsDenied": {
"primaryText": "Unable to access camera",
"secondaryText": "Click the lock icon in the address bar to grant camera permissions to this webpage. A page refresh may be required.",
Expand All @@ -246,6 +254,18 @@
"primaryButtonText": "Continue without microphone",
"linkText": "Need help? Get troubleshooting help"
},
"CameraDomainPermissionsDeniedSafari": {
"primaryText": "Unable to access camera",
"secondaryText": "Refresh the page to allow permissions, or check this browser’s settings and verify permissions are enabled for this website.",
"primaryButtonText": "Continue without camera",
"linkText": "Need help? Get troubleshooting help"
},
"MicrophoneDomainPermissionsDeniedSafari": {
"primaryText": "Unable to access microphone",
"secondaryText": "Refresh the page to allow permissions, or check this browser’s settings and verify permissions are enabled for this website.",
"primaryButtonText": "Continue without microphone",
"linkText": "Need help? Get troubleshooting help"
},
"UnsupportedBrowser": {
"primaryText": "Browser not supported",
"secondaryText": "Please join this call using a compatible browser.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,9 @@ export const CallArrangement = (props: CallArrangementProps): JSX.Element => {
if (!rolePermissions.cameraButton && props.errorBarProps) {
errorBarProps = {
...props.errorBarProps,
activeErrorMessages: props.errorBarProps.activeErrorMessages.filter((e) => e.type !== 'callCameraAccessDenied')
activeErrorMessages: props.errorBarProps.activeErrorMessages.filter(
(e) => e.type !== 'callCameraAccessDenied' && e.type !== 'callCameraAccessDeniedSafari'
)
};
}

Expand Down
Loading