diff --git a/.gitignore b/.gitignore index a065ebfb8be3..aeee5f730bfc 100644 --- a/.gitignore +++ b/.gitignore @@ -113,6 +113,11 @@ tsconfig.tsbuildinfo .yalc yalc.lock +# Expo +.expo +dist/ +web-build/ + # Local https certificate/key config/webpack/*.pem diff --git a/assets/images/fullscreen.svg b/assets/images/fullscreen.svg new file mode 100644 index 000000000000..6ae05ed9ad16 --- /dev/null +++ b/assets/images/fullscreen.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/images/meter.svg b/assets/images/meter.svg new file mode 100644 index 000000000000..4808c3e6870a --- /dev/null +++ b/assets/images/meter.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/mute.svg b/assets/images/mute.svg new file mode 100644 index 000000000000..afe5e802e640 --- /dev/null +++ b/assets/images/mute.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/pause.svg b/assets/images/pause.svg new file mode 100644 index 000000000000..8546a7e5a0ee --- /dev/null +++ b/assets/images/pause.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/play.svg b/assets/images/play.svg new file mode 100644 index 000000000000..1695800f8498 --- /dev/null +++ b/assets/images/play.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/assets/images/volume-high.svg b/assets/images/volume-high.svg new file mode 100644 index 000000000000..90eb27e24e85 --- /dev/null +++ b/assets/images/volume-high.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/volume-low.svg b/assets/images/volume-low.svg new file mode 100644 index 000000000000..d0d4224f0660 --- /dev/null +++ b/assets/images/volume-low.svg @@ -0,0 +1,4 @@ + + + + diff --git a/config/webpack/webpack.common.js b/config/webpack/webpack.common.js index cc859f220608..209a4c0c2753 100644 --- a/config/webpack/webpack.common.js +++ b/config/webpack/webpack.common.js @@ -22,6 +22,8 @@ const includeModules = [ 'react-native-google-places-autocomplete', 'react-native-qrcode-svg', 'react-native-view-shot', + '@react-native/assets', + 'expo-av', ].join('|'); const envToLogoSuffixMap = { @@ -241,6 +243,7 @@ const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({ ], fallback: { 'process/browser': require.resolve('process/browser'), + crypto: false, }, }, diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index a584dc723aff..acfc4d933954 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 059DC4EFD39EF39437E6823D /* libPods-NotificationServiceExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A997AA8204EA3D90907FA80 /* libPods-NotificationServiceExtension.a */; }; 083353EB2B5AB22A00C603C0 /* attention.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 083353E72B5AB22900C603C0 /* attention.mp3 */; }; 083353EC2B5AB22A00C603C0 /* done.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 083353E82B5AB22900C603C0 /* done.mp3 */; }; 083353ED2B5AB22A00C603C0 /* receive.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 083353E92B5AB22900C603C0 /* receive.mp3 */; }; @@ -26,23 +25,23 @@ 26AF3C3540374A9FACB6C19E /* ExpensifyMono-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = DCF33E34FFEC48128CDD41D4 /* ExpensifyMono-Bold.otf */; }; 2A9F8CDA983746B0B9204209 /* ExpensifyNeue-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = 52796131E6554494B2DDB056 /* ExpensifyNeue-Bold.otf */; }; 30581EA8AAFD4FCE88C5D191 /* ExpensifyNeue-Italic.otf in Resources */ = {isa = PBXBuildFile; fileRef = BF6A4C5167244B9FB8E4D4E3 /* ExpensifyNeue-Italic.otf */; }; - 3661A1374980E5F6804511FE /* libPods-NewExpensify-NewExpensifyTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 076FD9E41E08971BBF51D580 /* libPods-NewExpensify-NewExpensifyTests.a */; }; 374FB8D728A133FE000D84EF /* OriginImageRequestHandler.mm in Sources */ = {isa = PBXBuildFile; fileRef = 374FB8D628A133FE000D84EF /* OriginImageRequestHandler.mm */; }; + 5B8996A7D8B007ECC41919E1 /* libPods-NewExpensify.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00D7E69E5ADD16FD4C44221B /* libPods-NewExpensify.a */; }; 7041848526A8E47D00E09F4D /* RCTStartupTimer.m in Sources */ = {isa = PBXBuildFile; fileRef = 7041848426A8E47D00E09F4D /* RCTStartupTimer.m */; }; 7041848626A8E47D00E09F4D /* RCTStartupTimer.m in Sources */ = {isa = PBXBuildFile; fileRef = 7041848426A8E47D00E09F4D /* RCTStartupTimer.m */; }; 70CF6E82262E297300711ADC /* BootSplash.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 70CF6E81262E297300711ADC /* BootSplash.storyboard */; }; + 716815DBCE9F49D420334791 /* libPods-NewExpensify-NewExpensifyTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B68AEB9429D8BB73F25A188C /* libPods-NewExpensify-NewExpensifyTests.a */; }; 7F5E81F06BCCF61AD02CEA06 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCD444BEDDB0AF1745B39049 /* ExpoModulesProvider.swift */; }; 7F9DD8DA2B2A445B005E3AFA /* ExpError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F9DD8D92B2A445B005E3AFA /* ExpError.swift */; }; 7FD73C9E2B23CE9500420AF3 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FD73C9D2B23CE9500420AF3 /* NotificationService.swift */; }; 7FD73CA22B23CE9500420AF3 /* NotificationServiceExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 7FD73C9B2B23CE9500420AF3 /* NotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 976CCB5F8C921482E6AEAE71 /* libPods-NewExpensify.a in Frameworks */ = {isa = PBXBuildFile; fileRef = AB40AC8872A3DD6EF53D8B94 /* libPods-NewExpensify.a */; }; + B8A1CD44D011AD7AE180DE14 /* libPods-NotificationServiceExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B944D5699A54FD5197A63866 /* libPods-NotificationServiceExtension.a */; }; BDB853621F354EBB84E619C2 /* ExpensifyNewKansas-MediumItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = D2AFB39EC1D44BF9B91D3227 /* ExpensifyNewKansas-MediumItalic.otf */; }; DD79042B2792E76D004484B4 /* RCTBootSplash.m in Sources */ = {isa = PBXBuildFile; fileRef = DD79042A2792E76D004484B4 /* RCTBootSplash.m */; }; DDCB2E57F334C143AC462B43 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D20D83B0E39BA6D21761E72 /* ExpoModulesProvider.swift */; }; E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */ = {isa = PBXBuildFile; }; E9DF872D2525201700607FDC /* AirshipConfig.plist in Resources */ = {isa = PBXBuildFile; fileRef = E9DF872C2525201700607FDC /* AirshipConfig.plist */; }; ED222ED90E074A5481A854FA /* ExpensifyNeue-BoldItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = 8B28D84EF339436DBD42A203 /* ExpensifyNeue-BoldItalic.otf */; }; - EEAE4F8907465429AA5B5520 /* libPods-NewExpensify.a in Frameworks */ = {isa = PBXBuildFile; fileRef = AEFE6CD54912D427D19133C7 /* libPods-NewExpensify.a */; }; F0C450EA2705020500FD2970 /* colors.json in Resources */ = {isa = PBXBuildFile; fileRef = F0C450E92705020500FD2970 /* colors.json */; }; FF941A8D48F849269AB85C9A /* ExpensifyNewKansas-Medium.otf in Resources */ = {isa = PBXBuildFile; fileRef = 44BF435285B94E5B95F90994 /* ExpensifyNewKansas-Medium.otf */; }; /* End PBXBuildFile section */ @@ -80,9 +79,10 @@ /* Begin PBXFileReference section */ 008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = ""; }; + 00D7E69E5ADD16FD4C44221B /* libPods-NewExpensify.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NewExpensify.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 00E356EE1AD99517003FC87E /* NewExpensifyTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NewExpensifyTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 076FD9E41E08971BBF51D580 /* libPods-NewExpensify-NewExpensifyTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NewExpensify-NewExpensifyTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 030F99CEB3AEF1F11B001798 /* Pods-NewExpensify-NewExpensifyTests.debugproduction.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.debugproduction.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.debugproduction.xcconfig"; sourceTree = ""; }; 083353E72B5AB22900C603C0 /* attention.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = attention.mp3; path = ../assets/sounds/attention.mp3; sourceTree = ""; }; 083353E82B5AB22900C603C0 /* done.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = done.mp3; path = ../assets/sounds/done.mp3; sourceTree = ""; }; 083353E92B5AB22900C603C0 /* receive.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = receive.mp3; path = ../assets/sounds/receive.mp3; sourceTree = ""; }; @@ -92,72 +92,55 @@ 0F5BE0CD252686320097D869 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 0F5E534E263B73D5004CA14F /* EnvironmentChecker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EnvironmentChecker.h; sourceTree = ""; }; 0F5E534F263B73FD004CA14F /* EnvironmentChecker.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EnvironmentChecker.m; sourceTree = ""; }; + 0FF2BC0D01CA2C62CE94229E /* Pods-NewExpensify.debugdevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.debugdevelopment.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.debugdevelopment.xcconfig"; sourceTree = ""; }; + 11B2BA236BB72A603FBB7B99 /* Pods-NotificationServiceExtension.releasedevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.releasedevelopment.xcconfig"; path = "Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.releasedevelopment.xcconfig"; sourceTree = ""; }; + 12EB734390650799DB8AC627 /* Pods-NotificationServiceExtension.debugdevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.debugdevelopment.xcconfig"; path = "Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.debugdevelopment.xcconfig"; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* New Expensify Dev.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "New Expensify Dev.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = NewExpensify/AppDelegate.h; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = NewExpensify/Info.plist; sourceTree = ""; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = NewExpensify/main.m; sourceTree = ""; }; 18D050DF262400AF000D658B /* BridgingFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BridgingFile.swift; sourceTree = ""; }; - 1A997AA8204EA3D90907FA80 /* libPods-NotificationServiceExtension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NotificationServiceExtension.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 1DDE5449979A136852B939B5 /* Pods-NewExpensify.release adhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.release adhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.release adhoc.xcconfig"; sourceTree = ""; }; - 25A4587E168FD67CF890B448 /* Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig"; sourceTree = ""; }; - 30FFBD291B71222A393D9CC9 /* Pods-NewExpensify.releasedevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.releasedevelopment.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.releasedevelopment.xcconfig"; sourceTree = ""; }; - 32181F72DC539FFD1D1F0CA4 /* Pods-NewExpensify.releaseproduction.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.releaseproduction.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.releaseproduction.xcconfig"; sourceTree = ""; }; - 34A8FDD1F9AA58B8F15C8380 /* Pods-NewExpensify.release production.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.release production.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.release production.xcconfig"; sourceTree = ""; }; + 3328C9B8861CA667C42B47F3 /* Pods-NewExpensify.debugproduction.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.debugproduction.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.debugproduction.xcconfig"; sourceTree = ""; }; 374FB8D528A133A7000D84EF /* OriginImageRequestHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = OriginImageRequestHandler.h; path = NewExpensify/OriginImageRequestHandler.h; sourceTree = ""; }; 374FB8D628A133FE000D84EF /* OriginImageRequestHandler.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = OriginImageRequestHandler.mm; path = NewExpensify/OriginImageRequestHandler.mm; sourceTree = ""; }; - 3BBA44B891E03FAB8255E6F1 /* Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig"; sourceTree = ""; }; 44BF435285B94E5B95F90994 /* ExpensifyNewKansas-Medium.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNewKansas-Medium.otf"; path = "../assets/fonts/native/ExpensifyNewKansas-Medium.otf"; sourceTree = ""; }; + 466D03D63F4B48E009C04FA3 /* Pods-NewExpensify-NewExpensifyTests.releaseproduction.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.releaseproduction.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.releaseproduction.xcconfig"; sourceTree = ""; }; 4D20D83B0E39BA6D21761E72 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-NewExpensify/ExpoModulesProvider.swift"; sourceTree = ""; }; - 4E9593A0EE1C84B8A8EC062F /* Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig"; sourceTree = ""; }; 52796131E6554494B2DDB056 /* ExpensifyNeue-Bold.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNeue-Bold.otf"; path = "../assets/fonts/native/ExpensifyNeue-Bold.otf"; sourceTree = ""; }; - 52E63EFD054926BFEA3EC143 /* Pods-NewExpensify-NewExpensifyTests.debugproduction.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.debugproduction.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.debugproduction.xcconfig"; sourceTree = ""; }; - 68F4F270A8D1414FC14F356F /* Pods-NewExpensify.releaseadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.releaseadhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.releaseadhoc.xcconfig"; sourceTree = ""; }; + 5A1F158A9A6CBE170EC19D9C /* Pods-NewExpensify.debugadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.debugadhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.debugadhoc.xcconfig"; sourceTree = ""; }; 7041848326A8E40900E09F4D /* RCTStartupTimer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RCTStartupTimer.h; path = NewExpensify/RCTStartupTimer.h; sourceTree = ""; }; 7041848426A8E47D00E09F4D /* RCTStartupTimer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = RCTStartupTimer.m; path = NewExpensify/RCTStartupTimer.m; sourceTree = ""; }; 70CF6E81262E297300711ADC /* BootSplash.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = BootSplash.storyboard; path = NewExpensify/BootSplash.storyboard; sourceTree = ""; }; - 75CABB0D0ABB0082FE0EB600 /* Pods-NewExpensify.release staging.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.release staging.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.release staging.xcconfig"; sourceTree = ""; }; - 76BE68DA894BB75DDFE278DC /* Pods-NewExpensify.releasedevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.releasedevelopment.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.releasedevelopment.xcconfig"; sourceTree = ""; }; - 7B318CF669A0F7FE948D5CED /* Pods-NewExpensify.debugadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.debugadhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.debugadhoc.xcconfig"; sourceTree = ""; }; + 7312B334B72E8BE41A811FAB /* Pods-NewExpensify.releaseadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.releaseadhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.releaseadhoc.xcconfig"; sourceTree = ""; }; 7F9DD8D92B2A445B005E3AFA /* ExpError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpError.swift; sourceTree = ""; }; 7FD73C9B2B23CE9500420AF3 /* NotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 7FD73C9D2B23CE9500420AF3 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; 7FD73C9F2B23CE9500420AF3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 8709DF3C8D91F0FC1581CDD7 /* Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig"; sourceTree = ""; }; + 802CB9E7554756F188C79554 /* Pods-NotificationServiceExtension.debugadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.debugadhoc.xcconfig"; path = "Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.debugadhoc.xcconfig"; sourceTree = ""; }; 8B28D84EF339436DBD42A203 /* ExpensifyNeue-BoldItalic.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNeue-BoldItalic.otf"; path = "../assets/fonts/native/ExpensifyNeue-BoldItalic.otf"; sourceTree = ""; }; - 8D3B36BF88E773E3C1A383FA /* Pods-NewExpensify.debug staging.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.debug staging.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.debug staging.xcconfig"; sourceTree = ""; }; - 90E08F0C8C924EDA018C8866 /* Pods-NotificationServiceExtension.releaseproduction.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.releaseproduction.xcconfig"; path = "Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.releaseproduction.xcconfig"; sourceTree = ""; }; - 96552D489D9F09B6A5ABD81B /* Pods-NewExpensify-NewExpensifyTests.release production.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.release production.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.release production.xcconfig"; sourceTree = ""; }; - AB40AC8872A3DD6EF53D8B94 /* libPods-NewExpensify.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NewExpensify.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - AEFE6CD54912D427D19133C7 /* libPods-NewExpensify.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NewExpensify.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 96ADA7C82BA6A08C4A56344A /* Pods-NotificationServiceExtension.releaseproduction.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.releaseproduction.xcconfig"; path = "Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.releaseproduction.xcconfig"; sourceTree = ""; }; + 972584042DB4782F830B063A /* Pods-NewExpensify-NewExpensifyTests.releasedevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.releasedevelopment.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.releasedevelopment.xcconfig"; sourceTree = ""; }; + 9EADA69D62F2E7B3D96E5B1C /* Pods-NewExpensify.releaseproduction.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.releaseproduction.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.releaseproduction.xcconfig"; sourceTree = ""; }; + AC9422F6C8A49AE701481721 /* Pods-NotificationServiceExtension.releaseadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.releaseadhoc.xcconfig"; path = "Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.releaseadhoc.xcconfig"; sourceTree = ""; }; + B4CF7147C89747459BAC5BB7 /* Pods-NewExpensify.releasedevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.releasedevelopment.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.releasedevelopment.xcconfig"; sourceTree = ""; }; + B68AEB9429D8BB73F25A188C /* libPods-NewExpensify-NewExpensifyTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NewExpensify-NewExpensifyTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + B944D5699A54FD5197A63866 /* libPods-NotificationServiceExtension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NotificationServiceExtension.a"; sourceTree = BUILT_PRODUCTS_DIR; }; BCD444BEDDB0AF1745B39049 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-NewExpensify-NewExpensifyTests/ExpoModulesProvider.swift"; sourceTree = ""; }; - BD6E1BA27D6ABE0AC9D70586 /* Pods-NewExpensify-NewExpensifyTests.release development.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.release development.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.release development.xcconfig"; sourceTree = ""; }; - BD8828A882E2D6B51362AAC3 /* Pods-NewExpensify.releaseadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.releaseadhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.releaseadhoc.xcconfig"; sourceTree = ""; }; BF6A4C5167244B9FB8E4D4E3 /* ExpensifyNeue-Italic.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNeue-Italic.otf"; path = "../assets/fonts/native/ExpensifyNeue-Italic.otf"; sourceTree = ""; }; - C3788801E65E896FA7C77298 /* Pods-NewExpensify.debugproduction.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.debugproduction.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.debugproduction.xcconfig"; sourceTree = ""; }; - C3FF914C045A138C061D306E /* Pods-NotificationServiceExtension.debugproduction.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.debugproduction.xcconfig"; path = "Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.debugproduction.xcconfig"; sourceTree = ""; }; - CE2F84BEE9A6DCC228AF7E42 /* Pods-NewExpensify.debugproduction.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.debugproduction.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.debugproduction.xcconfig"; sourceTree = ""; }; - CECC4CBB97A55705A33BEA9E /* Pods-NewExpensify.debug development.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.debug development.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.debug development.xcconfig"; sourceTree = ""; }; + CB32BB7E082E2450F04DA6E7 /* Pods-NotificationServiceExtension.debugproduction.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.debugproduction.xcconfig"; path = "Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.debugproduction.xcconfig"; sourceTree = ""; }; + D15262BE5F713CDB4DA576AE /* Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig"; sourceTree = ""; }; D2AFB39EC1D44BF9B91D3227 /* ExpensifyNewKansas-MediumItalic.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNewKansas-MediumItalic.otf"; path = "../assets/fonts/native/ExpensifyNewKansas-MediumItalic.otf"; sourceTree = ""; }; - D3F458C994019E6A571461B7 /* Pods-NotificationServiceExtension.debugadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.debugadhoc.xcconfig"; path = "Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.debugadhoc.xcconfig"; sourceTree = ""; }; - DB76E0D5C670190A0997C71E /* Pods-NewExpensify-NewExpensifyTests.debug production.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.debug production.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.debug production.xcconfig"; sourceTree = ""; }; DCF33E34FFEC48128CDD41D4 /* ExpensifyMono-Bold.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyMono-Bold.otf"; path = "../assets/fonts/native/ExpensifyMono-Bold.otf"; sourceTree = ""; }; DD7904292792E76D004484B4 /* RCTBootSplash.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RCTBootSplash.h; path = NewExpensify/RCTBootSplash.h; sourceTree = ""; }; DD79042A2792E76D004484B4 /* RCTBootSplash.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RCTBootSplash.m; path = NewExpensify/RCTBootSplash.m; sourceTree = ""; }; - E2C8555C607612465A7473F8 /* Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig"; sourceTree = ""; }; - E2F1036F70CBFE39E9352674 /* Pods-NewExpensify-NewExpensifyTests.debug development.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.debug development.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.debug development.xcconfig"; sourceTree = ""; }; - E2F78D2A9B3DB96F0524690B /* Pods-NewExpensify-NewExpensifyTests.releaseproduction.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.releaseproduction.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.releaseproduction.xcconfig"; sourceTree = ""; }; - E61AD6D2DE65B6FB14945CDF /* Pods-NotificationServiceExtension.releaseadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.releaseadhoc.xcconfig"; path = "Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.releaseadhoc.xcconfig"; sourceTree = ""; }; - E681F80D97E6E4BB26194246 /* Pods-NewExpensify-NewExpensifyTests.releasedevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.releasedevelopment.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.releasedevelopment.xcconfig"; sourceTree = ""; }; E704648954784DDFBAADF568 /* ExpensifyMono-Regular.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyMono-Regular.otf"; path = "../assets/fonts/native/ExpensifyMono-Regular.otf"; sourceTree = ""; }; + E750C93A45B47BDC5149C5AA /* Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig"; sourceTree = ""; }; E9DF872C2525201700607FDC /* AirshipConfig.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = AirshipConfig.plist; sourceTree = ""; }; - EA58D43E81BC49541F7FC7E7 /* Pods-NewExpensify.debugdevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.debugdevelopment.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.debugdevelopment.xcconfig"; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; ED2971642150620600B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS12.0.sdk/System/Library/Frameworks/JavaScriptCore.framework; sourceTree = DEVELOPER_DIR; }; - F082D95EE104912B48EA98BA /* Pods-NotificationServiceExtension.releasedevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.releasedevelopment.xcconfig"; path = "Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.releasedevelopment.xcconfig"; sourceTree = ""; }; F0C450E92705020500FD2970 /* colors.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = colors.json; path = ../colors.json; sourceTree = ""; }; F4F8A052A22040339996324B /* ExpensifyNeue-Regular.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNeue-Regular.otf"; path = "../assets/fonts/native/ExpensifyNeue-Regular.otf"; sourceTree = ""; }; - FBEBA6FBED49FB41D6F93896 /* Pods-NotificationServiceExtension.debugdevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.debugdevelopment.xcconfig"; path = "Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.debugdevelopment.xcconfig"; sourceTree = ""; }; - FF0EADDA6099EF76253FA7AB /* Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig"; sourceTree = ""; }; + F98306ABF3F272DF04DF65CC /* Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -165,7 +148,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 3661A1374980E5F6804511FE /* libPods-NewExpensify-NewExpensifyTests.a in Frameworks */, + 716815DBCE9F49D420334791 /* libPods-NewExpensify-NewExpensifyTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -174,9 +157,8 @@ buildActionMask = 2147483647; files = ( E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */, - 976CCB5F8C921482E6AEAE71 /* libPods-NewExpensify.a in Frameworks */, E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */, - EEAE4F8907465429AA5B5520 /* libPods-NewExpensify.a in Frameworks */, + 5B8996A7D8B007ECC41919E1 /* libPods-NewExpensify.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -184,7 +166,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 059DC4EFD39EF39437E6823D /* libPods-NotificationServiceExtension.a in Frameworks */, + B8A1CD44D011AD7AE180DE14 /* libPods-NotificationServiceExtension.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -230,10 +212,9 @@ children = ( ED297162215061F000B7C4FE /* JavaScriptCore.framework */, ED2971642150620600B7C4FE /* JavaScriptCore.framework */, - AEFE6CD54912D427D19133C7 /* libPods-NewExpensify.a */, - 1A997AA8204EA3D90907FA80 /* libPods-NotificationServiceExtension.a */, - AB40AC8872A3DD6EF53D8B94 /* libPods-NewExpensify.a */, - 076FD9E41E08971BBF51D580 /* libPods-NewExpensify-NewExpensifyTests.a */, + 00D7E69E5ADD16FD4C44221B /* libPods-NewExpensify.a */, + B68AEB9429D8BB73F25A188C /* libPods-NewExpensify-NewExpensifyTests.a */, + B944D5699A54FD5197A63866 /* libPods-NotificationServiceExtension.a */, ); name = Frameworks; sourceTree = ""; @@ -338,39 +319,24 @@ EC29677F0A49C2946A495A33 /* Pods */ = { isa = PBXGroup; children = ( - CECC4CBB97A55705A33BEA9E /* Pods-NewExpensify.debug development.xcconfig */, - 8D3B36BF88E773E3C1A383FA /* Pods-NewExpensify.debug staging.xcconfig */, - 1DDE5449979A136852B939B5 /* Pods-NewExpensify.release adhoc.xcconfig */, - 75CABB0D0ABB0082FE0EB600 /* Pods-NewExpensify.release staging.xcconfig */, - 34A8FDD1F9AA58B8F15C8380 /* Pods-NewExpensify.release production.xcconfig */, - E2F1036F70CBFE39E9352674 /* Pods-NewExpensify-NewExpensifyTests.debug development.xcconfig */, - DB76E0D5C670190A0997C71E /* Pods-NewExpensify-NewExpensifyTests.debug production.xcconfig */, - BD6E1BA27D6ABE0AC9D70586 /* Pods-NewExpensify-NewExpensifyTests.release development.xcconfig */, - 96552D489D9F09B6A5ABD81B /* Pods-NewExpensify-NewExpensifyTests.release production.xcconfig */, - CE2F84BEE9A6DCC228AF7E42 /* Pods-NewExpensify.debugproduction.xcconfig */, - 30FFBD291B71222A393D9CC9 /* Pods-NewExpensify.releasedevelopment.xcconfig */, - BD8828A882E2D6B51362AAC3 /* Pods-NewExpensify.releaseadhoc.xcconfig */, - 8709DF3C8D91F0FC1581CDD7 /* Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig */, - 25A4587E168FD67CF890B448 /* Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig */, - E2C8555C607612465A7473F8 /* Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig */, - FBEBA6FBED49FB41D6F93896 /* Pods-NotificationServiceExtension.debugdevelopment.xcconfig */, - D3F458C994019E6A571461B7 /* Pods-NotificationServiceExtension.debugadhoc.xcconfig */, - C3FF914C045A138C061D306E /* Pods-NotificationServiceExtension.debugproduction.xcconfig */, - F082D95EE104912B48EA98BA /* Pods-NotificationServiceExtension.releasedevelopment.xcconfig */, - E61AD6D2DE65B6FB14945CDF /* Pods-NotificationServiceExtension.releaseadhoc.xcconfig */, - 90E08F0C8C924EDA018C8866 /* Pods-NotificationServiceExtension.releaseproduction.xcconfig */, - EA58D43E81BC49541F7FC7E7 /* Pods-NewExpensify.debugdevelopment.xcconfig */, - 7B318CF669A0F7FE948D5CED /* Pods-NewExpensify.debugadhoc.xcconfig */, - C3788801E65E896FA7C77298 /* Pods-NewExpensify.debugproduction.xcconfig */, - 76BE68DA894BB75DDFE278DC /* Pods-NewExpensify.releasedevelopment.xcconfig */, - 68F4F270A8D1414FC14F356F /* Pods-NewExpensify.releaseadhoc.xcconfig */, - 32181F72DC539FFD1D1F0CA4 /* Pods-NewExpensify.releaseproduction.xcconfig */, - 3BBA44B891E03FAB8255E6F1 /* Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig */, - 4E9593A0EE1C84B8A8EC062F /* Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig */, - 52E63EFD054926BFEA3EC143 /* Pods-NewExpensify-NewExpensifyTests.debugproduction.xcconfig */, - E681F80D97E6E4BB26194246 /* Pods-NewExpensify-NewExpensifyTests.releasedevelopment.xcconfig */, - FF0EADDA6099EF76253FA7AB /* Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig */, - E2F78D2A9B3DB96F0524690B /* Pods-NewExpensify-NewExpensifyTests.releaseproduction.xcconfig */, + 0FF2BC0D01CA2C62CE94229E /* Pods-NewExpensify.debugdevelopment.xcconfig */, + 5A1F158A9A6CBE170EC19D9C /* Pods-NewExpensify.debugadhoc.xcconfig */, + B4CF7147C89747459BAC5BB7 /* Pods-NewExpensify.releasedevelopment.xcconfig */, + 7312B334B72E8BE41A811FAB /* Pods-NewExpensify.releaseadhoc.xcconfig */, + 9EADA69D62F2E7B3D96E5B1C /* Pods-NewExpensify.releaseproduction.xcconfig */, + E750C93A45B47BDC5149C5AA /* Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig */, + F98306ABF3F272DF04DF65CC /* Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig */, + 972584042DB4782F830B063A /* Pods-NewExpensify-NewExpensifyTests.releasedevelopment.xcconfig */, + D15262BE5F713CDB4DA576AE /* Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig */, + 466D03D63F4B48E009C04FA3 /* Pods-NewExpensify-NewExpensifyTests.releaseproduction.xcconfig */, + 12EB734390650799DB8AC627 /* Pods-NotificationServiceExtension.debugdevelopment.xcconfig */, + 802CB9E7554756F188C79554 /* Pods-NotificationServiceExtension.debugadhoc.xcconfig */, + CB32BB7E082E2450F04DA6E7 /* Pods-NotificationServiceExtension.debugproduction.xcconfig */, + 11B2BA236BB72A603FBB7B99 /* Pods-NotificationServiceExtension.releasedevelopment.xcconfig */, + AC9422F6C8A49AE701481721 /* Pods-NotificationServiceExtension.releaseadhoc.xcconfig */, + 96ADA7C82BA6A08C4A56344A /* Pods-NotificationServiceExtension.releaseproduction.xcconfig */, + 3328C9B8861CA667C42B47F3 /* Pods-NewExpensify.debugproduction.xcconfig */, + 030F99CEB3AEF1F11B001798 /* Pods-NewExpensify-NewExpensifyTests.debugproduction.xcconfig */, ); path = Pods; sourceTree = ""; @@ -382,13 +348,13 @@ isa = PBXNativeTarget; buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "NewExpensifyTests" */; buildPhases = ( - A3D1E02743106A34295E533A /* [CP] Check Pods Manifest.lock */, + D128CAF2A1070B0F2B3F12E4 /* [CP] Check Pods Manifest.lock */, 04B99F6AA578E2A877802F05 /* [Expo] Configure project */, 00E356EA1AD99517003FC87E /* Sources */, 00E356EB1AD99517003FC87E /* Frameworks */, 00E356EC1AD99517003FC87E /* Resources */, - 822809AAD6B368BF9F9BA00E /* [CP] Embed Pods Frameworks */, - 5CC6761AF98472E1C710DB80 /* [CP] Copy Pods Resources */, + CE4668E6E6809DEA0626B5B3 /* [CP] Copy Pods Resources */, + 47778FAEB28E5E04DCCD0D39 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -404,7 +370,7 @@ isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "NewExpensify" */; buildPhases = ( - 468C095F6D4C79E555B55A4F /* [CP] Check Pods Manifest.lock */, + 5DDD2A7C43E9B381CD68A232 /* [CP] Check Pods Manifest.lock */, FD10A7F022414F080027D42C /* Start Packager */, 5CF45ABA52C0BB0D7B9D139A /* [Expo] Configure project */, 13B07F871A680F5B00A75B9A /* Sources */, @@ -412,10 +378,10 @@ 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, - CB8E29994749C6913C3FA05D /* [CP] Embed Pods Frameworks */, - F6E16E41F88F567A8CDD037C /* [CP] Copy Pods Resources */, - 04A2B3BE14CFE4961BE987E8 /* [CP-User] [RNFB] Core Configuration */, - 2D8F47B51A8E72FBA2BA4874 /* [CP-User] [RNFB] Crashlytics Configuration */, + 50067B6C26F88BAB5F0B478A /* [CP] Embed Pods Frameworks */, + 4E1386759AEE7859543483C9 /* [CP] Copy Pods Resources */, + EEA310C4723D0EF8581FAA1D /* [CP-User] [RNFB] Core Configuration */, + A9A9BDEB11952C562415526C /* [CP-User] [RNFB] Crashlytics Configuration */, ); buildRules = ( ); @@ -431,7 +397,7 @@ isa = PBXNativeTarget; buildConfigurationList = 7FD73CAA2B23CE9500420AF3 /* Build configuration list for PBXNativeTarget "NotificationServiceExtension" */; buildPhases = ( - F3D35ED760B830954BD8A7BB /* [CP] Check Pods Manifest.lock */, + 7B34459944ACFD30D85D5F85 /* [CP] Check Pods Manifest.lock */, 7FD73C972B23CE9500420AF3 /* Sources */, 7FD73C982B23CE9500420AF3 /* Frameworks */, 7FD73C992B23CE9500420AF3 /* Resources */, @@ -549,19 +515,6 @@ shellPath = /bin/sh; shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios relative | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli')\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n"; }; - 04A2B3BE14CFE4961BE987E8 /* [CP-User] [RNFB] Core Configuration */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", - ); - name = "[CP-User] [RNFB] Core Configuration"; - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nset -e\n\n_MAX_LOOKUPS=2;\n_SEARCH_RESULT=''\n_RN_ROOT_EXISTS=''\n_CURRENT_LOOKUPS=1\n_JSON_ROOT=\"'react-native'\"\n_JSON_FILE_NAME='firebase.json'\n_JSON_OUTPUT_BASE64='e30=' # { }\n_CURRENT_SEARCH_DIR=${PROJECT_DIR}\n_PLIST_BUDDY=/usr/libexec/PlistBuddy\n_TARGET_PLIST=\"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\"\n_DSYM_PLIST=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n# plist arrays\n_PLIST_ENTRY_KEYS=()\n_PLIST_ENTRY_TYPES=()\n_PLIST_ENTRY_VALUES=()\n\nfunction setPlistValue {\n echo \"info: setting plist entry '$1' of type '$2' in file '$4'\"\n ${_PLIST_BUDDY} -c \"Add :$1 $2 '$3'\" $4 || echo \"info: '$1' already exists\"\n}\n\nfunction getFirebaseJsonKeyValue () {\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n ruby -e \"require 'rubygems';require 'json'; output=JSON.parse('$1'); puts output[$_JSON_ROOT]['$2']\"\n else\n echo \"\"\n fi;\n}\n\nfunction jsonBoolToYesNo () {\n if [[ $1 == \"false\" ]]; then\n echo \"NO\"\n elif [[ $1 == \"true\" ]]; then\n echo \"YES\"\n else echo \"NO\"\n fi\n}\n\necho \"info: -> RNFB build script started\"\necho \"info: 1) Locating ${_JSON_FILE_NAME} file:\"\n\nif [[ -z ${_CURRENT_SEARCH_DIR} ]]; then\n _CURRENT_SEARCH_DIR=$(pwd)\nfi;\n\nwhile true; do\n _CURRENT_SEARCH_DIR=$(dirname \"$_CURRENT_SEARCH_DIR\")\n if [[ \"$_CURRENT_SEARCH_DIR\" == \"/\" ]] || [[ ${_CURRENT_LOOKUPS} -gt ${_MAX_LOOKUPS} ]]; then break; fi;\n echo \"info: ($_CURRENT_LOOKUPS of $_MAX_LOOKUPS) Searching in '$_CURRENT_SEARCH_DIR' for a ${_JSON_FILE_NAME} file.\"\n _SEARCH_RESULT=$(find \"$_CURRENT_SEARCH_DIR\" -maxdepth 2 -name ${_JSON_FILE_NAME} -print | /usr/bin/head -n 1)\n if [[ ${_SEARCH_RESULT} ]]; then\n echo \"info: ${_JSON_FILE_NAME} found at $_SEARCH_RESULT\"\n break;\n fi;\n _CURRENT_LOOKUPS=$((_CURRENT_LOOKUPS+1))\ndone\n\nif [[ ${_SEARCH_RESULT} ]]; then\n _JSON_OUTPUT_RAW=$(cat \"${_SEARCH_RESULT}\")\n _RN_ROOT_EXISTS=$(ruby -e \"require 'rubygems';require 'json'; output=JSON.parse('$_JSON_OUTPUT_RAW'); puts output[$_JSON_ROOT]\" || echo '')\n\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n _JSON_OUTPUT_BASE64=$(python -c 'import json,sys,base64;print(base64.b64encode(json.dumps(json.loads(open('\"'${_SEARCH_RESULT}'\"').read())['${_JSON_ROOT}'])))' || echo \"e30=\")\n fi\n\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n\n # config.app_data_collection_default_enabled\n _APP_DATA_COLLECTION_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_data_collection_default_enabled\")\n if [[ $_APP_DATA_COLLECTION_ENABLED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseDataCollectionDefaultEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_DATA_COLLECTION_ENABLED\")\")\n fi\n\n # config.analytics_auto_collection_enabled\n _ANALYTICS_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_auto_collection_enabled\")\n if [[ $_ANALYTICS_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_COLLECTION\")\")\n fi\n\n # config.analytics_collection_deactivated\n _ANALYTICS_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_collection_deactivated\")\n if [[ $_ANALYTICS_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_DEACTIVATED\")\")\n fi\n\n # config.analytics_idfv_collection_enabled\n _ANALYTICS_IDFV_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_idfv_collection_enabled\")\n if [[ $_ANALYTICS_IDFV_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_IDFV_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_IDFV_COLLECTION\")\")\n fi\n\n # config.analytics_default_allow_ad_personalization_signals\n _ANALYTICS_PERSONALIZATION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_personalization_signals\")\n if [[ $_ANALYTICS_PERSONALIZATION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_PERSONALIZATION_SIGNALS\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_PERSONALIZATION\")\")\n fi\n\n # config.perf_auto_collection_enabled\n _PERF_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_auto_collection_enabled\")\n if [[ $_PERF_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_enabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_AUTO_COLLECTION\")\")\n fi\n\n # config.perf_collection_deactivated\n _PERF_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_collection_deactivated\")\n if [[ $_PERF_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_deactivated\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_DEACTIVATED\")\")\n fi\n\n # config.messaging_auto_init_enabled\n _MESSAGING_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"messaging_auto_init_enabled\")\n if [[ $_MESSAGING_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseMessagingAutoInitEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_MESSAGING_AUTO_INIT\")\")\n fi\n\n # config.in_app_messaging_auto_colllection_enabled\n _FIAM_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"in_app_messaging_auto_collection_enabled\")\n if [[ $_FIAM_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseInAppMessagingAutomaticDataCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_FIAM_AUTO_INIT\")\")\n fi\n\n # config.app_check_token_auto_refresh\n _APP_CHECK_TOKEN_AUTO_REFRESH=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_check_token_auto_refresh\")\n if [[ $_APP_CHECK_TOKEN_AUTO_REFRESH ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAppCheckTokenAutoRefreshEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_CHECK_TOKEN_AUTO_REFRESH\")\")\n fi\n\n # config.crashlytics_disable_auto_disabler - undocumented for now - mainly for debugging, document if becomes useful\n _CRASHLYTICS_AUTO_DISABLE_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"crashlytics_disable_auto_disabler\")\n if [[ $_CRASHLYTICS_AUTO_DISABLE_ENABLED == \"true\" ]]; then\n echo \"Disabled Crashlytics auto disabler.\" # do nothing\n else\n _PLIST_ENTRY_KEYS+=(\"FirebaseCrashlyticsCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"NO\")\n fi\nelse\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n echo \"warning: A firebase.json file was not found, whilst this file is optional it is recommended to include it to configure firebase services in React Native Firebase.\"\nfi;\n\necho \"info: 2) Injecting Info.plist entries: \"\n\n# Log out the keys we're adding\nfor i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n echo \" -> $i) ${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\"\ndone\n\nfor plist in \"${_TARGET_PLIST}\" \"${_DSYM_PLIST}\" ; do\n if [[ -f \"${plist}\" ]]; then\n\n # paths with spaces break the call to setPlistValue. temporarily modify\n # the shell internal field separator variable (IFS), which normally\n # includes spaces, to consist only of line breaks\n oldifs=$IFS\n IFS=\"\n\"\n\n for i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n setPlistValue \"${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\" \"${plist}\"\n done\n\n # restore the original internal field separator value\n IFS=$oldifs\n else\n echo \"warning: A Info.plist build output file was not found (${plist})\"\n fi\ndone\n\necho \"info: <- RNFB build script finished\"\n"; - }; 04B99F6AA578E2A877802F05 /* [Expo] Configure project */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -581,49 +534,51 @@ shellPath = /bin/sh; shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-NewExpensify-NewExpensifyTests/expo-configure-project.sh\"\n"; }; - 2D8F47B51A8E72FBA2BA4874 /* [CP-User] [RNFB] Crashlytics Configuration */ = { + 47778FAEB28E5E04DCCD0D39 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}", - "$(SRCROOT)/$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", - ); - name = "[CP-User] [RNFB] Crashlytics Configuration"; - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nset -e\n\nif [[ ${PODS_ROOT} ]]; then\n echo \"info: Exec FirebaseCrashlytics Run from Pods\"\n \"${PODS_ROOT}/FirebaseCrashlytics/run\"\nelse\n echo \"info: Exec FirebaseCrashlytics Run from framework\"\n \"${PROJECT_DIR}/FirebaseCrashlytics.framework/run\"\nfi\n"; - }; - 468C095F6D4C79E555B55A4F /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/MapboxMaps/MapboxMaps.framework", + "${BUILT_PRODUCTS_DIR}/Turf/Turf.framework", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-DoubleConversion/double-conversion.framework/double-conversion", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-Glog/glog.framework/glog", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxCommon/MapboxCommon.framework/MapboxCommon", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxCoreMaps/MapboxCoreMaps.framework/MapboxCoreMaps", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxMobileEvents/MapboxMobileEvents.framework/MapboxMobileEvents", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/Onfido/Onfido.framework/Onfido", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/Plaid/LinkKit.framework/LinkKit", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", ); + name = "[CP] Embed Pods Frameworks"; outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-NewExpensify-checkManifestLockResult.txt", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxMaps.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Turf.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/double-conversion.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/glog.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxCommon.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxCoreMaps.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxMobileEvents.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Onfido.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/LinkKit.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 5CC6761AF98472E1C710DB80 /* [CP] Copy Pods Resources */ = { + 4E1386759AEE7859543483C9 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests-resources.sh", + "${PODS_ROOT}/Target Support Files/Pods-NewExpensify/Pods-NewExpensify-resources.sh", "${PODS_CONFIGURATION_BUILD_DIR}/Airship/AirshipAutomationResources.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/Airship/AirshipCoreResources.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/Airship/AirshipExtendedActionsResources.bundle", @@ -646,35 +601,16 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests-resources.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NewExpensify/Pods-NewExpensify-resources.sh\"\n"; showEnvVarsInLog = 0; }; - 5CF45ABA52C0BB0D7B9D139A /* [Expo] Configure project */ = { + 50067B6C26F88BAB5F0B478A /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - ); inputPaths = ( - ); - name = "[Expo] Configure project"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-NewExpensify/expo-configure-project.sh\"\n"; - }; - 822809AAD6B368BF9F9BA00E /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests-frameworks.sh", + "${PODS_ROOT}/Target Support Files/Pods-NewExpensify/Pods-NewExpensify-frameworks.sh", "${BUILT_PRODUCTS_DIR}/MapboxMaps/MapboxMaps.framework", "${BUILT_PRODUCTS_DIR}/Turf/Turf.framework", "${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-DoubleConversion/double-conversion.framework/double-conversion", @@ -703,70 +639,51 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NewExpensify/Pods-NewExpensify-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - A3D1E02743106A34295E533A /* [CP] Check Pods Manifest.lock */ = { + 5CF45ABA52C0BB0D7B9D139A /* [Expo] Configure project */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", ); - name = "[CP] Check Pods Manifest.lock"; + name = "[Expo] Configure project"; outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-NewExpensify-NewExpensifyTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; + shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-NewExpensify/expo-configure-project.sh\"\n"; }; - CB8E29994749C6913C3FA05D /* [CP] Embed Pods Frameworks */ = { + 5DDD2A7C43E9B381CD68A232 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-NewExpensify/Pods-NewExpensify-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/MapboxMaps/MapboxMaps.framework", - "${BUILT_PRODUCTS_DIR}/Turf/Turf.framework", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-DoubleConversion/double-conversion.framework/double-conversion", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-Glog/glog.framework/glog", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxCommon/MapboxCommon.framework/MapboxCommon", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxCoreMaps/MapboxCoreMaps.framework/MapboxCoreMaps", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxMobileEvents/MapboxMobileEvents.framework/MapboxMobileEvents", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/Onfido/Onfido.framework/Onfido", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/Plaid/LinkKit.framework/LinkKit", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( ); - name = "[CP] Embed Pods Frameworks"; outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxMaps.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Turf.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/double-conversion.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/glog.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxCommon.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxCoreMaps.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxMobileEvents.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Onfido.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/LinkKit.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", + "$(DERIVED_FILE_DIR)/Pods-NewExpensify-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NewExpensify/Pods-NewExpensify-frameworks.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - F3D35ED760B830954BD8A7BB /* [CP] Check Pods Manifest.lock */ = { + 7B34459944ACFD30D85D5F85 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -788,13 +705,27 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - F6E16E41F88F567A8CDD037C /* [CP] Copy Pods Resources */ = { + A9A9BDEB11952C562415526C /* [CP-User] [RNFB] Crashlytics Configuration */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-NewExpensify/Pods-NewExpensify-resources.sh", + "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}", + "$(SRCROOT)/$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", + ); + name = "[CP-User] [RNFB] Crashlytics Configuration"; + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nset -e\n\nif [[ ${PODS_ROOT} ]]; then\n echo \"info: Exec FirebaseCrashlytics Run from Pods\"\n \"${PODS_ROOT}/FirebaseCrashlytics/run\"\nelse\n echo \"info: Exec FirebaseCrashlytics Run from framework\"\n \"${PROJECT_DIR}/FirebaseCrashlytics.framework/run\"\nfi\n"; + }; + CE4668E6E6809DEA0626B5B3 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests-resources.sh", "${PODS_CONFIGURATION_BUILD_DIR}/Airship/AirshipAutomationResources.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/Airship/AirshipCoreResources.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/Airship/AirshipExtendedActionsResources.bundle", @@ -817,9 +748,44 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NewExpensify/Pods-NewExpensify-resources.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests-resources.sh\"\n"; showEnvVarsInLog = 0; }; + D128CAF2A1070B0F2B3F12E4 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-NewExpensify-NewExpensifyTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + EEA310C4723D0EF8581FAA1D /* [CP-User] [RNFB] Core Configuration */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", + ); + name = "[CP-User] [RNFB] Core Configuration"; + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nset -e\n\n_MAX_LOOKUPS=2;\n_SEARCH_RESULT=''\n_RN_ROOT_EXISTS=''\n_CURRENT_LOOKUPS=1\n_JSON_ROOT=\"'react-native'\"\n_JSON_FILE_NAME='firebase.json'\n_JSON_OUTPUT_BASE64='e30=' # { }\n_CURRENT_SEARCH_DIR=${PROJECT_DIR}\n_PLIST_BUDDY=/usr/libexec/PlistBuddy\n_TARGET_PLIST=\"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\"\n_DSYM_PLIST=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n# plist arrays\n_PLIST_ENTRY_KEYS=()\n_PLIST_ENTRY_TYPES=()\n_PLIST_ENTRY_VALUES=()\n\nfunction setPlistValue {\n echo \"info: setting plist entry '$1' of type '$2' in file '$4'\"\n ${_PLIST_BUDDY} -c \"Add :$1 $2 '$3'\" $4 || echo \"info: '$1' already exists\"\n}\n\nfunction getFirebaseJsonKeyValue () {\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n ruby -e \"require 'rubygems';require 'json'; output=JSON.parse('$1'); puts output[$_JSON_ROOT]['$2']\"\n else\n echo \"\"\n fi;\n}\n\nfunction jsonBoolToYesNo () {\n if [[ $1 == \"false\" ]]; then\n echo \"NO\"\n elif [[ $1 == \"true\" ]]; then\n echo \"YES\"\n else echo \"NO\"\n fi\n}\n\necho \"info: -> RNFB build script started\"\necho \"info: 1) Locating ${_JSON_FILE_NAME} file:\"\n\nif [[ -z ${_CURRENT_SEARCH_DIR} ]]; then\n _CURRENT_SEARCH_DIR=$(pwd)\nfi;\n\nwhile true; do\n _CURRENT_SEARCH_DIR=$(dirname \"$_CURRENT_SEARCH_DIR\")\n if [[ \"$_CURRENT_SEARCH_DIR\" == \"/\" ]] || [[ ${_CURRENT_LOOKUPS} -gt ${_MAX_LOOKUPS} ]]; then break; fi;\n echo \"info: ($_CURRENT_LOOKUPS of $_MAX_LOOKUPS) Searching in '$_CURRENT_SEARCH_DIR' for a ${_JSON_FILE_NAME} file.\"\n _SEARCH_RESULT=$(find \"$_CURRENT_SEARCH_DIR\" -maxdepth 2 -name ${_JSON_FILE_NAME} -print | /usr/bin/head -n 1)\n if [[ ${_SEARCH_RESULT} ]]; then\n echo \"info: ${_JSON_FILE_NAME} found at $_SEARCH_RESULT\"\n break;\n fi;\n _CURRENT_LOOKUPS=$((_CURRENT_LOOKUPS+1))\ndone\n\nif [[ ${_SEARCH_RESULT} ]]; then\n _JSON_OUTPUT_RAW=$(cat \"${_SEARCH_RESULT}\")\n _RN_ROOT_EXISTS=$(ruby -e \"require 'rubygems';require 'json'; output=JSON.parse('$_JSON_OUTPUT_RAW'); puts output[$_JSON_ROOT]\" || echo '')\n\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n _JSON_OUTPUT_BASE64=$(python -c 'import json,sys,base64;print(base64.b64encode(json.dumps(json.loads(open('\"'${_SEARCH_RESULT}'\"').read())['${_JSON_ROOT}'])))' || echo \"e30=\")\n fi\n\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n\n # config.app_data_collection_default_enabled\n _APP_DATA_COLLECTION_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_data_collection_default_enabled\")\n if [[ $_APP_DATA_COLLECTION_ENABLED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseDataCollectionDefaultEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_DATA_COLLECTION_ENABLED\")\")\n fi\n\n # config.analytics_auto_collection_enabled\n _ANALYTICS_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_auto_collection_enabled\")\n if [[ $_ANALYTICS_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_COLLECTION\")\")\n fi\n\n # config.analytics_collection_deactivated\n _ANALYTICS_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_collection_deactivated\")\n if [[ $_ANALYTICS_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_DEACTIVATED\")\")\n fi\n\n # config.analytics_idfv_collection_enabled\n _ANALYTICS_IDFV_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_idfv_collection_enabled\")\n if [[ $_ANALYTICS_IDFV_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_IDFV_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_IDFV_COLLECTION\")\")\n fi\n\n # config.analytics_default_allow_ad_personalization_signals\n _ANALYTICS_PERSONALIZATION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_personalization_signals\")\n if [[ $_ANALYTICS_PERSONALIZATION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_PERSONALIZATION_SIGNALS\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_PERSONALIZATION\")\")\n fi\n\n # config.perf_auto_collection_enabled\n _PERF_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_auto_collection_enabled\")\n if [[ $_PERF_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_enabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_AUTO_COLLECTION\")\")\n fi\n\n # config.perf_collection_deactivated\n _PERF_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_collection_deactivated\")\n if [[ $_PERF_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_deactivated\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_DEACTIVATED\")\")\n fi\n\n # config.messaging_auto_init_enabled\n _MESSAGING_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"messaging_auto_init_enabled\")\n if [[ $_MESSAGING_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseMessagingAutoInitEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_MESSAGING_AUTO_INIT\")\")\n fi\n\n # config.in_app_messaging_auto_colllection_enabled\n _FIAM_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"in_app_messaging_auto_collection_enabled\")\n if [[ $_FIAM_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseInAppMessagingAutomaticDataCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_FIAM_AUTO_INIT\")\")\n fi\n\n # config.app_check_token_auto_refresh\n _APP_CHECK_TOKEN_AUTO_REFRESH=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_check_token_auto_refresh\")\n if [[ $_APP_CHECK_TOKEN_AUTO_REFRESH ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAppCheckTokenAutoRefreshEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_CHECK_TOKEN_AUTO_REFRESH\")\")\n fi\n\n # config.crashlytics_disable_auto_disabler - undocumented for now - mainly for debugging, document if becomes useful\n _CRASHLYTICS_AUTO_DISABLE_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"crashlytics_disable_auto_disabler\")\n if [[ $_CRASHLYTICS_AUTO_DISABLE_ENABLED == \"true\" ]]; then\n echo \"Disabled Crashlytics auto disabler.\" # do nothing\n else\n _PLIST_ENTRY_KEYS+=(\"FirebaseCrashlyticsCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"NO\")\n fi\nelse\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n echo \"warning: A firebase.json file was not found, whilst this file is optional it is recommended to include it to configure firebase services in React Native Firebase.\"\nfi;\n\necho \"info: 2) Injecting Info.plist entries: \"\n\n# Log out the keys we're adding\nfor i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n echo \" -> $i) ${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\"\ndone\n\nfor plist in \"${_TARGET_PLIST}\" \"${_DSYM_PLIST}\" ; do\n if [[ -f \"${plist}\" ]]; then\n\n # paths with spaces break the call to setPlistValue. temporarily modify\n # the shell internal field separator variable (IFS), which normally\n # includes spaces, to consist only of line breaks\n oldifs=$IFS\n IFS=\"\n\"\n\n for i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n setPlistValue \"${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\" \"${plist}\"\n done\n\n # restore the original internal field separator value\n IFS=$oldifs\n else\n echo \"warning: A Info.plist build output file was not found (${plist})\"\n fi\ndone\n\necho \"info: <- RNFB build script finished\"\n"; + }; FD10A7F022414F080027D42C /* Start Packager */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -895,7 +861,7 @@ /* Begin XCBuildConfiguration section */ 00E356F61AD99517003FC87E /* DebugDevelopment */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 3BBA44B891E03FAB8255E6F1 /* Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig */; + baseConfigurationReference = E750C93A45B47BDC5149C5AA /* Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -925,7 +891,7 @@ }; 00E356F71AD99517003FC87E /* ReleaseDevelopment */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E681F80D97E6E4BB26194246 /* Pods-NewExpensify-NewExpensifyTests.releasedevelopment.xcconfig */; + baseConfigurationReference = 972584042DB4782F830B063A /* Pods-NewExpensify-NewExpensifyTests.releasedevelopment.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -952,7 +918,7 @@ }; 13B07F941A680F5B00A75B9A /* DebugDevelopment */ = { isa = XCBuildConfiguration; - baseConfigurationReference = EA58D43E81BC49541F7FC7E7 /* Pods-NewExpensify.debugdevelopment.xcconfig */; + baseConfigurationReference = 0FF2BC0D01CA2C62CE94229E /* Pods-NewExpensify.debugdevelopment.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIconDev; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; @@ -991,7 +957,7 @@ }; 13B07F951A680F5B00A75B9A /* ReleaseDevelopment */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 76BE68DA894BB75DDFE278DC /* Pods-NewExpensify.releasedevelopment.xcconfig */; + baseConfigurationReference = B4CF7147C89747459BAC5BB7 /* Pods-NewExpensify.releasedevelopment.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIconDev; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; @@ -1028,7 +994,7 @@ }; 7FD73CA42B23CE9500420AF3 /* DebugDevelopment */ = { isa = XCBuildConfiguration; - baseConfigurationReference = FBEBA6FBED49FB41D6F93896 /* Pods-NotificationServiceExtension.debugdevelopment.xcconfig */; + baseConfigurationReference = 12EB734390650799DB8AC627 /* Pods-NotificationServiceExtension.debugdevelopment.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -1114,7 +1080,7 @@ }; 7FD73CA52B23CE9500420AF3 /* DebugAdHoc */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D3F458C994019E6A571461B7 /* Pods-NotificationServiceExtension.debugadhoc.xcconfig */; + baseConfigurationReference = 802CB9E7554756F188C79554 /* Pods-NotificationServiceExtension.debugadhoc.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -1199,7 +1165,7 @@ }; 7FD73CA62B23CE9500420AF3 /* DebugProduction */ = { isa = XCBuildConfiguration; - baseConfigurationReference = C3FF914C045A138C061D306E /* Pods-NotificationServiceExtension.debugproduction.xcconfig */; + baseConfigurationReference = CB32BB7E082E2450F04DA6E7 /* Pods-NotificationServiceExtension.debugproduction.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -1285,7 +1251,7 @@ }; 7FD73CA72B23CE9500420AF3 /* ReleaseDevelopment */ = { isa = XCBuildConfiguration; - baseConfigurationReference = F082D95EE104912B48EA98BA /* Pods-NotificationServiceExtension.releasedevelopment.xcconfig */; + baseConfigurationReference = 11B2BA236BB72A603FBB7B99 /* Pods-NotificationServiceExtension.releasedevelopment.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -1364,7 +1330,7 @@ }; 7FD73CA82B23CE9500420AF3 /* ReleaseAdHoc */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E61AD6D2DE65B6FB14945CDF /* Pods-NotificationServiceExtension.releaseadhoc.xcconfig */; + baseConfigurationReference = AC9422F6C8A49AE701481721 /* Pods-NotificationServiceExtension.releaseadhoc.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -1442,7 +1408,7 @@ }; 7FD73CA92B23CE9500420AF3 /* ReleaseProduction */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 90E08F0C8C924EDA018C8866 /* Pods-NotificationServiceExtension.releaseproduction.xcconfig */; + baseConfigurationReference = 96ADA7C82BA6A08C4A56344A /* Pods-NotificationServiceExtension.releaseproduction.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -1737,7 +1703,7 @@ }; CF9AF93F29EE9276001FA527 /* DebugProduction */ = { isa = XCBuildConfiguration; - baseConfigurationReference = C3788801E65E896FA7C77298 /* Pods-NewExpensify.debugproduction.xcconfig */; + baseConfigurationReference = 3328C9B8861CA667C42B47F3 /* Pods-NewExpensify.debugproduction.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; @@ -1775,7 +1741,7 @@ }; CF9AF94029EE9276001FA527 /* DebugProduction */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 52E63EFD054926BFEA3EC143 /* Pods-NewExpensify-NewExpensifyTests.debugproduction.xcconfig */; + baseConfigurationReference = 030F99CEB3AEF1F11B001798 /* Pods-NewExpensify-NewExpensifyTests.debugproduction.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -1879,7 +1845,7 @@ }; CF9AF94529EE927A001FA527 /* DebugAdHoc */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7B318CF669A0F7FE948D5CED /* Pods-NewExpensify.debugadhoc.xcconfig */; + baseConfigurationReference = 5A1F158A9A6CBE170EC19D9C /* Pods-NewExpensify.debugadhoc.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIconAdHoc; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; @@ -1917,7 +1883,7 @@ }; CF9AF94629EE927A001FA527 /* DebugAdHoc */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 4E9593A0EE1C84B8A8EC062F /* Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig */; + baseConfigurationReference = F98306ABF3F272DF04DF65CC /* Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -2015,7 +1981,7 @@ }; CF9AF94829EE928E001FA527 /* ReleaseProduction */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 32181F72DC539FFD1D1F0CA4 /* Pods-NewExpensify.releaseproduction.xcconfig */; + baseConfigurationReference = 9EADA69D62F2E7B3D96E5B1C /* Pods-NewExpensify.releaseproduction.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; @@ -2051,7 +2017,7 @@ }; CF9AF94929EE928E001FA527 /* ReleaseProduction */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E2F78D2A9B3DB96F0524690B /* Pods-NewExpensify-NewExpensifyTests.releaseproduction.xcconfig */; + baseConfigurationReference = 466D03D63F4B48E009C04FA3 /* Pods-NewExpensify-NewExpensifyTests.releaseproduction.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -2147,7 +2113,7 @@ }; CF9AF94E29EE9293001FA527 /* ReleaseAdHoc */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 68F4F270A8D1414FC14F356F /* Pods-NewExpensify.releaseadhoc.xcconfig */; + baseConfigurationReference = 7312B334B72E8BE41A811FAB /* Pods-NewExpensify.releaseadhoc.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIconAdHoc; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; @@ -2183,7 +2149,7 @@ }; CF9AF94F29EE9293001FA527 /* ReleaseAdHoc */ = { isa = XCBuildConfiguration; - baseConfigurationReference = FF0EADDA6099EF76253FA7AB /* Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig */; + baseConfigurationReference = D15262BE5F713CDB4DA576AE /* Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 53bfdc978252..c45fa52877ec 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -32,6 +32,9 @@ PODS: - React-Core - CocoaAsyncSocket (7.6.5) - DoubleConversion (1.1.6) + - EXAV (13.10.4): + - ExpoModulesCore + - ReactCommon/turbomodule/core - Expo (50.0.4): - ExpoModulesCore - ExpoImage (1.10.1): @@ -1469,6 +1472,7 @@ DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - BVLinearGradient (from `../node_modules/react-native-linear-gradient`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) + - EXAV (from `../node_modules/expo-av/ios`) - Expo (from `../node_modules/expo`) - ExpoImage (from `../node_modules/expo-image/ios`) - ExpoModulesCore (from `../node_modules/expo-modules-core`) @@ -1650,6 +1654,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-linear-gradient" DoubleConversion: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" + EXAV: + :path: "../node_modules/expo-av/ios" Expo: :path: "../node_modules/expo" ExpoImage: @@ -1851,6 +1857,7 @@ SPEC CHECKSUMS: BVLinearGradient: 421743791a59d259aec53f4c58793aad031da2ca CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 DoubleConversion: fea03f2699887d960129cc54bba7e52542b6f953 + EXAV: 09a4d87fa6b113fbb0ada3aade6799f78271cb44 Expo: 1e3bcf9dd99de57a636127057f6b488f0609681a ExpoImage: 1cdaa65a6a70bb01067e21ad1347ff2d973885f5 ExpoModulesCore: 96d1751929ad10622773bb729ab28a8423f0dd0c diff --git a/package-lock.json b/package-lock.json index 009059d196a9..8dbd94f470eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,6 +54,7 @@ "domhandler": "^4.3.0", "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#a8ed0f8e1be3a1e09016e07a74cfd13c85bbc167", "expo": "^50.0.3", + "expo-av": "~13.10.4", "expo-image": "1.10.1", "fbjs": "^3.0.2", "htmlparser2": "^7.2.0", @@ -31354,6 +31355,14 @@ "md5-file": "^3.2.3" } }, + "node_modules/expo-av": { + "version": "13.10.4", + "resolved": "https://registry.npmjs.org/expo-av/-/expo-av-13.10.4.tgz", + "integrity": "sha512-Qlj+ILJ8wVK7J8DT9e6B1FyIcI3kM0iPhgmt7vnSqAWAbexOJRK7IP3MlKqKvHAhwm2OxIU3qJkShF7hvk20xA==", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-constants": { "version": "15.4.5", "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-15.4.5.tgz", diff --git a/package.json b/package.json index 21e9038acd96..e0e0d030cb4a 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "domhandler": "^4.3.0", "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#a8ed0f8e1be3a1e09016e07a74cfd13c85bbc167", "expo": "^50.0.3", + "expo-av": "~13.10.4", "expo-image": "1.10.1", "fbjs": "^3.0.2", "htmlparser2": "^7.2.0", diff --git a/patches/@react-native+assets-registry+0.73.1.patch b/patches/@react-native+assets-registry+0.73.1.patch new file mode 100644 index 000000000000..fc931b4ab52d --- /dev/null +++ b/patches/@react-native+assets-registry+0.73.1.patch @@ -0,0 +1,36 @@ +diff --git a/node_modules/@react-native/assets-registry/registry.js b/node_modules/@react-native/assets-registry/registry.js +index 02470da..3851d92 100644 +--- a/node_modules/@react-native/assets-registry/registry.js ++++ b/node_modules/@react-native/assets-registry/registry.js +@@ -10,28 +10,15 @@ + + 'use strict'; + +-export type PackagerAsset = { +- +__packager_asset: boolean, +- +fileSystemLocation: string, +- +httpServerLocation: string, +- +width: ?number, +- +height: ?number, +- +scales: Array, +- +hash: string, +- +name: string, +- +type: string, +- ... +-}; ++const assets = []; + +-const assets: Array = []; +- +-function registerAsset(asset: PackagerAsset): number { ++function registerAsset(asset) { + // `push` returns new array length, so the first asset will + // get id 1 (not 0) to make the value truthy + return assets.push(asset); + } + +-function getAssetByID(assetId: number): PackagerAsset { ++function getAssetByID(assetId) { + return assets[assetId - 1]; + } + diff --git a/src/App.tsx b/src/App.tsx index 52baedc79421..9562ea647e25 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -21,6 +21,9 @@ import SafeArea from './components/SafeArea'; import ThemeIllustrationsProvider from './components/ThemeIllustrationsProvider'; import ThemeProvider from './components/ThemeProvider'; import ThemeStylesProvider from './components/ThemeStylesProvider'; +import {PlaybackContextProvider} from './components/VideoPlayerContexts/PlaybackContext'; +import {VideoPopoverMenuContextProvider} from './components/VideoPlayerContexts/VideoPopoverMenuContext'; +import {VolumeContextProvider} from './components/VideoPlayerContexts/VolumeContext'; import {CurrentReportIDContextProvider} from './components/withCurrentReportID'; import {EnvironmentProvider} from './components/withEnvironment'; import {KeyboardStateProvider} from './components/withKeyboardState'; @@ -81,6 +84,9 @@ function App({url}: AppProps) { CustomStatusBarAndBackgroundContextProvider, ActiveElementRoleProvider, ActiveWorkspaceContextProvider, + PlaybackContextProvider, + VolumeContextProvider, + VideoPopoverMenuContextProvider, ]} > diff --git a/src/CONST.ts b/src/CONST.ts index 5c99c5877559..5ea1ee70a9d0 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -989,6 +989,10 @@ const CONST = { ATTACHMENT_PREVIEW_ATTRIBUTE: 'src', ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE: 'data-name', ATTACHMENT_LOCAL_URL_PREFIX: ['blob:', 'file:'], + ATTACHMENT_THUMBNAIL_URL_ATTRIBUTE: 'data-expensify-thumbnail-url', + ATTACHMENT_THUMBNAIL_WIDTH_ATTRIBUTE: 'data-expensify-width', + ATTACHMENT_THUMBNAIL_HEIGHT_ATTRIBUTE: 'data-expensify-height', + ATTACHMENT_DURATION_ATTRIBUTE: 'data-expensify-duration', ATTACHMENT_PICKER_TYPE: { FILE: 'file', @@ -3210,6 +3214,29 @@ const CONST = { REPORT: 'REPORT', }, + THUMBNAIL_IMAGE: { + SMALL_SCREEN: { + SIZE: 250, + }, + WIDE_SCREEN: { + SIZE: 350, + }, + NAN_ASPECT_RATIO: 1.5, + }, + + VIDEO_PLAYER: { + POPOVER_Y_OFFSET: -30, + PLAYBACK_SPEEDS: [0.25, 0.5, 1, 1.5, 2], + HIDE_TIME_TEXT_WIDTH: 250, + MIN_WIDTH: 170, + MIN_HEIGHT: 120, + CONTROLS_POSITION: { + NATIVE: 32, + NORMAL: 8, + }, + DEFAULT_VIDEO_DIMENSIONS: {width: 1900, height: 1400}, + }, + INTRO_CHOICES: { TRACK: 'newDotTrack', SUBMIT: 'newDotSubmit', diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx index 33409f855bec..47a481e19d79 100644 --- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx +++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import AttachmentView from '@components/Attachments/AttachmentView'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import {ShowContextMenuContext, showContextMenuForReport} from '@components/ShowContextMenuContext'; diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.js b/src/components/Attachments/AttachmentCarousel/CarouselItem.js index 878b4eef9539..edc8ab11fd27 100644 --- a/src/components/Attachments/AttachmentCarousel/CarouselItem.js +++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.js @@ -35,17 +35,25 @@ const propTypes = { /** The id of the transaction related to the attachment */ transactionID: PropTypes.string, + + duration: PropTypes.number, }).isRequired, /** onPress callback */ onPress: PropTypes.func, + + isModalHovered: PropTypes.bool, + + /** Whether the attachment is currently being viewed in the carousel */ + isFocused: PropTypes.bool.isRequired, }; const defaultProps = { onPress: undefined, + isModalHovered: false, }; -function CarouselItem({item, onPress}) { +function CarouselItem({item, onPress, isFocused, isModalHovered}) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {isAttachmentHidden} = useContext(ReportAttachmentsContext); @@ -97,6 +105,9 @@ function CarouselItem({item, onPress}) { isAuthTokenRequired={item.isAuthTokenRequired} onPress={onPress} transactionID={item.transactionID} + isHovered={isModalHovered} + isFocused={isFocused} + optionalVideoDuration={item.duration} /> diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js b/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js index 6994a7a210bf..b934bdfdd738 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js +++ b/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js @@ -18,24 +18,36 @@ function extractAttachmentsFromReport(parentReportAction, reportActions) { const htmlParser = new HtmlParser({ onopentag: (name, attribs) => { - if (name !== 'img' || !attribs.src) { + if (name === 'video') { + const splittedUrl = attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE].split('/'); + attachments.unshift({ + reportActionID: null, + source: tryResolveUrlFromApiRoot(attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]), + isAuthTokenRequired: Boolean(attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]), + file: {name: splittedUrl[splittedUrl.length - 1]}, + duration: Number(attribs[CONST.ATTACHMENT_DURATION_ATTRIBUTE]), + isReceipt: false, + hasBeenFlagged: false, + }); return; } - const expensifySource = attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]; - const source = tryResolveUrlFromApiRoot(expensifySource || attribs.src); - const fileName = attribs[CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE] || FileUtils.getFileName(`${source}`); - - // By iterating actions in chronological order and prepending each attachment - // we ensure correct order of attachments even across actions with multiple attachments. - attachments.unshift({ - reportActionID: attribs['data-id'], - source, - isAuthTokenRequired: Boolean(expensifySource), - file: {name: fileName}, - isReceipt: false, - hasBeenFlagged: attribs['data-flagged'] === 'true', - }); + if (name === 'img' && attribs.src) { + const expensifySource = attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]; + const source = tryResolveUrlFromApiRoot(expensifySource || attribs.src); + const fileName = attribs[CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE] || FileUtils.getFileName(`${source}`); + + // By iterating actions in chronological order and prepending each attachment + // we ensure correct order of attachments even across actions with multiple attachments. + attachments.unshift({ + reportActionID: attribs['data-id'], + source, + isAuthTokenRequired: Boolean(expensifySource), + file: {name: fileName}, + isReceipt: false, + hasBeenFlagged: attribs['data-flagged'] === 'true', + }); + } }, }); diff --git a/src/components/Attachments/AttachmentCarousel/index.js b/src/components/Attachments/AttachmentCarousel/index.js index 9be1322e5ba8..ef6a11f6e67c 100644 --- a/src/components/Attachments/AttachmentCarousel/index.js +++ b/src/components/Attachments/AttachmentCarousel/index.js @@ -49,6 +49,10 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, const initialPage = _.findIndex(attachmentsFromReport, compareImage); + if (_.isEqual(attachments, attachmentsFromReport)) { + return; + } + // Dismiss the modal when deleting an attachment during its display in preview. if (initialPage === -1 && _.find(attachments, compareImage)) { Navigation.dismissModal(); @@ -64,8 +68,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate(attachmentsFromReport[initialPage]); } } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [reportActions, parentReportActions, compareImage]); + }, [attachments, reportActions, parentReportActions, compareImage, report.parentReportActionID, setDownloadButtonVisibility, onNavigate]); /** * Updates the page state when the user navigates between attachments @@ -137,15 +140,18 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, * @returns {JSX.Element} */ const renderItem = useCallback( - ({item}) => ( + ({item, index}) => ( setShouldShowArrows(!shouldShowArrows) : undefined} + onPress={canUseTouchScreen ? () => setShouldShowArrows((oldState) => !oldState) : undefined} + isModalHovered={shouldShowArrows} + index={index} + activeIndex={page} /> ), - [activeSource, attachments.length, canUseTouchScreen, setShouldShowArrows, shouldShowArrows], + [activeSource, attachments.length, canUseTouchScreen, page, setShouldShowArrows, shouldShowArrows], ); return ( diff --git a/src/components/Attachments/AttachmentView/AttachmentViewVideo/index.js b/src/components/Attachments/AttachmentView/AttachmentViewVideo/index.js new file mode 100644 index 000000000000..2b71e799beed --- /dev/null +++ b/src/components/Attachments/AttachmentView/AttachmentViewVideo/index.js @@ -0,0 +1,44 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import VideoPlayer from '@components/VideoPlayer'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; + +const propTypes = { + /** Video file source URL */ + source: PropTypes.string.isRequired, + + /** Whether the video is currently being hovered over */ + isHovered: PropTypes.bool, + + shouldUseSharedVideoElement: PropTypes.bool, + + videoDuration: PropTypes.number, +}; + +const defaultProps = { + isHovered: false, + shouldUseSharedVideoElement: false, + videoDuration: 0, +}; + +function AttachmentViewVideo({source, isHovered, shouldUseSharedVideoElement, videoDuration}) { + const {isSmallScreen} = useWindowDimensions(); + const styles = useThemeStyles(); + + return ( + + ); +} + +AttachmentViewVideo.propTypes = propTypes; +AttachmentViewVideo.defaultProps = defaultProps; +AttachmentViewVideo.displayName = 'AttachmentViewVideo'; + +export default React.memo(AttachmentViewVideo); diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js index fafdbd27d844..d02957aa8137 100755 --- a/src/components/Attachments/AttachmentView/index.js +++ b/src/components/Attachments/AttachmentView/index.js @@ -1,6 +1,6 @@ import Str from 'expensify-common/lib/str'; import PropTypes from 'prop-types'; -import React, {memo, useState} from 'react'; +import React, {memo, useEffect, useState} from 'react'; import {ActivityIndicator, ScrollView, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -11,6 +11,7 @@ import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; +import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -23,6 +24,7 @@ import variables from '@styles/variables'; import ONYXKEYS from '@src/ONYXKEYS'; import AttachmentViewImage from './AttachmentViewImage'; import AttachmentViewPdf from './AttachmentViewPdf'; +import AttachmentViewVideo from './AttachmentViewVideo'; import {attachmentViewDefaultProps, attachmentViewPropTypes} from './propTypes'; const propTypes = { @@ -54,6 +56,10 @@ const propTypes = { /** The id of the transaction related to the attachment */ // eslint-disable-next-line react/no-unused-prop-types transactionID: PropTypes.string, + + isHovered: PropTypes.bool, + + optionalVideoDuration: PropTypes.number, }; const defaultProps = { @@ -65,6 +71,8 @@ const defaultProps = { isWorkspaceAvatar: false, maybeIcon: false, transactionID: '', + isHovered: false, + optionalVideoDuration: 0, }; function AttachmentView({ @@ -84,11 +92,23 @@ function AttachmentView({ maybeIcon, fallbackSource, transaction, + isHovered, + optionalVideoDuration, }) { + const {updateCurrentlyPlayingURL} = usePlaybackContext(); const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const [loadComplete, setLoadComplete] = useState(false); + const isVideo = Str.isVideo(source); + + useEffect(() => { + if (!isFocused) { + return; + } + updateCurrentlyPlayingURL(isVideo ? source : null); + }, [isFocused, isVideo, source, updateCurrentlyPlayingURL]); + const [imageError, setImageError] = useState(false); useNetwork({onReconnect: () => setImageError(false)}); @@ -181,6 +201,17 @@ function AttachmentView({ ); } + if (isVideo || (file && Str.isVideo(file.name))) { + return ( + + ); + } + return ( diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx index ca0e81ae9255..863fe6fbabb1 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx @@ -1,3 +1,4 @@ +import Str from 'expensify-common/lib/str'; import React from 'react'; import {TNodeChildrenRenderer} from 'react-native-render-html'; import type {CustomRendererProps, TBlock} from 'react-native-render-html'; @@ -28,6 +29,7 @@ function AnchorRenderer({tnode, style, key}: AnchorRendererProps) { const attrHref = htmlAttribs.href || htmlAttribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE] || ''; const internalNewExpensifyPath = Link.getInternalNewExpensifyPath(attrHref); const internalExpensifyPath = Link.getInternalExpensifyPath(attrHref); + const isVideo = attrHref && Str.isVideo(attrHref); if (!HTMLEngineUtils.isChildOfComment(tnode)) { // This is not a comment from a chat, the AnchorForCommentsOnly uses a Pressable to create a context menu on right click. @@ -44,7 +46,7 @@ function AnchorRenderer({tnode, style, key}: AnchorRendererProps) { ); } - if (isAttachment) { + if (isAttachment && !isVideo) { return ( & { + /** Key of the element */ + key?: string; +}; + +function VideoRenderer({tnode, key}: VideoRendererProps) { + const htmlAttribs = tnode.attributes; + const attrHref = htmlAttribs.href || htmlAttribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE] || ''; + const sourceURL = tryResolveUrlFromApiRoot(attrHref); + const fileName = FileUtils.getFileName(`${sourceURL}`); + const thumbnailUrl = htmlAttribs[CONST.ATTACHMENT_THUMBNAIL_URL_ATTRIBUTE]; + const width = Number(htmlAttribs[CONST.ATTACHMENT_THUMBNAIL_WIDTH_ATTRIBUTE]); + const height = Number(htmlAttribs[CONST.ATTACHMENT_THUMBNAIL_HEIGHT_ATTRIBUTE]); + const duration = Number(htmlAttribs[CONST.ATTACHMENT_DURATION_ATTRIBUTE]); + const activeRoute = Navigation.getActiveRoute(); + const {reportID} = parseReportRouteParams(activeRoute); + + return ( + { + const route = ROUTES.REPORT_ATTACHMENTS.getRoute(reportID, sourceURL); + Navigation.navigate(route); + }} + /> + ); +} + +VideoRenderer.displayName = 'VideoRenderer'; + +export default VideoRenderer; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts index f2c8cbe89a98..1914bcf4b5ff 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts @@ -7,6 +7,7 @@ import MentionHereRenderer from './MentionHereRenderer'; import MentionUserRenderer from './MentionUserRenderer'; import NextStepEmailRenderer from './NextStepEmailRenderer'; import PreRenderer from './PreRenderer'; +import VideoRenderer from './VideoRenderer'; /** * This collection defines our custom renderers. It is a mapping from HTML tag type to the corresponding component. @@ -16,7 +17,7 @@ const HTMLEngineProviderComponentList: CustomTagRendererRecord = { a: AnchorRenderer, code: CodeRenderer, img: ImageRenderer, - video: AnchorRenderer, // temporary until we have a video player component + video: VideoRenderer, // Custom tag renderers edited: EditedRenderer, diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index 58b936d8b082..06619e2e215f 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -67,6 +67,7 @@ import Flag from '@assets/images/flag.svg'; import FlagLevelOne from '@assets/images/flag_level_01.svg'; import FlagLevelTwo from '@assets/images/flag_level_02.svg'; import FlagLevelThree from '@assets/images/flag_level_03.svg'; +import Fullscreen from '@assets/images/fullscreen.svg'; import Gallery from '@assets/images/gallery.svg'; import Gear from '@assets/images/gear.svg'; import Globe from '@assets/images/globe.svg'; @@ -90,19 +91,23 @@ import MagnifyingGlass from '@assets/images/magnifying-glass.svg'; import Mail from '@assets/images/mail.svg'; import Megaphone from '@assets/images/megaphone.svg'; import Menu from '@assets/images/menu.svg'; +import Meter from '@assets/images/meter.svg'; import MoneyBag from '@assets/images/money-bag.svg'; import MoneyCircle from '@assets/images/money-circle.svg'; import Monitor from '@assets/images/monitor.svg'; +import Mute from '@assets/images/mute.svg'; import NewWindow from '@assets/images/new-window.svg'; import NewWorkspace from '@assets/images/new-workspace.svg'; import OfflineCloud from '@assets/images/offline-cloud.svg'; import Offline from '@assets/images/offline.svg'; import OldDotWireframe from '@assets/images/olddot-wireframe.svg'; import Paperclip from '@assets/images/paperclip.svg'; +import Pause from '@assets/images/pause.svg'; import Paycheck from '@assets/images/paycheck.svg'; import Pencil from '@assets/images/pencil.svg'; import Phone from '@assets/images/phone.svg'; import Pin from '@assets/images/pin.svg'; +import Play from '@assets/images/play.svg'; import Plus from '@assets/images/plus.svg'; import Printer from '@assets/images/printer.svg'; import Profile from '@assets/images/profile.svg'; @@ -134,6 +139,8 @@ import UploadAlt from '@assets/images/upload-alt.svg'; import Upload from '@assets/images/upload.svg'; import User from '@assets/images/user.svg'; import Users from '@assets/images/users.svg'; +import VolumeHigh from '@assets/images/volume-high.svg'; +import VolumeLow from '@assets/images/volume-low.svg'; import Wallet from '@assets/images/wallet.svg'; import Workspace from '@assets/images/workspace-default-avatar.svg'; import Wrench from '@assets/images/wrench.svg'; @@ -207,6 +214,7 @@ export { FlagLevelOne, FlagLevelTwo, FlagLevelThree, + Fullscreen, Gallery, Gear, Globe, @@ -231,10 +239,12 @@ export { MagnifyingGlass, Mail, Menu, + Meter, Megaphone, MoneyBag, MoneyCircle, Monitor, + Mute, ExpensifyLogoNew, NewWindow, NewWorkspace, @@ -242,10 +252,12 @@ export { OfflineCloud, OldDotWireframe, Paperclip, + Pause, Paycheck, Pencil, Phone, Pin, + Play, Plus, Printer, Profile, @@ -270,6 +282,8 @@ export { UploadAlt, User, Users, + VolumeHigh, + VolumeLow, Wallet, Workspace, Zoom, diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index cfb7cffb2799..8fc9c62bfb38 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -234,6 +234,9 @@ type MenuItemBaseProps = { /** Is this in the Pane */ isPaneMenu?: boolean; + + /** Adds padding to the left of the text when there is no icon. */ + shouldPutLeftPaddingWhenNoIcon?: boolean; }; type MenuItemProps = (IconProps | AvatarProps | NoIcon) & MenuItemBaseProps; @@ -301,6 +304,7 @@ function MenuItem( displayInDefaultIconColor = false, contentFit = 'cover', isPaneMenu = false, + shouldPutLeftPaddingWhenNoIcon = false, }: MenuItemProps, ref: ForwardedRef, ) { @@ -321,7 +325,7 @@ function MenuItem( styles.flexShrink1, styles.popoverMenuText, // eslint-disable-next-line no-nested-ternary - icon && !Array.isArray(icon) ? (avatarSize === CONST.AVATAR_SIZE.SMALL ? styles.ml2 : styles.ml3) : {}, + shouldPutLeftPaddingWhenNoIcon || (icon && !Array.isArray(icon)) ? (avatarSize === CONST.AVATAR_SIZE.SMALL ? styles.ml2 : styles.ml3) : {}, shouldShowBasicTitle ? {} : styles.textStrong, numberOfLinesTitle !== 1 ? styles.preWrap : styles.pre, interactive && disabled ? {...styles.userSelectNone} : {}, @@ -448,6 +452,7 @@ function MenuItem( ]} /> )} + {!icon && shouldPutLeftPaddingWhenNoIcon && } {icon && !Array.isArray(icon) && ( {typeof icon !== 'string' && iconType === CONST.ICON_TYPE_ICON && ( diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index 34391933da32..5310163a7433 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -1,6 +1,6 @@ import type {ImageContentFit} from 'expo-image'; import type {RefObject} from 'react'; -import React, {useRef} from 'react'; +import React, {useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; import type {ModalProps} from 'react-native-modal'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; @@ -11,6 +11,7 @@ import CONST from '@src/CONST'; import type {AnchorPosition} from '@src/styles'; import type AnchorAlignment from '@src/types/utils/AnchorAlignment'; import type IconAsset from '@src/types/utils/IconAsset'; +import * as Expensicons from './Icon/Expensicons'; import MenuItem from './MenuItem'; import PopoverWithMeasuredContent from './PopoverWithMeasuredContent'; import Text from './Text'; @@ -42,6 +43,15 @@ type PopoverMenuItem = { /** Determines how the icon should be resized to fit its container */ contentFit?: ImageContentFit; + + /** Sub menu items to be rendered after a menu item is selected */ + subMenuItems?: PopoverMenuItem[]; + + /** Determines whether an icon should be displayed on the right side of the menu item. */ + shouldShowRightIcon?: boolean; + + /** Adds padding to the left of the text when there is no icon. */ + shouldPutLeftPaddingWhenNoIcon?: boolean; }; type PopoverModalProps = Pick; @@ -107,12 +117,53 @@ function PopoverMenu({ const styles = useThemeStyles(); const {isSmallScreenWidth} = useWindowDimensions(); const selectedItemIndex = useRef(null); - const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({initialFocusedIndex: -1, maxIndex: menuItems.length - 1, isActive: isVisible}); + + const [currentMenuItems, setCurrentMenuItems] = useState(menuItems); + const [enteredSubMenuIndexes, setEnteredSubMenuIndexes] = useState([]); + + const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({initialFocusedIndex: -1, maxIndex: currentMenuItems.length - 1, isActive: isVisible}); const selectItem = (index: number) => { - const selectedItem = menuItems[index]; - onItemSelected(selectedItem, index); - selectedItemIndex.current = index; + const selectedItem = currentMenuItems[index]; + if (selectedItem?.subMenuItems) { + setCurrentMenuItems([...selectedItem.subMenuItems]); + setEnteredSubMenuIndexes([...enteredSubMenuIndexes, index]); + } else { + onItemSelected(selectedItem, index); + selectedItemIndex.current = index; + } + }; + + const getPreviousSubMenu = () => { + let currentItems = menuItems; + for (let i = 0; i < enteredSubMenuIndexes.length - 1; i++) { + const nextItems = currentItems[enteredSubMenuIndexes[i]].subMenuItems; + if (!nextItems) { + return currentItems; + } + currentItems = nextItems; + } + return currentItems; + }; + + const renderBackButtonItem = () => { + const previousMenuItems = getPreviousSubMenu(); + const previouslySelectedItem = previousMenuItems[enteredSubMenuIndexes[enteredSubMenuIndexes.length - 1]]; + + return ( + { + setCurrentMenuItems(previousMenuItems); + enteredSubMenuIndexes.splice(-1); + }} + /> + ); }; useKeyboardShortcut( @@ -130,17 +181,29 @@ function PopoverMenu({ const onModalHide = () => { setFocusedIndex(-1); if (selectedItemIndex.current !== null) { - menuItems[selectedItemIndex.current].onSelected(); + currentMenuItems[selectedItemIndex.current].onSelected(); selectedItemIndex.current = null; } }; + useEffect(() => { + if (menuItems.length === 0) { + return; + } + setEnteredSubMenuIndexes([]); + setCurrentMenuItems(menuItems); + }, [menuItems]); + return ( { + setCurrentMenuItems(menuItems); + setEnteredSubMenuIndexes([]); + onClose(); + }} isVisible={isVisible} onModalHide={onModalHide} animationIn={animationIn} @@ -153,7 +216,8 @@ function PopoverMenu({ > {!!headerText && {headerText}} - {menuItems.map((item, menuIndex) => ( + {enteredSubMenuIndexes.length > 0 && renderBackButtonItem()} + {currentMenuItems.map((item, menuIndex) => ( selectItem(menuIndex)} focused={focusedIndex === menuIndex} displayInDefaultIconColor={item.displayInDefaultIconColor} + shouldShowRightIcon={item.shouldShowRightIcon} + shouldPutLeftPaddingWhenNoIcon={item.shouldPutLeftPaddingWhenNoIcon} /> ))} diff --git a/src/components/PopoverWithoutOverlay/index.tsx b/src/components/PopoverWithoutOverlay/index.tsx index a87e1f1f0412..71fedf0e6a01 100644 --- a/src/components/PopoverWithoutOverlay/index.tsx +++ b/src/components/PopoverWithoutOverlay/index.tsx @@ -7,6 +7,7 @@ import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import variables from '@styles/variables'; import * as Modal from '@userActions/Modal'; import viewRef from '@src/types/utils/viewRef'; import type PopoverWithoutOverlayProps from './types'; @@ -119,7 +120,7 @@ function PopoverWithoutOverlay( return ( e.stopPropagation()} diff --git a/src/components/ThumbnailImage.tsx b/src/components/ThumbnailImage.tsx index 6f7369aaee93..5c5932c5814b 100644 --- a/src/components/ThumbnailImage.tsx +++ b/src/components/ThumbnailImage.tsx @@ -1,13 +1,10 @@ -import lodashClamp from 'lodash/clamp'; import React, {useCallback, useEffect, useState} from 'react'; import type {ImageSourcePropType, StyleProp, ViewStyle} from 'react-native'; -import {Dimensions, View} from 'react-native'; +import {View} from 'react-native'; import useNetwork from '@hooks/useNetwork'; -import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import useThumbnailDimensions from '@hooks/useThumbnailDimensions'; import variables from '@styles/variables'; import type IconAsset from '@src/types/utils/IconAsset'; import Icon from './Icon'; @@ -45,44 +42,6 @@ type UpdateImageSizeParams = { height: number; }; -type CalculateThumbnailImageSizeResult = { - thumbnailWidth?: number; - thumbnailHeight?: number; -}; - -/** - * Compute the thumbnails width and height given original image dimensions. - * - * @param width - Width of the original image. - * @param height - Height of the original image. - * @param windowHeight - Height of the device/browser window. - * @returns - Object containing thumbnails width and height. - */ - -function calculateThumbnailImageSize(width: number, height: number, windowHeight: number): CalculateThumbnailImageSizeResult { - if (!width || !height) { - return {}; - } - // Width of the thumbnail works better as a constant than it does - // a percentage of the screen width since it is relative to each screen - // Note: Clamp minimum width 40px to support touch device - let thumbnailScreenWidth = lodashClamp(width, 40, 250); - const imageHeight = height / (width / thumbnailScreenWidth); - // On mWeb, when soft keyboard opens, window height changes, making thumbnail height inconsistent. We use screen height instead. - const screenHeight = DeviceCapabilities.canUseTouchScreen() ? Dimensions.get('screen').height : windowHeight; - let thumbnailScreenHeight = lodashClamp(imageHeight, 40, screenHeight * 0.4); - const aspectRatio = height / width; - - // If thumbnail height is greater than its width, then the image is portrait otherwise landscape. - // For portrait images, we need to adjust the width of the image to keep the aspect ratio and vice-versa. - if (thumbnailScreenHeight > thumbnailScreenWidth) { - thumbnailScreenWidth = Math.round(thumbnailScreenHeight * (1 / aspectRatio)); - } else { - thumbnailScreenHeight = Math.round(thumbnailScreenWidth * aspectRatio); - } - return {thumbnailWidth: Math.max(40, thumbnailScreenWidth), thumbnailHeight: Math.max(40, thumbnailScreenHeight)}; -} - function ThumbnailImage({ previewSourceURL, style, @@ -95,13 +54,10 @@ function ThumbnailImage({ }: ThumbnailImageProps) { const styles = useThemeStyles(); const theme = useTheme(); - const StyleUtils = useStyleUtils(); const {isOffline} = useNetwork(); - const {windowHeight} = useWindowDimensions(); - const initialDimensions = calculateThumbnailImageSize(imageWidth, imageHeight, windowHeight); - const [currentImageWidth, setCurrentImageWidth] = useState(initialDimensions.thumbnailWidth); - const [currentImageHeight, setCurrentImageHeight] = useState(initialDimensions.thumbnailHeight); const [failedToLoad, setFailedToLoad] = useState(false); + const [imageDimensions, setImageDimensions] = useState({width: imageWidth, height: imageHeight}); + const {thumbnailDimensionsStyles} = useThumbnailDimensions(imageDimensions.width, imageDimensions.height); useEffect(() => { setFailedToLoad(false); @@ -113,15 +69,15 @@ function ThumbnailImage({ */ const updateImageSize = useCallback( ({width, height}: UpdateImageSizeParams) => { - const {thumbnailWidth, thumbnailHeight} = calculateThumbnailImageSize(width, height, windowHeight); - - setCurrentImageWidth(thumbnailWidth); - setCurrentImageHeight(thumbnailHeight); + if (!shouldDynamicallyResize) { + return; + } + setImageDimensions({width, height}); }, - [windowHeight], + [shouldDynamicallyResize], ); - const sizeStyles = shouldDynamicallyResize ? [StyleUtils.getWidthAndHeightStyle(currentImageWidth ?? 0, currentImageHeight)] : [styles.w100, styles.h100]; + const sizeStyles = shouldDynamicallyResize ? [thumbnailDimensionsStyles] : [styles.w100, styles.h100]; if (failedToLoad) { return ( diff --git a/src/components/Tooltip/BaseTooltip/index.tsx b/src/components/Tooltip/BaseTooltip/index.tsx index 2487cbbfe092..5f6c38d19bff 100644 --- a/src/components/Tooltip/BaseTooltip/index.tsx +++ b/src/components/Tooltip/BaseTooltip/index.tsx @@ -67,6 +67,7 @@ function Tooltip( shouldHandleScroll = false, shiftHorizontal = 0, shiftVertical = 0, + shouldForceRenderingBelow = false, }: TooltipProps, ref: ForwardedRef, ) { @@ -220,6 +221,7 @@ function Tooltip( // We pass a key, so whenever the content changes this component will completely remount with a fresh state. // This prevents flickering/moving while remaining performant. key={[text, ...renderTooltipContentKey, preferredLocale].join('-')} + shouldForceRenderingBelow={shouldForceRenderingBelow} /> )} diff --git a/src/components/Tooltip/TooltipRenderedOnPageBody.tsx b/src/components/Tooltip/TooltipRenderedOnPageBody.tsx index 5ffd8596e6de..0e97c2463532 100644 --- a/src/components/Tooltip/TooltipRenderedOnPageBody.tsx +++ b/src/components/Tooltip/TooltipRenderedOnPageBody.tsx @@ -34,7 +34,7 @@ type TooltipRenderedOnPageBodyProps = { /** Any additional amount to manually adjust the vertical position of the tooltip. A positive value shifts the tooltip down, and a negative value shifts it up. */ shiftVertical?: number; -} & Pick; +} & Pick; // Props will change frequently. // On every tooltip hover, we update the position in state which will result in re-rendering. @@ -54,6 +54,7 @@ function TooltipRenderedOnPageBody({ numberOfLines, maxWidth = 0, renderTooltipContent, + shouldForceRenderingBelow = false, }: TooltipRenderedOnPageBodyProps) { // The width of tooltip's inner content. Has to be undefined in the beginning // as a width of 0 will cause the content to be rendered of a width of 0, @@ -95,8 +96,23 @@ function TooltipRenderedOnPageBody({ tooltipWrapperHeight: wrapperMeasuredHeight, manualShiftHorizontal: shiftHorizontal, manualShiftVertical: shiftVertical, + shouldForceRenderingBelow, }), - [StyleUtils, animation, windowWidth, xOffset, yOffset, targetWidth, targetHeight, maxWidth, contentMeasuredWidth, wrapperMeasuredHeight, shiftHorizontal, shiftVertical], + [ + StyleUtils, + animation, + windowWidth, + xOffset, + yOffset, + targetWidth, + targetHeight, + maxWidth, + contentMeasuredWidth, + wrapperMeasuredHeight, + shiftHorizontal, + shiftVertical, + shouldForceRenderingBelow, + ], ); let content; diff --git a/src/components/Tooltip/types.ts b/src/components/Tooltip/types.ts index 253013f49761..a949e98018b3 100644 --- a/src/components/Tooltip/types.ts +++ b/src/components/Tooltip/types.ts @@ -27,6 +27,8 @@ type TooltipProps = ChildrenProps & { /** passes this down to Hoverable component to decide whether to handle the scroll behaviour to show hover once the scroll ends */ shouldHandleScroll?: boolean; + + shouldForceRenderingBelow?: boolean; }; type TooltipExtendedProps = TooltipProps & { diff --git a/src/components/VideoPlayer/BaseVideoPlayer.js b/src/components/VideoPlayer/BaseVideoPlayer.js new file mode 100644 index 000000000000..73dbf8407c0c --- /dev/null +++ b/src/components/VideoPlayer/BaseVideoPlayer.js @@ -0,0 +1,254 @@ +/* eslint-disable no-underscore-dangle */ +import {Video, VideoFullscreenUpdate} from 'expo-av'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; +import {View} from 'react-native'; +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import Hoverable from '@components/Hoverable'; +import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; +import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext'; +import VideoPopoverMenu from '@components/VideoPopoverMenu'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; +import * as Browser from '@libs/Browser'; +import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import {videoPlayerDefaultProps, videoPlayerPropTypes} from './propTypes'; +import VideoPlayerControls from './VideoPlayerControls'; + +const isMobileSafari = Browser.isMobileSafari(); + +function BaseVideoPlayer({ + url, + resizeMode, + onVideoLoaded, + isLooping, + style, + videoPlayerStyle, + videoStyle, + videoControlsStyle, + videoDuration, + shouldUseSharedVideoElement, + shouldUseSmallVideoControls, + shouldShowVideoControls, + onPlaybackStatusUpdate, + onFullscreenUpdate, + // TODO: investigate what is the root cause of the bug with unexpected video switching + // isVideoHovered caused a bug with unexpected video switching. We are investigating the root cause of the issue, + // but current workaround is just not to use it here for now. This causes not displaying the video controls when + // user hovers the mouse over the carousel arrows, but this UI bug feels much less troublesome for now. + // eslint-disable-next-line no-unused-vars + isVideoHovered, +}) { + const styles = useThemeStyles(); + const {isSmallScreenWidth} = useWindowDimensions(); + const {pauseVideo, playVideo, currentlyPlayingURL, updateSharedElements, sharedElement, originalParent, shareVideoPlayerElements, currentVideoPlayerRef, updateCurrentlyPlayingURL} = + usePlaybackContext(); + const [duration, setDuration] = useState(videoDuration * 1000); + const [position, setPosition] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [isBuffering, setIsBuffering] = useState(true); + const [sourceURL] = useState(url.includes('blob:') || url.includes('file:///') ? url : addEncryptedAuthTokenToURL(url)); + const [isPopoverVisible, setIsPopoverVisible] = useState(false); + const [popoverAnchorPosition, setPopoverAnchorPosition] = useState({horizontal: 0, vertical: 0}); + const videoPlayerRef = useRef(null); + const videoPlayerElementParentRef = useRef(null); + const videoPlayerElementRef = useRef(null); + const sharedVideoPlayerParentRef = useRef(null); + const videoResumeTryNumber = useRef(0); + const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen(); + const isCurrentlyURLSet = currentlyPlayingURL === url; + + const togglePlayCurrentVideo = useCallback(() => { + videoResumeTryNumber.current = 0; + if (!isCurrentlyURLSet) { + updateCurrentlyPlayingURL(url); + } else if (isPlaying) { + pauseVideo(); + } else { + playVideo(); + } + }, [isCurrentlyURLSet, isPlaying, pauseVideo, playVideo, updateCurrentlyPlayingURL, url]); + + const showPopoverMenu = (e) => { + setPopoverAnchorPosition({horizontal: e.nativeEvent.pageX, vertical: e.nativeEvent.pageY}); + setIsPopoverVisible(true); + }; + + const hidePopoverMenu = () => { + setIsPopoverVisible(false); + }; + + // fix for iOS mWeb: preventing iOS native player edfault behavior from pausing the video when exiting fullscreen + const preventPausingWhenExitingFullscreen = useCallback( + (isVideoPlaying) => { + if (videoResumeTryNumber.current === 0 || isVideoPlaying) { + return; + } + if (videoResumeTryNumber.current === 1) { + playVideo(); + } + videoResumeTryNumber.current -= 1; + }, + [playVideo], + ); + + const handlePlaybackStatusUpdate = useCallback( + (e) => { + const isVideoPlaying = e.isPlaying || false; + preventPausingWhenExitingFullscreen(isVideoPlaying); + setIsPlaying(isVideoPlaying); + setIsLoading(!e.isLoaded || Number.isNaN(e.durationMillis)); // when video is ready to display duration is not NaN + setIsBuffering(e.isBuffering || false); + setDuration(e.durationMillis || videoDuration * 1000); + setPosition(e.positionMillis || 0); + + onPlaybackStatusUpdate(e); + }, + [onPlaybackStatusUpdate, preventPausingWhenExitingFullscreen, videoDuration], + ); + + const handleFullscreenUpdate = useCallback( + (e) => { + onFullscreenUpdate(e); + // fix for iOS native and mWeb: when switching to fullscreen and then exiting + // the fullscreen mode while playing, the video pauses + if (!isPlaying || e.fullscreenUpdate !== VideoFullscreenUpdate.PLAYER_DID_DISMISS) { + return; + } + + if (isMobileSafari) { + pauseVideo(); + } + playVideo(); + videoResumeTryNumber.current = 3; + }, + [isPlaying, onFullscreenUpdate, pauseVideo, playVideo], + ); + + const bindFunctions = useCallback(() => { + currentVideoPlayerRef.current._onPlaybackStatusUpdate = handlePlaybackStatusUpdate; + currentVideoPlayerRef.current._onFullscreenUpdate = handleFullscreenUpdate; + // update states after binding + currentVideoPlayerRef.current.getStatusAsync().then((status) => { + handlePlaybackStatusUpdate(status); + }); + }, [currentVideoPlayerRef, handleFullscreenUpdate, handlePlaybackStatusUpdate]); + + // update shared video elements + useEffect(() => { + if (shouldUseSharedVideoElement || url !== currentlyPlayingURL) { + return; + } + shareVideoPlayerElements(videoPlayerRef.current, videoPlayerElementParentRef.current, videoPlayerElementRef.current); + }, [currentlyPlayingURL, shouldUseSharedVideoElement, shareVideoPlayerElements, updateSharedElements, url]); + + // append shared video element to new parent (used for example in attachment modal) + useEffect(() => { + if (url !== currentlyPlayingURL || !sharedElement || !shouldUseSharedVideoElement) { + return; + } + + const newParentRef = sharedVideoPlayerParentRef.current; + videoPlayerRef.current = currentVideoPlayerRef.current; + if (currentlyPlayingURL === url) { + newParentRef.appendChild(sharedElement); + bindFunctions(); + } + return () => { + if (!originalParent && !newParentRef.childNodes[0]) { + return; + } + originalParent.appendChild(sharedElement); + }; + }, [bindFunctions, currentVideoPlayerRef, currentlyPlayingURL, isSmallScreenWidth, originalParent, sharedElement, shouldUseSharedVideoElement, url]); + + return ( + <> + + + {(isHovered) => ( + + {shouldUseSharedVideoElement ? ( + <> + + {/* We are adding transparent absolute View between appended video component and control buttons to enable + catching onMouse events from Attachment Carousel. Due to late appending React doesn't handle + element's events properly. */} + + + ) : ( + { + if (!el) { + return; + } + videoPlayerElementParentRef.current = el; + if (el.childNodes && el.childNodes[0]) { + videoPlayerElementRef.current = el.childNodes[0]; + } + }} + > + { + togglePlayCurrentVideo(); + }} + style={styles.flex1} + > + + + )} + + {(isLoading || isBuffering) && } + + {shouldShowVideoControls && !isLoading && (isPopoverVisible || isHovered || canUseTouchScreen) && ( + + )} + + )} + + + + + ); +} + +BaseVideoPlayer.propTypes = videoPlayerPropTypes; +BaseVideoPlayer.defaultProps = videoPlayerDefaultProps; +BaseVideoPlayer.displayName = 'BaseVideoPlayer'; + +export default BaseVideoPlayer; diff --git a/src/components/VideoPlayer/IconButton.js b/src/components/VideoPlayer/IconButton.js new file mode 100644 index 000000000000..71c1a2150692 --- /dev/null +++ b/src/components/VideoPlayer/IconButton.js @@ -0,0 +1,70 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Hoverable from '@components/Hoverable'; +import Icon from '@components/Icon'; +import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; +import Tooltip from '@components/Tooltip'; +import useThemeStyles from '@hooks/useThemeStyles'; +import stylePropTypes from '@styles/stylePropTypes'; + +const propTypes = { + // use IconAsset as soon as it will be migrated to TS + // eslint-disable-next-line react/forbid-prop-types + src: PropTypes.any.isRequired, + + onPress: PropTypes.func, + + fill: PropTypes.string, + + tooltipText: PropTypes.string, + + style: stylePropTypes, + + hoverStyle: stylePropTypes, + + small: PropTypes.bool, + + shouldForceRenderingTooltipBelow: PropTypes.bool, +}; + +const defaultProps = { + fill: 'white', + onPress: () => {}, + style: {}, + hoverStyle: {}, + small: false, + tooltipText: '', + shouldForceRenderingTooltipBelow: false, +}; + +function IconButton({src, fill, onPress, style, hoverStyle, tooltipText, small, shouldForceRenderingTooltipBelow}) { + const styles = useThemeStyles(); + return ( + + + {(isHovered) => ( + + + + )} + + + ); +} + +IconButton.propTypes = propTypes; +IconButton.defaultProps = defaultProps; +IconButton.displayName = 'IconButton'; + +export default IconButton; diff --git a/src/components/VideoPlayer/VideoPlayerControls/ProgressBar/index.js b/src/components/VideoPlayer/VideoPlayerControls/ProgressBar/index.js new file mode 100644 index 000000000000..c6eb1a179726 --- /dev/null +++ b/src/components/VideoPlayer/VideoPlayerControls/ProgressBar/index.js @@ -0,0 +1,92 @@ +import PropTypes from 'prop-types'; +import React, {useEffect, useState} from 'react'; +import {Gesture, GestureDetector} from 'react-native-gesture-handler'; +import Animated, {runOnJS, useAnimatedStyle, useSharedValue} from 'react-native-reanimated'; +import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext'; +import useThemeStyles from '@hooks/useThemeStyles'; + +const propTypes = { + duration: PropTypes.number.isRequired, + + position: PropTypes.number.isRequired, + + seekPosition: PropTypes.func.isRequired, +}; + +const defaultProps = {}; + +function getProgress(currentPosition, maxPosition) { + return Math.min(Math.max((currentPosition / maxPosition) * 100, 0), 100); +} + +function ProgressBar({duration, position, seekPosition}) { + const styles = useThemeStyles(); + const {pauseVideo, playVideo, checkVideoPlaying} = usePlaybackContext(); + const [sliderWidth, setSliderWidth] = useState(1); + const [isSliderPressed, setIsSliderPressed] = useState(false); + const progressWidth = useSharedValue(0); + const wasVideoPlayingOnCheck = useSharedValue(false); + + const onCheckVideoPlaying = (isPlaying) => { + wasVideoPlayingOnCheck.value = isPlaying; + }; + + const progressBarInteraction = (event) => { + const progress = getProgress(event.x, sliderWidth); + progressWidth.value = progress; + runOnJS(seekPosition)((progress * duration) / 100); + }; + + const onSliderLayout = (e) => { + setSliderWidth(e.nativeEvent.layout.width); + }; + + const pan = Gesture.Pan() + .onBegin((event) => { + runOnJS(setIsSliderPressed)(true); + runOnJS(checkVideoPlaying)(onCheckVideoPlaying); + runOnJS(pauseVideo)(); + runOnJS(progressBarInteraction)(event); + }) + .onChange((event) => { + runOnJS(progressBarInteraction)(event); + }) + .onFinalize(() => { + runOnJS(setIsSliderPressed)(false); + if (!wasVideoPlayingOnCheck.value) { + return; + } + runOnJS(playVideo)(); + }); + + useEffect(() => { + if (isSliderPressed) { + return; + } + progressWidth.value = getProgress(position, duration); + }, [duration, isSliderPressed, position, progressWidth]); + + const progressBarStyle = useAnimatedStyle(() => ({width: `${progressWidth.value}%`})); + + return ( + + + + + + + + ); +} + +ProgressBar.propTypes = propTypes; +ProgressBar.defaultProps = defaultProps; +ProgressBar.displayName = 'ProgressBar'; + +export default ProgressBar; diff --git a/src/components/VideoPlayer/VideoPlayerControls/VolumeButton/index.js b/src/components/VideoPlayer/VideoPlayerControls/VolumeButton/index.js new file mode 100644 index 000000000000..1c1d042d3893 --- /dev/null +++ b/src/components/VideoPlayer/VideoPlayerControls/VolumeButton/index.js @@ -0,0 +1,106 @@ +import PropTypes from 'prop-types'; +import React, {memo, useState} from 'react'; +import {View} from 'react-native'; +import {Gesture, GestureDetector} from 'react-native-gesture-handler'; +import Animated, {runOnJS, useAnimatedStyle, useDerivedValue} from 'react-native-reanimated'; +import Hoverable from '@components/Hoverable'; +import * as Expensicons from '@components/Icon/Expensicons'; +import IconButton from '@components/VideoPlayer/IconButton'; +import {useVolumeContext} from '@components/VideoPlayerContexts/VolumeContext'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import stylePropTypes from '@styles/stylePropTypes'; + +const propTypes = { + style: stylePropTypes.isRequired, + small: PropTypes.bool, +}; + +const defaultProps = { + small: false, +}; + +const getVolumeIcon = (volume) => { + if (volume === 0) { + return Expensicons.Mute; + } + if (volume <= 0.5) { + return Expensicons.VolumeLow; + } + return Expensicons.VolumeHigh; +}; + +function VolumeButton({style, small}) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {updateVolume, volume} = useVolumeContext(); + const [sliderHeight, setSliderHeight] = useState(1); + const [volumeIcon, setVolumeIcon] = useState({icon: getVolumeIcon(volume.value)}); + const [isSliderBeingUsed, setIsSliderBeingUsed] = useState(false); + + const onSliderLayout = (e) => { + setSliderHeight(e.nativeEvent.layout.height); + }; + + const pan = Gesture.Pan() + .onBegin(() => { + runOnJS(setIsSliderBeingUsed)(true); + }) + .onChange((event) => { + const val = Math.floor((1 - event.y / sliderHeight) * 100) / 100; + volume.value = Math.min(Math.max(val, 0), 1); + }) + .onEnd(() => { + runOnJS(setIsSliderBeingUsed)(false); + }); + + const progressBarStyle = useAnimatedStyle(() => ({height: `${volume.value * 100}%`})); + + const updateIcon = (vol) => { + setVolumeIcon({icon: getVolumeIcon(vol)}); + }; + + useDerivedValue(() => { + runOnJS(updateVolume)(volume.value); + runOnJS(updateIcon)(volume.value); + }, [volume]); + + return ( + + {(isHovered) => ( + + {(isSliderBeingUsed || isHovered) && ( + + + + + + + + + + + )} + + updateVolume(volume.value === 0 ? 1 : 0)} + src={volumeIcon.icon} + fill={styles.white} + small={small} + shouldForceRenderingTooltipBelow + /> + + )} + + ); +} + +VolumeButton.propTypes = propTypes; +VolumeButton.defaultProps = defaultProps; +VolumeButton.displayName = 'VolumeButton'; + +export default memo(VolumeButton); diff --git a/src/components/VideoPlayer/VideoPlayerControls/index.js b/src/components/VideoPlayer/VideoPlayerControls/index.js new file mode 100644 index 000000000000..5a926123feef --- /dev/null +++ b/src/components/VideoPlayer/VideoPlayerControls/index.js @@ -0,0 +1,123 @@ +import PropTypes from 'prop-types'; +import React, {useCallback, useMemo, useState} from 'react'; +import {View} from 'react-native'; +import Animated from 'react-native-reanimated'; +import * as Expensicons from '@components/Icon/Expensicons'; +import refPropTypes from '@components/refPropTypes'; +import Text from '@components/Text'; +import IconButton from '@components/VideoPlayer/IconButton'; +import convertMillisecondsToTime from '@components/VideoPlayer/utils'; +import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import stylePropTypes from '@styles/stylePropTypes'; +import CONST from '@src/CONST'; +import ProgressBar from './ProgressBar'; +import VolumeButton from './VolumeButton'; + +const propTypes = { + duration: PropTypes.number.isRequired, + + position: PropTypes.number.isRequired, + + url: PropTypes.string.isRequired, + + videoPlayerRef: refPropTypes.isRequired, + + isPlaying: PropTypes.bool.isRequired, + + // Defines if component should have small icons and tighter spacing inline + small: PropTypes.bool, + + style: stylePropTypes, + + showPopoverMenu: PropTypes.func.isRequired, + + togglePlayCurrentVideo: PropTypes.func.isRequired, +}; + +const defaultProps = { + small: false, + style: undefined, +}; + +function VideoPlayerControls({duration, position, url, videoPlayerRef, isPlaying, small, style, showPopoverMenu, togglePlayCurrentVideo}) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {updateCurrentlyPlayingURL} = usePlaybackContext(); + const [shouldShowTime, setShouldShowTime] = useState(false); + const iconSpacing = small ? styles.mr3 : styles.mr4; + + const onLayout = (e) => { + setShouldShowTime(e.nativeEvent.layout.width > CONST.VIDEO_PLAYER.HIDE_TIME_TEXT_WIDTH); + }; + + const enterFullScreenMode = useCallback(() => { + updateCurrentlyPlayingURL(url); + videoPlayerRef.current.presentFullscreenPlayer(); + }, [updateCurrentlyPlayingURL, url, videoPlayerRef]); + + const seekPosition = useCallback( + (newPosition) => { + videoPlayerRef.current.setStatusAsync({positionMillis: newPosition}); + }, + [videoPlayerRef], + ); + + const durationFormatted = useMemo(() => convertMillisecondsToTime(duration), [duration]); + + return ( + + + + + {shouldShowTime && ( + + {convertMillisecondsToTime(position)} + / + {durationFormatted} + + )} + + + + + + + + + + + + ); +} + +VideoPlayerControls.propTypes = propTypes; +VideoPlayerControls.defaultProps = defaultProps; +VideoPlayerControls.displayName = 'VideoPlayerControls'; + +export default VideoPlayerControls; diff --git a/src/components/VideoPlayer/index.js b/src/components/VideoPlayer/index.js new file mode 100644 index 000000000000..c51e9bd680e1 --- /dev/null +++ b/src/components/VideoPlayer/index.js @@ -0,0 +1,19 @@ +import React, {forwardRef} from 'react'; +import BaseVideoPlayer from './BaseVideoPlayer'; +import {videoPlayerDefaultProps, videoPlayerPropTypes} from './propTypes'; + +function VideoPlayer(props, ref) { + return ( + + ); +} + +VideoPlayer.displayName = 'VideoPlayer'; +VideoPlayer.propTypes = videoPlayerPropTypes; +VideoPlayer.defaultProps = videoPlayerDefaultProps; + +export default forwardRef(VideoPlayer); diff --git a/src/components/VideoPlayer/index.native.js b/src/components/VideoPlayer/index.native.js new file mode 100644 index 000000000000..6581ff01cb75 --- /dev/null +++ b/src/components/VideoPlayer/index.native.js @@ -0,0 +1,23 @@ +import React, {forwardRef} from 'react'; +import CONST from '@src/CONST'; +import BaseVideoPlayer from './BaseVideoPlayer'; +import {videoPlayerDefaultProps, videoPlayerPropTypes} from './propTypes'; + +function VideoPlayer({videoControlsStyle, ...props}, ref) { + return ( + + ); +} + +VideoPlayer.displayName = 'VideoPlayer'; +VideoPlayer.propTypes = videoPlayerPropTypes; +VideoPlayer.defaultProps = videoPlayerDefaultProps; + +export default forwardRef(VideoPlayer); diff --git a/src/components/VideoPlayer/propTypes.js b/src/components/VideoPlayer/propTypes.js new file mode 100644 index 000000000000..6a21cb6c3dc6 --- /dev/null +++ b/src/components/VideoPlayer/propTypes.js @@ -0,0 +1,57 @@ +import {ResizeMode} from 'expo-av'; +import PropTypes from 'prop-types'; +import stylePropTypes from '@styles/stylePropTypes'; + +const videoPlayerPropTypes = { + url: PropTypes.string.isRequired, + + onVideoLoaded: PropTypes.func, + + resizeMode: PropTypes.string, + + isLooping: PropTypes.bool, + + // style for the whole video player component + style: stylePropTypes, + + // style for the video player inside the component + videoPlayerStyle: stylePropTypes, + + // style for the video element inside the video player + videoStyle: stylePropTypes, + + videoControlsStyle: stylePropTypes, + + videoDuration: PropTypes.number, + + shouldUseSharedVideoElement: PropTypes.bool, + + shouldUseSmallVideoControls: PropTypes.bool, + + shouldShowVideoControls: PropTypes.bool, + + isVideoHovered: PropTypes.bool, + + onFullscreenUpdate: PropTypes.func, + + onPlaybackStatusUpdate: PropTypes.func, +}; + +const videoPlayerDefaultProps = { + onVideoLoaded: () => {}, + resizeMode: ResizeMode.CONTAIN, + isLooping: false, + style: undefined, + videoPlayerStyle: undefined, + videoStyle: undefined, + videoControlsStyle: undefined, + videoDuration: 0, + shouldUseSharedVideoElement: false, + shouldUseSmallVideoControls: false, + shouldShowVideoControls: true, + isVideoHovered: false, + onFullscreenUpdate: () => {}, + onPlaybackStatusUpdate: () => {}, +}; + +export {videoPlayerDefaultProps, videoPlayerPropTypes}; diff --git a/src/components/VideoPlayer/utils.ts b/src/components/VideoPlayer/utils.ts new file mode 100644 index 000000000000..57f5422d0ce5 --- /dev/null +++ b/src/components/VideoPlayer/utils.ts @@ -0,0 +1,9 @@ +import {format} from 'date-fns'; + +// Converts milliseconds to 'minutes:seconds' format +const convertMillisecondsToTime = (milliseconds: number) => { + const date = new Date(milliseconds); + return format(date, 'mm:ss'); +}; + +export default convertMillisecondsToTime; diff --git a/src/components/VideoPlayerContexts/PlaybackContext.js b/src/components/VideoPlayerContexts/PlaybackContext.js new file mode 100644 index 000000000000..b77068f3aea2 --- /dev/null +++ b/src/components/VideoPlayerContexts/PlaybackContext.js @@ -0,0 +1,124 @@ +import PropTypes from 'prop-types'; +import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; +import useCurrentReportID from '@hooks/useCurrentReportID'; + +const PlaybackContext = React.createContext(null); + +function PlaybackContextProvider({children}) { + const [currentlyPlayingURL, setCurrentlyPlayingURL] = useState(null); + const [sharedElement, setSharedElement] = useState(null); + const [originalParent, setOriginalParent] = useState(null); + const currentVideoPlayerRef = useRef(null); + const {currentReportID} = useCurrentReportID(); + + const pauseVideo = useCallback(() => { + if (!(currentVideoPlayerRef && currentVideoPlayerRef.current && currentVideoPlayerRef.current.setStatusAsync)) { + return; + } + currentVideoPlayerRef.current.setStatusAsync({shouldPlay: false}); + }, [currentVideoPlayerRef]); + + const stopVideo = useCallback(() => { + if (!(currentVideoPlayerRef && currentVideoPlayerRef.current && currentVideoPlayerRef.current.stopAsync)) { + return; + } + currentVideoPlayerRef.current.stopAsync({shouldPlay: false}); + }, [currentVideoPlayerRef]); + + const playVideo = useCallback(() => { + if (!(currentVideoPlayerRef && currentVideoPlayerRef.current && currentVideoPlayerRef.current.setStatusAsync)) { + return; + } + currentVideoPlayerRef.current.getStatusAsync().then((status) => { + const newStatus = {shouldPlay: true}; + if (status.durationMillis === status.positionMillis) { + newStatus.positionMillis = 0; + } + currentVideoPlayerRef.current.setStatusAsync(newStatus); + }); + }, [currentVideoPlayerRef]); + + const unloadVideo = useCallback(() => { + if (!(currentVideoPlayerRef && currentVideoPlayerRef.current && currentVideoPlayerRef.current.unloadAsync)) { + return; + } + currentVideoPlayerRef.current.unloadAsync(); + }, [currentVideoPlayerRef]); + + const updateCurrentlyPlayingURL = useCallback( + (url) => { + if (currentlyPlayingURL && url !== currentlyPlayingURL) { + pauseVideo(); + } + setCurrentlyPlayingURL(url); + }, + [currentlyPlayingURL, pauseVideo], + ); + + const shareVideoPlayerElements = useCallback( + (ref, parent, child) => { + currentVideoPlayerRef.current = ref; + setOriginalParent(parent); + setSharedElement(child); + playVideo(); + }, + [playVideo], + ); + + const checkVideoPlaying = useCallback( + (statusCallback) => { + currentVideoPlayerRef.current.getStatusAsync().then((status) => { + statusCallback(status.isPlaying); + }); + }, + [currentVideoPlayerRef], + ); + + const resetVideoPlayerData = useCallback(() => { + stopVideo(); + unloadVideo(); + setCurrentlyPlayingURL(null); + setSharedElement(null); + setOriginalParent(null); + currentVideoPlayerRef.current = null; + }, [stopVideo, unloadVideo]); + + useEffect(() => { + if (!currentReportID) { + return; + } + resetVideoPlayerData(); + }, [currentReportID, resetVideoPlayerData]); + + const contextValue = useMemo( + () => ({ + updateCurrentlyPlayingURL, + currentlyPlayingURL, + originalParent, + sharedElement, + currentVideoPlayerRef, + shareVideoPlayerElements, + playVideo, + pauseVideo, + checkVideoPlaying, + }), + [updateCurrentlyPlayingURL, currentlyPlayingURL, originalParent, sharedElement, shareVideoPlayerElements, playVideo, pauseVideo, checkVideoPlaying], + ); + return {children}; +} + +function usePlaybackContext() { + const context = useContext(PlaybackContext); + if (context === undefined) { + throw new Error('usePlaybackContext must be used within a PlaybackContextProvider'); + } + return context; +} + +PlaybackContextProvider.displayName = 'PlaybackContextProvider'; +PlaybackContextProvider.propTypes = { + /** Actual content wrapped by this component */ + children: PropTypes.node.isRequired, +}; + +export {PlaybackContextProvider, usePlaybackContext}; diff --git a/src/components/VideoPlayerContexts/VideoPopoverMenuContext.js b/src/components/VideoPlayerContexts/VideoPopoverMenuContext.js new file mode 100644 index 000000000000..23d1aec1817c --- /dev/null +++ b/src/components/VideoPlayerContexts/VideoPopoverMenuContext.js @@ -0,0 +1,78 @@ +import PropTypes from 'prop-types'; +import React, {useCallback, useContext, useMemo, useState} from 'react'; +import _ from 'underscore'; +import * as Expensicons from '@components/Icon/Expensicons'; +import useLocalize from '@hooks/useLocalize'; +import fileDownload from '@libs/fileDownload'; +import * as Url from '@libs/Url'; +import CONST from '@src/CONST'; +import {usePlaybackContext} from './PlaybackContext'; + +const VideoPopoverMenuContext = React.createContext(null); + +function VideoPopoverMenuContextProvider({children}) { + const {currentVideoPlayerRef} = usePlaybackContext(); + const {translate} = useLocalize(); + const [currentPlaybackSpeed, setCurrentPlaybackSpeed] = useState(CONST.VIDEO_PLAYER.PLAYBACK_SPEEDS[2]); + + const updatePlaybackSpeed = useCallback( + (speed) => { + setCurrentPlaybackSpeed(speed); + currentVideoPlayerRef.current.setStatusAsync({rate: speed}); + }, + [currentVideoPlayerRef], + ); + + const downloadAttachment = useCallback(() => { + currentVideoPlayerRef.current.getStatusAsync().then((status) => { + const sourceURI = `/${Url.getPathFromURL(status.uri)}`; + fileDownload(sourceURI); + }); + }, [currentVideoPlayerRef]); + + const menuItems = useMemo( + () => [ + { + icon: Expensicons.Download, + text: translate('common.download'), + onSelected: () => { + downloadAttachment(); + }, + }, + { + icon: Expensicons.Meter, + text: translate('videoPlayer.playbackSpeed'), + subMenuItems: [ + ..._.map(CONST.VIDEO_PLAYER.PLAYBACK_SPEEDS, (speed) => ({ + icon: currentPlaybackSpeed === speed ? Expensicons.Checkmark : null, + text: speed.toString(), + onSelected: () => { + updatePlaybackSpeed(speed); + }, + shouldPutLeftPaddingWhenNoIcon: true, + })), + ], + }, + ], + [currentPlaybackSpeed, downloadAttachment, translate, updatePlaybackSpeed], + ); + + const contextValue = useMemo(() => ({menuItems, updatePlaybackSpeed}), [menuItems, updatePlaybackSpeed]); + return {children}; +} + +function useVideoPopoverMenuContext() { + const context = useContext(VideoPopoverMenuContext); + if (context === undefined) { + throw new Error('useVideoPopoverMenuContext must be used within a VideoPopoverMenuContext'); + } + return context; +} + +VideoPopoverMenuContextProvider.displayName = 'VideoPopoverMenuContextProvider'; +VideoPopoverMenuContextProvider.propTypes = { + /** Actual content wrapped by this component */ + children: PropTypes.node.isRequired, +}; + +export {VideoPopoverMenuContextProvider, useVideoPopoverMenuContext}; diff --git a/src/components/VideoPlayerContexts/VolumeContext.js b/src/components/VideoPlayerContexts/VolumeContext.js new file mode 100644 index 000000000000..2df463654075 --- /dev/null +++ b/src/components/VideoPlayerContexts/VolumeContext.js @@ -0,0 +1,50 @@ +import PropTypes from 'prop-types'; +import React, {useCallback, useContext, useEffect, useMemo} from 'react'; +import {useSharedValue} from 'react-native-reanimated'; +import {usePlaybackContext} from './PlaybackContext'; + +const VolumeContext = React.createContext(null); + +function VolumeContextProvider({children}) { + const {currentVideoPlayerRef, originalParent} = usePlaybackContext(); + const volume = useSharedValue(0); + + const updateVolume = useCallback( + (newVolume) => { + if (!currentVideoPlayerRef.current) { + return; + } + currentVideoPlayerRef.current.setStatusAsync({volume: newVolume}); + volume.value = newVolume; + }, + [currentVideoPlayerRef, volume], + ); + + // We want to update the volume when currently playing video changes. + // When originalParent changed we're sure that currentVideoPlayerRef is updated. So we can apply the new volume. + useEffect(() => { + if (!originalParent) { + return; + } + updateVolume(volume.value); + }, [originalParent, updateVolume, volume.value]); + + const contextValue = useMemo(() => ({updateVolume, volume}), [updateVolume, volume]); + return {children}; +} + +function useVolumeContext() { + const context = useContext(VolumeContext); + if (context === undefined) { + throw new Error('useVolumeContext must be used within a VolumeContextProvider'); + } + return context; +} + +VolumeContextProvider.displayName = 'VolumeContextProvider'; +VolumeContextProvider.propTypes = { + /** Actual content wrapped by this component */ + children: PropTypes.node.isRequired, +}; + +export {VolumeContextProvider, useVolumeContext}; diff --git a/src/components/VideoPlayerPreview/VideoPlayerThumbnail.js b/src/components/VideoPlayerPreview/VideoPlayerThumbnail.js new file mode 100644 index 000000000000..9e6069e4d979 --- /dev/null +++ b/src/components/VideoPlayerPreview/VideoPlayerThumbnail.js @@ -0,0 +1,62 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import {View} from 'react-native'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import Image from '@components/Image'; +import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; +import useThemeStyles from '@hooks/useThemeStyles'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; + +const propTypes = { + onPress: PropTypes.func.isRequired, + + accessibilityLabel: PropTypes.string.isRequired, + + thumbnailUrl: PropTypes.string, +}; + +const defaultProps = { + thumbnailUrl: undefined, +}; + +function VideoPlayerThumbnail({thumbnailUrl, onPress, accessibilityLabel}) { + const styles = useThemeStyles(); + + return ( + + {thumbnailUrl && ( + + + + )} + + + + + + + ); +} + +VideoPlayerThumbnail.propTypes = propTypes; +VideoPlayerThumbnail.defaultProps = defaultProps; +VideoPlayerThumbnail.displayName = 'VideoPlayerThumbnail'; + +export default VideoPlayerThumbnail; diff --git a/src/components/VideoPlayerPreview/index.js b/src/components/VideoPlayerPreview/index.js new file mode 100644 index 000000000000..252bc53fc839 --- /dev/null +++ b/src/components/VideoPlayerPreview/index.js @@ -0,0 +1,101 @@ +import PropTypes from 'prop-types'; +import React, {useEffect, useState} from 'react'; +import {View} from 'react-native'; +import * as Expensicons from '@components/Icon/Expensicons'; +import VideoPlayer from '@components/VideoPlayer'; +import IconButton from '@components/VideoPlayer/IconButton'; +import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useThumbnailDimensions from '@hooks/useThumbnailDimensions'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import CONST from '@src/CONST'; +import VideoPlayerThumbnail from './VideoPlayerThumbnail'; + +const propTypes = { + videoUrl: PropTypes.string.isRequired, + + videoDimensions: PropTypes.shape({ + width: PropTypes.number.isRequired, + height: PropTypes.number.isRequired, + }), + + videoDuration: PropTypes.number, + + thumbnailUrl: PropTypes.string, + + fileName: PropTypes.string.isRequired, + + onShowModalPress: PropTypes.func.isRequired, +}; + +const defaultProps = { + videoDimensions: CONST.VIDEO_PLAYER.DEFAULT_VIDEO_DIMENSIONS, + thumbnailUrl: undefined, + videoDuration: 0, +}; + +function VideoPlayerPreview({videoUrl, thumbnailUrl, fileName, videoDimensions, videoDuration, onShowModalPress}) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {currentlyPlayingURL, updateCurrentlyPlayingURL} = usePlaybackContext(); + const {isSmallScreenWidth} = useWindowDimensions(); + const [isThumbnail, setIsThumbnail] = useState(true); + const [measuredDimensions, setMeasuredDimensions] = useState(videoDimensions); + const {thumbnailDimensionsStyles} = useThumbnailDimensions(measuredDimensions.width, measuredDimensions.height); + + const onVideoLoaded = (e) => { + setMeasuredDimensions({width: e.srcElement.videoWidth, height: e.srcElement.videoHeight}); + }; + + const handleOnPress = () => { + updateCurrentlyPlayingURL(videoUrl); + if (isSmallScreenWidth) { + onShowModalPress(); + } + }; + + useEffect(() => { + if (videoUrl !== currentlyPlayingURL) { + return; + } + setIsThumbnail(false); + }, [currentlyPlayingURL, updateCurrentlyPlayingURL, videoUrl]); + + return ( + + {isSmallScreenWidth || isThumbnail ? ( + + ) : ( + <> + + + + + )} + + ); +} + +VideoPlayerPreview.propTypes = propTypes; +VideoPlayerPreview.defaultProps = defaultProps; +VideoPlayerPreview.displayName = 'VideoPlayerPreview'; + +export default VideoPlayerPreview; diff --git a/src/components/VideoPopoverMenu/index.js b/src/components/VideoPopoverMenu/index.js new file mode 100644 index 000000000000..01aaa8e35174 --- /dev/null +++ b/src/components/VideoPopoverMenu/index.js @@ -0,0 +1,44 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import PopoverMenu from '@components/PopoverMenu'; +import {useVideoPopoverMenuContext} from '@components/VideoPlayerContexts/VideoPopoverMenuContext'; + +const propTypes = { + isPopoverVisible: PropTypes.bool, + + hidePopover: PropTypes.func, + + anchorPosition: PropTypes.shape({ + horizontal: PropTypes.number.isRequired, + vertical: PropTypes.number.isRequired, + }), +}; +const defaultProps = { + isPopoverVisible: false, + anchorPosition: { + horizontal: 0, + vertical: 0, + }, + hidePopover: () => {}, +}; + +function VideoPopoverMenu({isPopoverVisible, hidePopover, anchorPosition}) { + const {menuItems} = useVideoPopoverMenuContext(); + + return ( + + ); +} + +VideoPopoverMenu.propTypes = propTypes; +VideoPopoverMenu.defaultProps = defaultProps; +VideoPopoverMenu.displayName = 'VideoPopoverMenu'; + +export default VideoPopoverMenu; diff --git a/src/components/menuItemPropTypes.js b/src/components/menuItemPropTypes.js index 3b21cd13c848..80ae1edd5176 100644 --- a/src/components/menuItemPropTypes.js +++ b/src/components/menuItemPropTypes.js @@ -164,6 +164,8 @@ const propTypes = { /** Should check anonymous user in onPress function */ shouldCheckActionAllowedOnPress: PropTypes.bool, + shouldPutLeftPaddingWhenNoIcon: PropTypes.bool, + /** The menu item link or function to get the link */ link: PropTypes.oneOfType(PropTypes.func, PropTypes.string), diff --git a/src/hooks/useThumbnailDimensions.ts b/src/hooks/useThumbnailDimensions.ts new file mode 100644 index 000000000000..4c4ef3363eda --- /dev/null +++ b/src/hooks/useThumbnailDimensions.ts @@ -0,0 +1,30 @@ +import {useMemo} from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; +import type {DimensionValue} from 'react-native/Libraries/StyleSheet/StyleSheetTypes'; +import CONST from '@src/CONST'; +import useWindowDimensions from './useWindowDimensions'; + +type ThumbnailDimensions = { + thumbnailDimensionsStyles: { + aspectRatio?: number | string | undefined; + height?: DimensionValue | undefined; + width?: DimensionValue | undefined; + } & StyleProp; +}; + +export default function useThumbnailDimensions(width: number, height: number): ThumbnailDimensions { + const {isSmallScreenWidth} = useWindowDimensions(); + const fixedDimension = isSmallScreenWidth ? CONST.THUMBNAIL_IMAGE.SMALL_SCREEN.SIZE : CONST.THUMBNAIL_IMAGE.WIDE_SCREEN.SIZE; + const thumbnailDimensionsStyles = useMemo(() => { + if (!width || !height) { + return {width: fixedDimension, aspectRatio: CONST.THUMBNAIL_IMAGE.NAN_ASPECT_RATIO}; + } + const aspectRatio = (height && width / height) || 1; + if (width > height) { + return {width: fixedDimension, aspectRatio}; + } + return {height: fixedDimension, aspectRatio}; + }, [width, height, fixedDimension]); + + return {thumbnailDimensionsStyles}; +} diff --git a/src/hooks/useWindowDimensions/index.native.ts b/src/hooks/useWindowDimensions/index.native.ts index 5d556234aeb9..f727a40b0c3b 100644 --- a/src/hooks/useWindowDimensions/index.native.ts +++ b/src/hooks/useWindowDimensions/index.native.ts @@ -12,6 +12,7 @@ export default function (): WindowDimensions { const isSmallScreenWidth = true; const isMediumScreenWidth = false; const isLargeScreenWidth = false; + const isSmallScreen = true; return { windowWidth, @@ -20,5 +21,6 @@ export default function (): WindowDimensions { isSmallScreenWidth, isMediumScreenWidth, isLargeScreenWidth, + isSmallScreen, }; } diff --git a/src/hooks/useWindowDimensions/index.ts b/src/hooks/useWindowDimensions/index.ts index 4ba2c4ad9b41..abd92db8a040 100644 --- a/src/hooks/useWindowDimensions/index.ts +++ b/src/hooks/useWindowDimensions/index.ts @@ -23,6 +23,9 @@ export default function (useCachedViewportHeight = false): WindowDimensions { const isMediumScreenWidth = windowWidth > variables.mobileResponsiveWidthBreakpoint && windowWidth <= variables.tabletResponsiveWidthBreakpoint; const isLargeScreenWidth = windowWidth > variables.tabletResponsiveWidthBreakpoint; + const lowerScreenDimmension = Math.min(windowWidth, windowHeight); + const isSmallScreen = lowerScreenDimmension <= variables.mobileResponsiveWidthBreakpoint; + const [cachedViewportHeight, setCachedViewportHeight] = useState(windowHeight); const handleFocusIn = useRef((event: FocusEvent) => { @@ -82,5 +85,6 @@ export default function (useCachedViewportHeight = false): WindowDimensions { isSmallScreenWidth, isMediumScreenWidth, isLargeScreenWidth, + isSmallScreen, }; } diff --git a/src/hooks/useWindowDimensions/types.ts b/src/hooks/useWindowDimensions/types.ts index 9b59d4968935..d7a1fe41d187 100644 --- a/src/hooks/useWindowDimensions/types.ts +++ b/src/hooks/useWindowDimensions/types.ts @@ -5,6 +5,7 @@ type WindowDimensions = { isSmallScreenWidth: boolean; isMediumScreenWidth: boolean; isLargeScreenWidth: boolean; + isSmallScreen: boolean; }; export default WindowDimensions; diff --git a/src/languages/en.ts b/src/languages/en.ts index ce1d7e842a1d..f33ddde1fb53 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2325,4 +2325,13 @@ export default { taxRateChanged: 'Tax rate was modified', taxRequired: 'Missing tax rate', }, + videoPlayer: { + play: 'Play', + pause: 'Pause', + fullscreen: 'Fullscreen', + playbackSpeed: 'Playback speed', + expand: 'Expand', + mute: 'Mute', + unmute: 'Unmute', + }, } satisfies TranslationBase; diff --git a/src/languages/es.ts b/src/languages/es.ts index d16df41dcd36..6bc225fbea28 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2816,4 +2816,13 @@ export default { taxRateChanged: 'La tasa de impuesto fue modificada', taxRequired: 'Falta la tasa de impuesto', }, + videoPlayer: { + play: 'Reproducir', + pause: 'Pausar', + fullscreen: 'Pantalla completa', + playbackSpeed: 'Velocidad', + expand: 'Expandir', + mute: 'Silenciar', + unmute: 'Activar sonido', + }, } satisfies EnglishTranslation; diff --git a/src/styles/index.ts b/src/styles/index.ts index 9667abe91cea..856f8ece917c 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -204,6 +204,15 @@ const webViewStyles = (theme: ThemeColors) => ...touchCalloutNone, }, + video: { + minWidth: CONST.VIDEO_PLAYER.MIN_WIDTH, + minHeight: CONST.VIDEO_PLAYER.MIN_HEIGHT, + borderRadius: variables.componentBorderRadiusNormal, + overflow: 'hidden', + backgroundColor: theme.highlightBG, + ...touchCalloutNone, + }, + p: { marginTop: 0, marginBottom: 0, @@ -4430,6 +4439,126 @@ const styles = (theme: ThemeColors) => lineHeight: variables.lineHeightXXLarge, }, + videoPlayerControlsContainer: { + position: 'absolute', + bottom: CONST.VIDEO_PLAYER.CONTROLS_POSITION.NORMAL, + left: CONST.VIDEO_PLAYER.CONTROLS_POSITION.NORMAL, + right: CONST.VIDEO_PLAYER.CONTROLS_POSITION.NORMAL, + backgroundColor: theme.videoPlayerBG, + borderRadius: 8, + flexDirection: 'column', + overflow: 'visible', + zIndex: 9000, + }, + + videoPlayerControlsButtonContainer: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + + progressBarOutline: { + width: '100%', + height: 4, + borderRadius: 8, + backgroundColor: theme.transparentWhite, + }, + + progressBarFill: { + height: '100%', + backgroundColor: colors.white, + borderRadius: 8, + }, + + videoPlayerControlsRow: { + flexDirection: 'row', + alignItems: 'center', + }, + + videoPlayerText: { + textAlign: 'center', + fontSize: variables.fontSizeLabel, + fontWeight: '700', + lineHeight: 16, + color: theme.white, + }, + + volumeSliderContainer: { + position: 'absolute', + left: 0, + bottom: 0, + width: '100%', + height: 100, + alignItems: 'center', + borderRadius: 4, + backgroundColor: colors.green700, + }, + + volumeSliderOverlay: { + width: 4, + height: 60, + backgroundColor: theme.transparentWhite, + borderRadius: 8, + marginTop: 8, + alignItems: 'center', + justifyContent: 'flex-end', + }, + + volumeSliderThumb: { + width: 8, + height: 8, + borderRadius: 8, + backgroundColor: colors.white, + marginBottom: -2, + }, + + volumeSliderFill: { + width: 4, + height: 20, + backgroundColor: colors.white, + borderRadius: 8, + }, + + videoIconButton: { + padding: 4, + borderRadius: 4, + }, + + videoIconButtonHovered: { + backgroundColor: colors.green700, + }, + + videoThumbnailContainer: { + width: '100%', + height: '100%', + alignItems: 'center', + justifyContent: 'center', + position: 'absolute', + top: 0, + left: 0, + }, + + videoThumbnailPlayButton: { + backgroundColor: theme.videoPlayerBG, + borderRadius: 100, + width: 72, + height: 72, + alignItems: 'center', + justifyContent: 'center', + }, + + videoExpandButton: { + position: 'absolute', + top: 12, + right: 12, + backgroundColor: theme.videoPlayerBG, + borderRadius: 8, + padding: 8, + }, + + videoPlayerTimeComponentWidth: { + width: 40, + }, colorSchemeStyle: (colorScheme: ColorScheme) => ({colorScheme}), updateAnimation: { diff --git a/src/styles/theme/themes/dark.ts b/src/styles/theme/themes/dark.ts index bce0145099b9..5aa8966a5e00 100644 --- a/src/styles/theme/themes/dark.ts +++ b/src/styles/theme/themes/dark.ts @@ -86,6 +86,8 @@ const darkTheme = { loungeAccessOverlay: colors.blue800, mapAttributionText: colors.black, white: colors.white, + videoPlayerBG: `${colors.productDark100}cc`, + transparentWhite: `${colors.white}51`, // Adding a color here will animate the status bar to the right color when the screen is opened. // Note that it needs to be a screen name, not a route url. diff --git a/src/styles/theme/themes/light.ts b/src/styles/theme/themes/light.ts index ebd9c07aa60a..0c1c0416cfc0 100644 --- a/src/styles/theme/themes/light.ts +++ b/src/styles/theme/themes/light.ts @@ -86,6 +86,8 @@ const lightTheme = { loungeAccessOverlay: colors.blue800, mapAttributionText: colors.black, white: colors.white, + videoPlayerBG: `${colors.productDark100}cc`, + transparentWhite: `${colors.white}51`, // Adding a color here will animate the status bar to the right color when the screen is opened. // Note that it needs to be a screen name, not a route url. diff --git a/src/styles/theme/types.ts b/src/styles/theme/types.ts index 6d7c0183c142..efd331484284 100644 --- a/src/styles/theme/types.ts +++ b/src/styles/theme/types.ts @@ -89,6 +89,8 @@ type ThemeColors = { loungeAccessOverlay: Color; mapAttributionText: Color; white: Color; + videoPlayerBG: Color; + transparentWhite: Color; PAGE_THEMES: Record; diff --git a/src/styles/utils/generators/TooltipStyleUtils.ts b/src/styles/utils/generators/TooltipStyleUtils.ts index 5bd72928df2b..21b271998370 100644 --- a/src/styles/utils/generators/TooltipStyleUtils.ts +++ b/src/styles/utils/generators/TooltipStyleUtils.ts @@ -116,6 +116,7 @@ type TooltipParams = { tooltipWrapperHeight?: number; manualShiftHorizontal?: number; manualShiftVertical?: number; + shouldForceRenderingBelow?: boolean; }; type GetTooltipStylesStyleUtil = {getTooltipStyles: (props: TooltipParams) => TooltipStyles}; @@ -155,6 +156,7 @@ const createTooltipStyleUtils: StyleUtilGenerator = ( tooltipWrapperHeight, manualShiftHorizontal = 0, manualShiftVertical = 0, + shouldForceRenderingBelow = false, }) => { const tooltipVerticalPadding = spacing.pv1; @@ -182,7 +184,8 @@ const createTooltipStyleUtils: StyleUtilGenerator = ( // If either a tooltip will try to render within GUTTER_WIDTH logical pixels of the top of the screen, // Or the wrapped component is overlapping at top-center with another element // we'll display it beneath its wrapped component rather than above it as usual. - shouldShowBelow = yOffset - tooltipHeight < GUTTER_WIDTH || !!(tooltip && isOverlappingAtTop(tooltip, xOffset, yOffset, tooltipTargetWidth, tooltipTargetHeight)); + shouldShowBelow = + shouldForceRenderingBelow || yOffset - tooltipHeight < GUTTER_WIDTH || !!(tooltip && isOverlappingAtTop(tooltip, xOffset, yOffset, tooltipTargetWidth, tooltipTargetHeight)); // When the tooltip size is ready, we can start animating the scale. scale = currentSize; diff --git a/src/styles/variables.ts b/src/styles/variables.ts index 24f69af120a3..c6c29fdc4b79 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -188,6 +188,7 @@ export default { reportActionItemImagesMoreCornerTriangleWidth: 40, bankCardWidth: 40, bankCardHeight: 26, + popoverzIndex: 10000, workspaceTypeIconWidth: 34, sectionMargin: 16, workspaceSectionMaxWidth: 680,