1082815dcSEvan Baconimport { ExpoConfig } from '@expo/config-types';
2082815dcSEvan Bacon
3*8a424bebSJames Ideimport { InfoPlist } from './IosConfig.types';
4082815dcSEvan Baconimport { createInfoPlistPlugin } from '../plugins/ios-plugins';
5082815dcSEvan Baconimport { gteSdkVersion } from '../utils/versions';
6082815dcSEvan Baconimport { addWarningIOS } from '../utils/warnings';
7082815dcSEvan Bacon
8082815dcSEvan Baconexport const withRequiresFullScreen = createInfoPlistPlugin(
9082815dcSEvan Bacon  setRequiresFullScreen,
10082815dcSEvan Bacon  'withRequiresFullScreen'
11082815dcSEvan Bacon);
12082815dcSEvan Bacon
13082815dcSEvan Bacon// NOTES: This is defaulted to `true` for now to match the behavior prior to SDK
14082815dcSEvan Bacon// 34, but will change to `false` in SDK +43.
15082815dcSEvan Baconexport function getRequiresFullScreen(config: Pick<ExpoConfig, 'ios' | 'sdkVersion'>) {
16082815dcSEvan Bacon  // Yes, the property is called ios.requireFullScreen, without the s - not "requires"
17082815dcSEvan Bacon  // This is confusing indeed because the actual property name does have the s
18082815dcSEvan Bacon  if (config.ios?.hasOwnProperty('requireFullScreen')) {
19082815dcSEvan Bacon    return !!config.ios.requireFullScreen;
20082815dcSEvan Bacon  } else {
21082815dcSEvan Bacon    // In SDK 43, the `requireFullScreen` default has been changed to false.
22082815dcSEvan Bacon    if (
23082815dcSEvan Bacon      gteSdkVersion(config, '43.0.0')
24082815dcSEvan Bacon      // TODO: Uncomment after SDK 43 is released.
25082815dcSEvan Bacon      // || !config.sdkVersion
26082815dcSEvan Bacon    ) {
27082815dcSEvan Bacon      return false;
28082815dcSEvan Bacon    }
29082815dcSEvan Bacon    return true;
30082815dcSEvan Bacon  }
31082815dcSEvan Bacon}
32082815dcSEvan Bacon
33082815dcSEvan Baconconst iPadInterfaceKey = 'UISupportedInterfaceOrientations~ipad';
34082815dcSEvan Bacon
35082815dcSEvan Baconconst requiredIPadInterface = [
36082815dcSEvan Bacon  'UIInterfaceOrientationPortrait',
37082815dcSEvan Bacon  'UIInterfaceOrientationPortraitUpsideDown',
38082815dcSEvan Bacon  'UIInterfaceOrientationLandscapeLeft',
39082815dcSEvan Bacon  'UIInterfaceOrientationLandscapeRight',
40082815dcSEvan Bacon];
41082815dcSEvan Bacon
42082815dcSEvan Baconfunction isStringArray(value: any): value is string[] {
43082815dcSEvan Bacon  return Array.isArray(value) && value.every((value) => typeof value === 'string');
44082815dcSEvan Bacon}
45082815dcSEvan Bacon
46082815dcSEvan Baconfunction hasMinimumOrientations(masks: string[]): boolean {
47082815dcSEvan Bacon  return requiredIPadInterface.every((mask) => masks.includes(mask));
48082815dcSEvan Bacon}
49082815dcSEvan Bacon
50082815dcSEvan Bacon/**
51082815dcSEvan Bacon * Require full screen being disabled requires all ipad interfaces to to be added,
52082815dcSEvan Bacon * otherwise submissions to the iOS App Store will fail.
53082815dcSEvan Bacon *
54082815dcSEvan Bacon * ERROR ITMS-90474: "Invalid Bundle. iPad Multitasking support requires these orientations: 'UIInterfaceOrientationPortrait,UIInterfaceOrientationPortraitUpsideDown,UIInterfaceOrientationLandscapeLeft,UIInterfaceOrientationLandscapeRight'. Found 'UIInterfaceOrientationPortrait,UIInterfaceOrientationPortraitUpsideDown' in bundle 'com.bacon.app'."
55082815dcSEvan Bacon *
56082815dcSEvan Bacon * @param interfaceOrientations
57082815dcSEvan Bacon * @returns
58082815dcSEvan Bacon */
59082815dcSEvan Baconfunction resolveExistingIpadInterfaceOrientations(interfaceOrientations: any): string[] {
60082815dcSEvan Bacon  if (
61082815dcSEvan Bacon    // Ensure type.
62082815dcSEvan Bacon    isStringArray(interfaceOrientations) &&
63082815dcSEvan Bacon    // Don't warn if it's an empty array, this is invalid regardless.
64082815dcSEvan Bacon    interfaceOrientations.length &&
65082815dcSEvan Bacon    // Check if the minimum requirements are met.
66082815dcSEvan Bacon    !hasMinimumOrientations(interfaceOrientations)
67082815dcSEvan Bacon  ) {
68082815dcSEvan Bacon    const existingList = interfaceOrientations!.join(', ');
69082815dcSEvan Bacon    addWarningIOS(
70082815dcSEvan Bacon      'ios.requireFullScreen',
71082815dcSEvan Bacon      `iPad multitasking requires all \`${iPadInterfaceKey}\` orientations to be defined in the Info.plist. The Info.plist currently defines values that are incompatible with multitasking, these will be overwritten to prevent submission failure. Existing: ${existingList}`
72082815dcSEvan Bacon    );
73082815dcSEvan Bacon    return interfaceOrientations;
74082815dcSEvan Bacon  }
75082815dcSEvan Bacon  return [];
76082815dcSEvan Bacon}
77082815dcSEvan Bacon
78082815dcSEvan Bacon// Whether requires full screen on iPad
79082815dcSEvan Baconexport function setRequiresFullScreen(
80082815dcSEvan Bacon  config: Pick<ExpoConfig, 'ios'>,
81082815dcSEvan Bacon  infoPlist: InfoPlist
82082815dcSEvan Bacon): InfoPlist {
83082815dcSEvan Bacon  const requiresFullScreen = getRequiresFullScreen(config);
84082815dcSEvan Bacon  if (!requiresFullScreen) {
85082815dcSEvan Bacon    const existing = resolveExistingIpadInterfaceOrientations(infoPlist[iPadInterfaceKey]);
86082815dcSEvan Bacon
87384598e2SBrent Vatne    // There currently exists no mechanism to safely undo this feature besides `npx expo prebuild --clear`,
88082815dcSEvan Bacon    // this seems ok though because anyone using `UISupportedInterfaceOrientations~ipad` probably
89082815dcSEvan Bacon    // wants them to be defined to this value anyways. This is also the default value used in the Xcode iOS template.
90082815dcSEvan Bacon
91082815dcSEvan Bacon    // Merge any previous interfaces with the required interfaces.
92082815dcSEvan Bacon    infoPlist[iPadInterfaceKey] = [...new Set(existing.concat(requiredIPadInterface))];
93082815dcSEvan Bacon  }
94082815dcSEvan Bacon
95082815dcSEvan Bacon  return {
96082815dcSEvan Bacon    ...infoPlist,
97082815dcSEvan Bacon    UIRequiresFullScreen: requiresFullScreen,
98082815dcSEvan Bacon  };
99082815dcSEvan Bacon}
100