DEV Community

Cover image for The Adapter Pattern?
Vicky V
Vicky V

Posted on • Updated on

The Adapter Pattern?

So recently I came across with a challenge where I had to create a way of allowing users to grant permission for camera/ mic in a web app and also on an android using the browser API and for the android is was a cordova plugin. In android is a little bit different with the same concept. So firstly need to check if the permission from the browser is allowed if not then to request permission. The logic is the sam. So instead of repeating myself, as I had to create (if I wouldn't use the adapter pattern two different Providers as well, plus use repetitive code on the files I want to use the adapter) I found out about the adapter pattern. That concept is one of the engineering patterns which is quite interesting, a bit difficult to get your head around the first time but then you can see how beneficial it can be plus next time you do it is much easier.

I will give you an example on a recent project and hope that helps you to use it on your work. I also use the mic-check package for microphone check, cause I saw it somewhere and thought it was cool. The example below is not fully finished but you will get the idea...

type AdapterType = {
  requestCameraPermission: () => void,
  requestMicrophonePermission: () => void,
  hasCameraPermission: () => Promise<boolean>,
  hasMicrophonePermission: () => Promise<boolean>,
};

export class PermissionsApiAdapter {
   hasCameraPermission = () =>
    navigator.permissions.query({ name: 'camera' }).then((permissionObj) => {
      console.log('permissionObj state:', permissionObj.state);

      return permissionObj.state === 'granted';
    });

  hasMicrophonePermission = () =>
    navigator.permissions
      .query({ name: 'microphone' })
      .then((permissionObj) => {
        console.log(permissionObj.state);

        return permissionObj.state === 'granted';
      });

  requestCameraPermission = () => {
    const cameraPermissions =
      navigator.mediaDevices &&
      navigator.mediaDevices.getUserMedia({
        video: true,
      });

    if (cameraPermissions) {
      return cameraPermissions
        .then(() => {
          alert('Accept permissions camera');
          console.log('request camera permissions enabled');

          return Promise.resolve(true);
        })
        .catch((err) =>
          Promise.reject(new Error(`${err.name} : ${err.message}`))
        );
    }

    return Promise.resolve(false);
  };

  requestMicrophonePermission = () => {
    const audioPermissions =
      navigator.mediaDevices &&
      navigator.mediaDevices.getUserMedia({
        audio: true,
      });

    if (audioPermissions) {
      return audioPermissions
        .then(() => {
          alert('Accept permissions microphone');
          console.log('request microphone permissions enabled');

          return Promise.resolve(true);
        })
        .catch((err: MediaPermissionsError) => {
          const { type } = err;

          if (type === MediaPermissionsErrorType.SystemPermissionDenied) {
            console.log(
              'browser does not have permission to access camera or microphone'
            );
          } else if (type === MediaPermissionsErrorType.UserPermissionDenied) {
            console.log('user didnt allow app to access camera or microphone');
          } else if (
            type === MediaPermissionsErrorType.CouldNotStartVideoSource
          ) {
            console.log(
              'camera is in use by another application (Zoom, Skype) or browser tab (Google Meet, Messenger Video'
            );
          }

          return Promise.reject(new Error(`${err.name} : ${err.message}`));
        });
    }

    return Promise.resolve(false);
  };
}

class WebAppsPermission {
  constructor(permissionAdapter: AdapterType) {
    this.permissionAdapter = permissionAdapter;
  }

  permissionAdapter: AdapterType;

  requestCameraPermission = () =>
    this.permissionAdapter.requestCameraPermission();
  requestMicrophonePermission = () =>
    this.permissionAdapter.requestMicrophonePermission();
  hasCameraPermission = () => this.permissionAdapter.hasCameraPermission();
  hasMicrophonePermission = () =>
    this.permissionAdapter.hasMicrophonePermission();
}

export default WebAppsPermission;
Enter fullscreen mode Exit fullscreen mode

Now for the Android Apps the adapter has same logic with a bit different functionality.


type AndroidAdapterType = {
  requestCameraPermission: () => void,
  requestMicrophonePermission: () => void,
  hasCameraPermission: () => Promise<boolean>,
  hasMicrophonePermission: () => Promise<boolean>,
};

export class AndroidPermissionsApiAdapter {
   hasCameraPermission = (): Promise<boolean> =>
    new Promise((resolve, reject) => {
      window.cordova.plugins.diagnostic.isCameraAuthorized(
        (authorized) => {
          console.log(
            `App is ${
              authorized ? 'authorized' : 'denied'
            } access to the camera`
          );

          resolve(authorized);
        },
        (error) => {
          console.error(`The following error occurred: ${error}`);
          reject(error);
        },
        false
      );
    });

  hasMicrophonePermission = (): Promise<boolean> =>
    new Promise((resolve, reject) => {
      window.cordova.plugins.diagnostic.isMicrophoneAuthorized(
        (authorized) => {
          console.log(
            `App is ${
              authorized ? 'authorized' : 'denied'
            } access to the microphone`
          );
          resolve(authorized);
        },
        (error) => {
          console.error(`The following error occurred: ${error}`);
          reject(error);
        }
      );
    });

  requestCameraPermission = (): Promise<boolean> =>
    new Promise((resolve, reject) => {
      window.cordova.plugins.diagnostic.requestCameraAuthorization(
        (status) => {
          console.log(
            `Authorization request for camera use was ${
              status ===
              window.cordova.plugins.diagnostic.permissionStatus.GRANTED
                ? 'granted'
                : 'denied'
            }`
          );
          resolve(status);
        },
        (error) => {
          console.error(error);
          reject(error);
        },

        false
      );
    });

  requestMicrophonePermission = (): Promise<boolean> =>
    new Promise((resolve, reject) => {
      window.cordova.plugins.diagnostic.requestMicrophoneAuthorization(
        (status) => {
          if (
            status ===
            window.cordova.plugins.diagnostic.permissionStatus.GRANTED
          ) {
            console.log('Microphone use is authorized');
          }

          resolve(status);
        },
        (error) => {
          console.error(error);
          reject(error);
        }
      );
    });
}

class AndroidPermissions {
  constructor(permissionAdapter: CordovaAdapterType) {
    this.permissionAdapter = permissionAdapter;
  }

  permissionAdapter: AndroidAdapterType;

  requestCameraPermission = () =>
    this.permissionAdapter.requestCameraPermission();
  requestMicrophonePermission = () =>
    this.permissionAdapter.requestMicrophonePermission();
  hasCameraPermission = () => this.permissionAdapter.hasCameraPermission();
  hasMicrophonePermission = () =>
    this.permissionAdapter.hasMicrophonePermission();
}

export default AndroidPermissions;

Enter fullscreen mode Exit fullscreen mode

And when I wanted to use it in a specific folder in my case was inside a Context Provider

const adapter = new PermissionsApiAdapter();
    const cordovaAdapter = new AndroidPermissionsApiAdapter();
    const mediaPermission = new WebAppsPermission(
      window.cordova ? cordovaAdapter : adapter
    );
Enter fullscreen mode Exit fullscreen mode

So now I can use the adaptors on my Provider, or at any place of my code I want to.

Hope that makes sense ☺️

Discussion (0)