From e4d75a5bfe78c1420e521bd7c1f439754be89c47 Mon Sep 17 00:00:00 2001 From: Bannon Tanner Date: Fri, 21 Aug 2026 12:11:18 -0500 Subject: [PATCH 1/5] FEATURE: Download support for iOS Previously, there was no way to download files in the Hub when on iOS. In React Native, downloads on iOS have to be manually handled and this commit adds support for that. --- ios/Discourse/Info.plist | 2 +- ios/Podfile.lock | 185 +++++++++++------- ios/ShareExtension/Info.plist | 2 +- js/lib/__tests__/handleDownload.test.js | 95 +++++++++ js/lib/handleDownload.js | 125 ++++++++++++ js/locale/en.json | 5 + .../WebViewComponent.js | 15 +- package.json | 1 + yarn.lock | 81 ++++++++ 9 files changed, 432 insertions(+), 79 deletions(-) create mode 100644 js/lib/__tests__/handleDownload.test.js create mode 100644 js/lib/handleDownload.js diff --git a/ios/Discourse/Info.plist b/ios/Discourse/Info.plist index 7b7a260ce..f7e7bc850 100644 --- a/ios/Discourse/Info.plist +++ b/ios/Discourse/Info.plist @@ -37,7 +37,7 @@ CFBundleVersion - 700 + 705 Fabric APIKey diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 400121e15..bbb25f591 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1648,6 +1648,35 @@ PODS: - React-RCTFBReactNativeSpec - ReactCommon/turbomodule/core - SocketRocket + - react-native-blob-util (0.24.10): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga - react-native-blur (4.4.1): - boost - DoubleConversion @@ -1776,7 +1805,7 @@ PODS: - react-native-segmented-control (2.2.2): - React-Core - react-native-vector-icons-fontawesome5 (12.2.0) - - react-native-webview (13.17.0): + - react-native-webview (13.16.0): - boost - DoubleConversion - fast_float @@ -2722,6 +2751,7 @@ DEPENDENCIES: - React-logger (from `../node_modules/react-native/ReactCommon/logger`) - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`) - React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) + - react-native-blob-util (from `../node_modules/react-native-blob-util`) - "react-native-blur (from `../node_modules/@react-native-community/blur`)" - react-native-key-pair (from `../node_modules/react-native-key-pair`) - react-native-safari-view (from `../node_modules/react-native-safari-view`) @@ -2862,6 +2892,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon" React-microtasksnativemodule: :path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks" + react-native-blob-util: + :path: "../node_modules/react-native-blob-util" react-native-blur: :path: "../node_modules/@react-native-community/blur" react-native-key-pair: @@ -2968,12 +3000,12 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 + boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 + DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6 FBLazyVector: 86588b5a1547e7a417942a08f49559b184e002c8 - fmt: 01b82d4ca6470831d1cc0852a1af644be019e8f6 - glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a + fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd + glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 hermes-engine: bbc1152da7d2d40f9e59c28acc6576fcf5d28e2a RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 RCTDeprecation: 300c5eb91114d4339b0bb39505d0f4824d7299b7 @@ -2981,87 +3013,88 @@ SPEC CHECKSUMS: RCTTypeSafety: cb974efcdc6695deedf7bf1eb942f2a0603a063f React: e7a4655b09d0e17e54be188cc34c2f3e2087318a React-callinvoker: 62192daaa2f30c3321fc531e4f776f7b09cf892b - React-Core: b23cdaaa9d76389d958c06af3c57aa6ad611c542 - React-CoreModules: 8e0f562e5695991e455abbebe1e968af71d52553 - React-cxxreact: 6ccbe0cc2c652b29409b14b23cfb3cd74e084691 + React-Core: c400b068fdb6172177f3b3fae00c10d1077244d7 + React-CoreModules: 8e911a5a504b45824374eec240a78de7a6db8ca2 + React-cxxreact: 06a91f55ac5f842219d6ca47e0f77187a5b5f4ac React-debug: 1834225a63b420b16e9b8b01ba5870aee96d0610 - React-defaultsnativemodule: dd88d445d542d58ab61a8a29a7c1d2272dfed577 - React-domnativemodule: fc3c24f4d3bb92770727ea48b4133dab77ded7f7 - React-Fabric: 00fe76339e568da0d0497cc72daeeb01e463871a - React-FabricComponents: 7bb179ee55db68f88c007800b0ac62c930115a85 - React-FabricImage: 21e01118011dd1e4ff3cdab20dbf57839cff52ee - React-featureflags: 6e67f2e252bc8ebb1d538c2ae8c14df432fe5fc0 - React-featureflagsnativemodule: eff5216a5cde5df5d09243d15db1bc401474deef - React-graphics: 8539372da8754118a565251ed08a88fc70f69340 - React-hermes: cc8c77acee1406c258622cd8abbee9049f6b5761 - React-idlecallbacksnativemodule: 7349675d1ccbec876c29b0e206ac08c762baaa36 - React-ImageManager: 4089d8ad52c86a8ae1d7591282fff1665ff5518b - React-jserrorhandler: 89a7a5fa8d04791e729119d1db03bf0ee85a9e29 - React-jsi: ea5c640ea63c127080f158dac7f4f393d13d415c - React-jsiexecutor: cf7920f82e46fe9a484c15c9f31e67d7179aa826 - React-jsinspector: 69e974b6313dbbb635ba503f2f4f2c389b30edbf - React-jsinspectorcdp: 231ddd5b7164c37589dcde3b8b6960136c891d6d - React-jsinspectornetwork: ff74911f79cf0a407a7f0ad0eeb0be64687ed815 - React-jsinspectortracing: df2aa2d944bb3fa280d9c920b9a06664bca8a7e8 - React-jsitooling: 77849c27e374a028ed8106e434a35267f6c6600b - React-jsitracing: 0dc6978e5b38c6e5e01e6aed484e4aec3f5f581b - React-logger: 7cfc7b1ae1f8e5fe5097f9c746137cc3a8fad4ce - React-Mapbuffer: 7018c5b7da5b13ed22fe55dae51d50187a00b2d7 - React-microtasksnativemodule: 8ff9cb220a8efa625b5885996bd69e69db9edf02 - react-native-blur: 477fac85a42b5506232ef1d2b82aad359feadd26 - react-native-key-pair: f69098449fa3f63baac4f4280910d4acb5ec3d89 - react-native-safari-view: 07dc856a2663fef31eaca6beb79b111b8f6cf1f2 - react-native-safari-web-auth: 63e920416493c35c795bae6b70fbda283497705e - react-native-safe-area-context: 8ed800930eb7d0b6651218f69656e823d37b6ef7 - react-native-segmented-control: be570ddc1678933470a39471e0ebd824f23e39a8 + React-defaultsnativemodule: 260aa990a9617c58df46c00321f396ad6ea7cc7f + React-domnativemodule: 9b3456a614c325da986867f27ca0eb34cb86828c + React-Fabric: fc7bcbac28989e6025ca6ae0988bff61bb78e5d3 + React-FabricComponents: ae4a9c82bedf7c95bace1b215caf8685bcb32e23 + React-FabricImage: c9cd4786180c150bb2a3841d65d360fd52be9ef8 + React-featureflags: 534cd678e05848fbfc8c7288d4b14bcd8894b696 + React-featureflagsnativemodule: bf7419f4d81226a3c4dd792445a03a6d703ce9a4 + React-graphics: 18296c3559d54a42baaf7f2ae9c137a2e0fe9d51 + React-hermes: b6e33fcd21aa7523dc76e62acd7a547e68c28a5b + React-idlecallbacksnativemodule: da8696a714ab16adb56bbfc9e0dfb4de7a713340 + React-ImageManager: 052ccce122e4fd4e09c5d4f30e56381704dac439 + React-jserrorhandler: 4c037384a32f57332abfa64181aeea915f9e0f0d + React-jsi: 3fde19aaf675c0607a0824c4d6002a4943820fd9 + React-jsiexecutor: 4f898228240cf261a02568e985dfa7e1d7ad1dfb + React-jsinspector: 4ad0cdfa25a45d1362e2ddd06c78727d7964b34f + React-jsinspectorcdp: a649cc98a448e0fd8d54ac2a9e3e53177a1d8bd3 + React-jsinspectornetwork: 2d701b6b152be202342f8269223046ec664c7d47 + React-jsinspectortracing: cd898b3d7ea89f3e0ae10020fe3504bb4b327dd8 + React-jsitooling: feca163583c69ba642cebb6b8ccd2f5e6732fed8 + React-jsitracing: 1965307a468987b20d2a020f8fe782efa591ded7 + React-logger: ea80169d826e0cd112fa4d68f58b2b3b968f1ecb + React-Mapbuffer: a5d550d1add940ed2bc65b20dc1413407bf1a63f + React-microtasksnativemodule: 5d00fefc19f0bc9a6432e5533683d6fc9c3da4e1 + react-native-blob-util: 598a68ff8f2402eac074c54eac9a539238a5a222 + react-native-blur: 736888dd75460ec75605254214caded098066cfc + react-native-key-pair: 5492baed2d7bbff57341ece66ecf5f5f340da439 + react-native-safari-view: 955d7160d159241b8e9395d12d10ea0ef863dcdd + react-native-safari-web-auth: 73f4f00a0f1b6b1ad247e7e2b4ff7df5fe87a997 + react-native-safe-area-context: fd72bd1eb562ff2a0fda20e9a7828c4b0cabf5a4 + react-native-segmented-control: 65df6cd0619b780b3843d574a72d4c7cec396097 react-native-vector-icons-fontawesome5: 1199fa2cf3323fac0b9b52b2aecb1329aa3602a5 - react-native-webview: a685ccea981f1410f67245117d44bfb91c6d7df6 - React-NativeModulesApple: 37c08c3c54db55854de816b0df0f3683832be35a + react-native-webview: fe1b2ebf06ca1ba26e22caf7228abce5d9de4ec9 + React-NativeModulesApple: b22e6abb44d78270dfdfc7d85efe29e35e0333a7 React-oscompat: 56d6de59f9ae95cd006a1c40be2cde83bc06a4e1 - React-perflogger: 4008bd05a8b6c157b06608c0ea0b8bd5d9c5e6c9 - React-performancetimeline: 9321ba7605abcfb3a2b497fd7cbaf5cfd8c7cf67 + React-perflogger: 0633844e495d8b34798c9bf0cb32ce315f1d5c9f + React-performancetimeline: a04dae9154c32eda1891fcfa51cb2680a0421b3e React-RCTActionSheet: 49138012280ec3bbb35193d8d09adb8bc61c982e - React-RCTAnimation: ebfe7c62016d4c17b56b2cab3a221908ae46288d - React-RCTAppDelegate: 0108657ba9a19f6a1cd62dcd19c2c0485b3fc251 - React-RCTBlob: 6cc309d1623f3c2679125a04a7425685b7219e6b - React-RCTFabric: 0a9ff5c9d1e1d7fc026bda6671180cbf56861c15 - React-RCTFBReactNativeSpec: ff3e37e2456afc04211334e86d07bf20488df0ae - React-RCTImage: bb98a59aeed953a48be3f917b9b745b213b340ab - React-RCTLinking: d6e9795d4d75d154c1dd821fd0746cc3e05d6670 - React-RCTNetwork: 5c8a7a2dd26728323189362f149e788548ac72bc - React-RCTRuntime: 96808e8fdce300a26c82d8c24174e33ba5210a7c - React-RCTSettings: b6a02d545ce10dd936b39914b32674db6e865307 - React-RCTText: c7d9232da0e9b5082a99a617483d9164a9cd46e9 - React-RCTVibration: fe636c985c1bf25e4a5b5b4d9315a3b882468a72 + React-RCTAnimation: c7ed4a9d5a4e43c9b10f68bb43cd238c4a2e7e89 + React-RCTAppDelegate: ea2ab6f4aef1489f72025b7128d8ab645b40eafb + React-RCTBlob: c052799460b245e1fffe3d1dddea36fa88e998a0 + React-RCTFabric: e7acf005f8ed58d09f755b980ff83703b3af9fcf + React-RCTFBReactNativeSpec: ffb22c3ee3d359ae9245ca94af203845da9371ec + React-RCTImage: 59fc2571f4f109a77139924f5babee8f9cd639c9 + React-RCTLinking: a045cb58c08188dce6c6f4621de105114b1b16ce + React-RCTNetwork: fc7115a2f5e15ae0aa05e9a9be726817feefb482 + React-RCTRuntime: a7bca9be4f571586b2a9d4b57cf605421ffb6335 + React-RCTSettings: 30d7dd7eae66290467a1e72bf42d927fa78c3884 + React-RCTText: 755d59284e66c7d33bb4f0ccc428fe69110c3e74 + React-RCTVibration: ffe019e588815df226f6f8ccdc65979f8b2bc440 React-rendererconsistency: d20fcb77173861cc7d8356239823e3b36966fc31 - React-renderercss: 56461d1e18db6a325048fdd04a51d68bd7ddb5a8 - React-rendererdebug: fcd44d3eb8a02d74beee778bb142e724016c7375 + React-renderercss: 63c720c32aaabd4788ac4136a071d49a052d8002 + React-rendererdebug: a25ddddc73cabf50d814d8dfbc60d257b3d854c4 React-rncore: bafb76fc01b78757a9592e92dbc227f9260bf0ac - React-RuntimeApple: 01e3ad08793efaa54cf85276457fa4a1f103d5b4 - React-RuntimeCore: 5c4bec5bf402a99b134e55972f2f4e676c70b9ab + React-RuntimeApple: 45f8ef1b220a91b4fa4a79820b81990bffd95aa5 + React-RuntimeCore: a0e095493b22ee3f6c639df4258cc5185674f0b8 React-runtimeexecutor: b35de9cb7f5d19c66ea9b067235f95b947697ba5 - React-RuntimeHermes: ba549a5834a6592d243b9a605530ecd7b6f5e79c - React-runtimescheduler: 9a9914d58caec7976aaae381cd2d997408f2260f + React-RuntimeHermes: 5b8126fffd1531475861dc0294a10b5f9793271a + React-runtimescheduler: 44fa97351d105afd0ffaecc4ed11cadad562deb6 React-timing: 4f97958cc918f0af9444f93e4a7083415e6f5daf - React-utils: f491e2726eb8ced8af13893e1f77317f0fa9a954 - ReactAppDependencyProvider: 8df342c127fd0c1e30e8b9f71ff814c22414a7c0 - ReactCodegen: 439c427ccc115d71d16cc84256e5fbdc7fcef57a - ReactCommon: 592ef441605638b95e533653259254b4bd35ff4f - RNBackgroundFetch: e44c9e85d7fb3122c37d8a806278f62c7682d7ea - RNCAsyncStorage: 579cd39b67a6eac3ffc4ebb45ad169b11b0f6fe7 - RNCPushNotificationIOS: 6c4ca3388c7434e4a662b92e4dfeeee858e6f440 - RNDeviceInfo: ae26ae45db3f9937f038a284bcd0a1db8d70db96 - RNFlashList: e3c782f37970179e6b3c64f0f6007eb87d725f95 - RNGestureHandler: db29d3e7edf41a11d133cc3ffd4029762376ed1a - RNLocalize: 8bf466de4c92d4721b254aabe1ff0a1456e7b9f4 - RNReanimated: 5479b2116dcbbd2ba1eefb5561d5b322da306884 - RNScreens: 26bb60cdb2ef2ca06fd87feefc495072f25982a7 - RNSiriShortcuts: 149966f5de0cac63bd9fd8c2d5c51235439d1760 - RNSVG: d407e5f2ede12bbcd92e4d23a405ad5a5f7294a3 - RNWorklets: 83e051db94475b21fa729851b0d3aa523cd6f67a + React-utils: 3c4b0b7788e4dc132d1bf918bc0615e2b21f36b3 + ReactAppDependencyProvider: 6c9197c1f6643633012ab646d2bfedd1b0d25989 + ReactCodegen: 9ea66ee246511816b72e9d6e380f884b7b3b99d7 + ReactCommon: 7aca047f2f453a7d7f0adeccb63810d61829235a + RNBackgroundFetch: 534f5bba01d1e45559aa7d53f42e00124378db93 + RNCAsyncStorage: 8e9406ce79bba50a7ab965e1c05a93044f230de6 + RNCPushNotificationIOS: 64218f3c776c03d7408284a819b2abfda1834bc8 + RNDeviceInfo: 29e01d5ae94bdb5a0f6c11a4c438132545b4df80 + RNFlashList: 995a7cad345dcb6f203db0f50bcf4b83cc04f3c9 + RNGestureHandler: cdca641e24d0ab743dcd90a24de4e6259e6aa0de + RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 + RNReanimated: 3d16d7db5b36d76df0477df0723d28d2235c97f8 + RNScreens: 45e1adbc3b4bab2900bf9bb9decfcfb59d0eed8c + RNSiriShortcuts: 17f5bcb1322eb9e9387101110db1cdb6ed82707c + RNSVG: 432ca012e24410cab11449afef353ce20573a59f + RNWorklets: a45d7a51e3c1f50e4d5cc73a1d11d34b76c843b4 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: a742cc68e8366fcfc681808162492bc0aa7a9498 PODFILE CHECKSUM: 8125ac004090fc2c53beddf13d18c2becd0d99e9 -COCOAPODS: 1.16.2 +COCOAPODS: 1.15.2 diff --git a/ios/ShareExtension/Info.plist b/ios/ShareExtension/Info.plist index a01b4454c..d69056d19 100644 --- a/ios/ShareExtension/Info.plist +++ b/ios/ShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.2.3 CFBundleVersion - 700 + 705 NSExtension NSExtensionAttributes diff --git a/js/lib/__tests__/handleDownload.test.js b/js/lib/__tests__/handleDownload.test.js new file mode 100644 index 000000000..8de31ec92 --- /dev/null +++ b/js/lib/__tests__/handleDownload.test.js @@ -0,0 +1,95 @@ +/* @flow */ +'use strict'; + +import { Alert, Platform, Share, ToastAndroid } from 'react-native'; +import ReactNativeBlobUtil from 'react-native-blob-util'; +import handleDownload, { + MAX_BRIDGED_DOWNLOAD_BYTES, + sanitizeFilename, + validateDownloadMessage, +} from '../handleDownload'; + +jest.mock('i18n-js', () => ({ + t: (key, params) => (params?.filename ? `${key}:${params.filename}` : key), +})); + +jest.mock('react-native-blob-util', () => ({ + fs: { + dirs: { CacheDir: '/cache' }, + writeFile: jest.fn(() => Promise.resolve()), + unlink: jest.fn(() => Promise.resolve()), + }, + MediaCollection: { + copyToMediaStore: jest.fn(() => Promise.resolve()), + }, +})); + +function message(overrides = {}) { + return { + type: 'download', + version: 1, + encoding: 'base64', + filename: 'theme.zip', + mimeType: 'application/zip', + byteLength: 1, + data: 'eA==', + ...overrides, + }; +} + +describe('handleDownload', () => { + beforeEach(() => { + jest.clearAllMocks(); + Platform.OS = 'ios'; + Share.share = jest.fn(() => Promise.resolve()); + Alert.alert = jest.fn(); + ToastAndroid.show = jest.fn(); + }); + + test('sanitizes filenames used in native paths', () => { + expect(sanitizeFilename('../../bad\\name:\u0000.zip')).toBe( + '_.._bad_name.zip', + ); + expect(sanitizeFilename('...')).toBe('download'); + }); + + test('rejects malformed and oversized messages', () => { + expect(() => + validateDownloadMessage(message({ data: 'not base64' })), + ).toThrow('Invalid download data'); + expect(() => + validateDownloadMessage( + message({ byteLength: MAX_BRIDGED_DOWNLOAD_BYTES + 1 }), + ), + ).toThrow('Download is too large'); + }); + + test('writes, shares, and removes an iOS temporary file', async () => { + await handleDownload(message()); + + expect(ReactNativeBlobUtil.fs.writeFile).toHaveBeenCalledWith( + expect.stringMatching(/^\/cache\/.+-theme\.zip$/), + 'eA==', + 'base64', + ); + expect(Share.share).toHaveBeenCalledWith( + expect.objectContaining({ title: 'theme.zip' }), + ); + expect(ReactNativeBlobUtil.fs.unlink).toHaveBeenCalled(); + expect(Alert.alert).not.toHaveBeenCalled(); + }); + + test('removes the temporary file and reports native failures', async () => { + ReactNativeBlobUtil.fs.writeFile.mockRejectedValueOnce( + new Error('disk full'), + ); + + await handleDownload(message()); + + expect(ReactNativeBlobUtil.fs.unlink).toHaveBeenCalled(); + expect(Alert.alert).toHaveBeenCalledWith( + 'download.failed_title', + 'disk full', + ); + }); +}); diff --git a/js/lib/handleDownload.js b/js/lib/handleDownload.js new file mode 100644 index 000000000..7cff55fac --- /dev/null +++ b/js/lib/handleDownload.js @@ -0,0 +1,125 @@ +/* @flow */ +'use strict'; + +import { Alert, Platform, Share, ToastAndroid } from 'react-native'; +import ReactNativeBlobUtil from 'react-native-blob-util'; +import i18n from 'i18n-js'; + +export const MAX_BRIDGED_DOWNLOAD_BYTES = 25 * 1024 * 1024; + +let downloadSequence = 0; + +export function sanitizeFilename(name) { + if (typeof name !== 'string') { + return 'download'; + } + + const cleaned = name + .normalize('NFC') + .replace(/[\p{Cc}\u202a-\u202e\u2066-\u2069]/gu, '') + .replace(/^\.+/, '') + .replace(/[. ]+$/, '') + .replace(/[/\\:]+/g, '_') + .replace(/_+(\.[^.]*)$/, '$1') + .trim() + .slice(0, 180); + + return cleaned || 'download'; +} + +function base64ByteLength(value) { + const padding = value.endsWith('==') ? 2 : value.endsWith('=') ? 1 : 0; + return Math.floor((value.length * 3) / 4) - padding; +} + +export function validateDownloadMessage(message) { + if ( + !message || + typeof message !== 'object' || + message.type !== 'download' || + message.version !== 1 || + message.encoding !== 'base64' || + typeof message.filename !== 'string' || + typeof message.mimeType !== 'string' || + typeof message.data !== 'string' || + !Number.isSafeInteger(message.byteLength) + ) { + throw new Error('Invalid download request'); + } + + if ( + message.byteLength < 0 || + message.byteLength > MAX_BRIDGED_DOWNLOAD_BYTES || + message.data.length > Math.ceil((MAX_BRIDGED_DOWNLOAD_BYTES * 4) / 3) + 2 + ) { + throw new Error('Download is too large'); + } + + if ( + !/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/i.test(message.mimeType) || + !/^[a-z0-9+/]*={0,2}$/i.test(message.data) || + message.data.length % 4 !== 0 || + base64ByteLength(message.data) !== message.byteLength + ) { + throw new Error('Invalid download data'); + } + + return { + filename: sanitizeFilename(message.filename), + mimeType: message.mimeType, + data: message.data, + }; +} + +function temporaryPath(filename) { + downloadSequence += 1; + const uniquePart = `${Date.now()}-${downloadSequence}-${Math.random() + .toString(36) + .slice(2, 10)}`; + return `${ReactNativeBlobUtil.fs.dirs.CacheDir}/${uniquePart}-${filename}`; +} + +async function saveAndroidDownload(path, { filename, mimeType }) { + if (ReactNativeBlobUtil.MediaCollection?.copyToMediaStore) { + await ReactNativeBlobUtil.MediaCollection.copyToMediaStore( + { name: filename, parentFolder: '', mimeType }, + 'Download', + path, + ); + ToastAndroid.show( + i18n.t('download.saved', { filename }), + ToastAndroid.LONG, + ); + return; + } + + throw new Error(i18n.t('download.unsupported')); +} + +export default async function handleDownload(message) { + let path; + + try { + const download = validateDownloadMessage(message); + path = temporaryPath(download.filename); + await ReactNativeBlobUtil.fs.writeFile(path, download.data, 'base64'); + + if (Platform.OS === 'ios') { + await Share.share({ + url: `file://${path}`, + title: download.filename, + }); + } else { + await saveAndroidDownload(path, download); + } + } catch (error) { + Alert.alert( + i18n.t('download.failed_title'), + error instanceof Error ? error.message : String(error), + ); + } finally { + if (path) { + await ReactNativeBlobUtil.fs.unlink(path).catch(() => {}); + } + } +} diff --git a/js/locale/en.json b/js/locale/en.json index e7e923909..f34d6f0c7 100644 --- a/js/locale/en.json +++ b/js/locale/en.json @@ -1,4 +1,9 @@ { + "download": { + "failed_title": "Download failed", + "saved": "Saved %{filename} to Downloads", + "unsupported": "Saving downloads is not supported on this device" + }, "add_site": "Add Site", "add_sites": "Get started and follow the Discourse communities you love!", "no_sites_yet": "Welcome to Discourse Hub!", diff --git a/js/screens/WebViewScreenComponents/WebViewComponent.js b/js/screens/WebViewScreenComponents/WebViewComponent.js index e957fe57a..2775f2806 100644 --- a/js/screens/WebViewScreenComponents/WebViewComponent.js +++ b/js/screens/WebViewScreenComponents/WebViewComponent.js @@ -17,6 +17,7 @@ import { import { WebView } from 'react-native-webview'; import ErrorScreen from '../WebViewScreenComponents/ErrorScreen'; import ProgressBar from '../../ProgressBar'; +import handleDownload from '../../lib/handleDownload'; import chroma from 'chroma-js'; import SafariView from 'react-native-safari-view'; import i18n from 'i18n-js'; @@ -452,7 +453,19 @@ class WebViewComponent extends React.Component { } _onMessage(event) { - let data = JSON.parse(event.nativeEvent.data); + let data; + + try { + data = JSON.parse(event.nativeEvent.data); + } catch (error) { + console.warn('WebView message: unparseable JSON', error); + return; + } + + if (data?.type === 'download') { + void handleDownload(data); + return; + } let { headerBg, shareUrl, dismiss, markRead, showLogin } = data; diff --git a/package.json b/package.json index 24fcefa8b..a65da6d74 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "react": "19.1.0", "react-native": "^0.80.2", "react-native-background-fetch": "^4.2.3", + "react-native-blob-util": "^0.24.10", "react-native-custom-tabs": "https://github.com/adminphoeniixx/react-native-custom-tabs", "react-native-device-info": "^13.1.0", "react-native-draggable-flatlist": "^4.0.3", diff --git a/yarn.lock b/yarn.lock index 75ccc1a10..14f249a1d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4954,6 +4954,20 @@ __metadata: languageName: node linkType: hard +"balanced-match@npm:^4.0.2": + version: 4.0.4 + resolution: "balanced-match@npm:4.0.4" + checksum: 10/fb07bb66a0959c2843fc055838047e2a95ccebb837c519614afb067ebfdf2fa967ca8d712c35ced07f2cd26fc6f07964230b094891315ad74f11eba3d53178a0 + languageName: node + linkType: hard + +"base-64@npm:1.0.0": + version: 1.0.0 + resolution: "base-64@npm:1.0.0" + checksum: 10/d10b64a1fc9b2c5a5f39f1ce1e6c9d1c5b249222bbfa3a0604c592d90623caf74419983feadd8a170f27dc0c3389704f72faafa3e645aeb56bfc030c93ff074a + languageName: node + linkType: hard + "base64-js@npm:^1.3.1, base64-js@npm:^1.5.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" @@ -5059,6 +5073,15 @@ __metadata: languageName: node linkType: hard +"brace-expansion@npm:^5.0.8": + version: 5.0.9 + resolution: "brace-expansion@npm:5.0.9" + dependencies: + balanced-match: "npm:^4.0.2" + checksum: 10/d8683d612919212c7cf298861acd1c15101e7e2ad186e7ae9747390e5b3fc9e9b55ce71107570d32985784d9d88fbbe438d725863aed7b0f3d08d5f080535eec + languageName: node + linkType: hard + "braces@npm:^3.0.3": version: 3.0.3 resolution: "braces@npm:3.0.3" @@ -6237,6 +6260,7 @@ __metadata: react: "npm:19.1.0" react-native: "npm:^0.80.2" react-native-background-fetch: "npm:^4.2.3" + react-native-blob-util: "npm:^0.24.10" react-native-custom-tabs: "https://github.com/adminphoeniixx/react-native-custom-tabs" react-native-device-info: "npm:^13.1.0" react-native-draggable-flatlist: "npm:^4.0.3" @@ -7643,6 +7667,17 @@ __metadata: languageName: node linkType: hard +"glob@npm:13.0.6": + version: 13.0.6 + resolution: "glob@npm:13.0.6" + dependencies: + minimatch: "npm:^10.2.2" + minipass: "npm:^7.1.3" + path-scurry: "npm:^2.0.2" + checksum: 10/201ad69e5f0aa74e1d8c00a481581f8b8c804b6a4fbfabeeb8541f5d756932800331daeba99b58fb9e4cd67e12ba5a7eba5b82fb476691588418060b84353214 + languageName: node + linkType: hard + "glob@npm:^10.2.2, glob@npm:^10.4.5": version: 10.4.5 resolution: "glob@npm:10.4.5" @@ -9886,6 +9921,13 @@ __metadata: languageName: node linkType: hard +"lru-cache@npm:^11.0.0": + version: 11.5.2 + resolution: "lru-cache@npm:11.5.2" + checksum: 10/122789c66605ddf29df58ff279e9e2c9499499d0b80b895ec524496f14bcde045d1582e3e38008f3457226d98067556719573b352119d6be8bdb1f8849f85681 + languageName: node + linkType: hard + "lru-cache@npm:^5.1.1": version: 5.1.1 resolution: "lru-cache@npm:5.1.1" @@ -10331,6 +10373,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^10.2.2": + version: 10.2.6 + resolution: "minimatch@npm:10.2.6" + dependencies: + brace-expansion: "npm:^5.0.8" + checksum: 10/a5e43f7c57d6061a77da320b42aa35ffff826030cf67c83c0eee47a26ede4d66486935881492b968c609bf27a7d48cc4b9f54b2806643cd8fb792fcf9240cc1b + languageName: node + linkType: hard + "minimatch@npm:^5.0.1": version: 5.1.6 resolution: "minimatch@npm:5.1.6" @@ -10430,6 +10481,13 @@ __metadata: languageName: node linkType: hard +"minipass@npm:^7.1.3": + version: 7.1.3 + resolution: "minipass@npm:7.1.3" + checksum: 10/175e4d5e20980c3cd316ae82d2c031c42f6c746467d8b1905b51060a0ba4461441a0c25bb67c025fd9617f9a3873e152c7b543c6b5ac83a1846be8ade80dffd6 + languageName: node + linkType: hard + "minizlib@npm:^2.1.1": version: 2.1.2 resolution: "minizlib@npm:2.1.2" @@ -11495,6 +11553,16 @@ __metadata: languageName: node linkType: hard +"path-scurry@npm:^2.0.2": + version: 2.0.2 + resolution: "path-scurry@npm:2.0.2" + dependencies: + lru-cache: "npm:^11.0.0" + minipass: "npm:^7.1.2" + checksum: 10/2b4257422bcb870a4c2d205b3acdbb213a72f5e2250f61c80f79c9d014d010f82bdf8584441612c8e1fa4eb098678f5704a66fa8377d72646bad4be38e57a2c3 + languageName: node + linkType: hard + "path-type@npm:^4.0.0": version: 4.0.0 resolution: "path-type@npm:4.0.0" @@ -11922,6 +11990,19 @@ __metadata: languageName: node linkType: hard +"react-native-blob-util@npm:^0.24.10": + version: 0.24.10 + resolution: "react-native-blob-util@npm:0.24.10" + dependencies: + base-64: "npm:1.0.0" + glob: "npm:13.0.6" + peerDependencies: + react: "*" + react-native: "*" + checksum: 10/3999fbe4c856fd45f09248a80c324c5cd464d9774c705138e864e06d8dab826d2be1ccba25beea09ce1352b66922094eef35435612d5b5e9bde5443803f0269a + languageName: node + linkType: hard + "react-native-custom-tabs@https://github.com/adminphoeniixx/react-native-custom-tabs": version: 0.1.8 resolution: "react-native-custom-tabs@https://github.com/adminphoeniixx/react-native-custom-tabs.git#commit=7605cda9d9d691c36367e98d8513937454627ecd" From 59c4bdc86d9ad6c5ac342db854908b1b91b49458 Mon Sep 17 00:00:00 2001 From: Bannon Tanner Date: Fri, 28 Aug 2026 12:41:51 -0500 Subject: [PATCH 2/5] DEV: Remove android manual download Previously, I had added support for android as well, but upon further testing, realized it already works on android without this manual setup. Removing the android-specific code to allow Android to handle it in its own native way. --- js/lib/__tests__/handleDownload.test.js | 94 +++++++++++----- js/lib/handleDownload.js | 102 ++++++++---------- js/locale/en.json | 3 +- .../WebViewComponent.js | 5 + 4 files changed, 117 insertions(+), 87 deletions(-) diff --git a/js/lib/__tests__/handleDownload.test.js b/js/lib/__tests__/handleDownload.test.js index 8de31ec92..e0f488d93 100644 --- a/js/lib/__tests__/handleDownload.test.js +++ b/js/lib/__tests__/handleDownload.test.js @@ -1,7 +1,7 @@ /* @flow */ 'use strict'; -import { Alert, Platform, Share, ToastAndroid } from 'react-native'; +import { Alert, Platform, Share } from 'react-native'; import ReactNativeBlobUtil from 'react-native-blob-util'; import handleDownload, { MAX_BRIDGED_DOWNLOAD_BYTES, @@ -10,28 +10,23 @@ import handleDownload, { } from '../handleDownload'; jest.mock('i18n-js', () => ({ - t: (key, params) => (params?.filename ? `${key}:${params.filename}` : key), + t: key => key, })); jest.mock('react-native-blob-util', () => ({ fs: { dirs: { CacheDir: '/cache' }, + mkdir: jest.fn(() => Promise.resolve()), writeFile: jest.fn(() => Promise.resolve()), unlink: jest.fn(() => Promise.resolve()), }, - MediaCollection: { - copyToMediaStore: jest.fn(() => Promise.resolve()), - }, })); function message(overrides = {}) { return { type: 'download', - version: 1, - encoding: 'base64', filename: 'theme.zip', mimeType: 'application/zip', - byteLength: 1, data: 'eA==', ...overrides, }; @@ -43,53 +38,98 @@ describe('handleDownload', () => { Platform.OS = 'ios'; Share.share = jest.fn(() => Promise.resolve()); Alert.alert = jest.fn(); - ToastAndroid.show = jest.fn(); }); - test('sanitizes filenames used in native paths', () => { - expect(sanitizeFilename('../../bad\\name:\u0000.zip')).toBe( - '_.._bad_name.zip', + test('strips path separators from filenames', () => { + expect(sanitizeFilename('../bad/name:file.zip')).toBe( + '.._bad_name_file.zip', ); - expect(sanitizeFilename('...')).toBe('download'); + expect(sanitizeFilename('')).toBe('download'); + expect(sanitizeFilename(null)).toBe('download'); }); - test('rejects malformed and oversized messages', () => { + test('rejects messages missing required fields', () => { + expect(() => validateDownloadMessage(null)).toThrow( + 'Invalid download request', + ); + expect(() => validateDownloadMessage(message({ type: 'other' }))).toThrow(); + expect(() => validateDownloadMessage(message({ filename: 123 }))).toThrow(); + }); + + test('rejects non-base64 payloads', () => { expect(() => - validateDownloadMessage(message({ data: 'not base64' })), + validateDownloadMessage(message({ data: 'not base64!' })), ).toThrow('Invalid download data'); - expect(() => - validateDownloadMessage( - message({ byteLength: MAX_BRIDGED_DOWNLOAD_BYTES + 1 }), - ), - ).toThrow('Download is too large'); + expect(() => validateDownloadMessage(message({ data: 'abc' }))).toThrow( + 'Invalid download data', + ); + }); + + test('rejects payloads over the receiver-side size cap', () => { + const overCap = 'A'.repeat( + Math.ceil(((MAX_BRIDGED_DOWNLOAD_BYTES + 4) * 4) / 3 / 4) * 4, + ); + expect(() => validateDownloadMessage(message({ data: overCap }))).toThrow( + 'Download is too large', + ); }); - test('writes, shares, and removes an iOS temporary file', async () => { + test('writes to a unique directory so the shared filename is preserved', async () => { await handleDownload(message()); + expect(ReactNativeBlobUtil.fs.mkdir).toHaveBeenCalledWith( + expect.stringMatching(/^\/cache\/download-/), + ); expect(ReactNativeBlobUtil.fs.writeFile).toHaveBeenCalledWith( - expect.stringMatching(/^\/cache\/.+-theme\.zip$/), + expect.stringMatching(/^\/cache\/download-.+\/theme\.zip$/), 'eA==', 'base64', ); expect(Share.share).toHaveBeenCalledWith( - expect.objectContaining({ title: 'theme.zip' }), + expect.objectContaining({ + url: expect.stringMatching( + /^file:\/\/\/cache\/download-.+\/theme\.zip$/, + ), + }), ); - expect(ReactNativeBlobUtil.fs.unlink).toHaveBeenCalled(); expect(Alert.alert).not.toHaveBeenCalled(); }); - test('removes the temporary file and reports native failures', async () => { + test('unlinks the temp directory after the cleanup delay', async () => { + jest.useFakeTimers(); + try { + await handleDownload(message()); + + expect(ReactNativeBlobUtil.fs.unlink).not.toHaveBeenCalled(); + jest.runAllTimers(); + expect(ReactNativeBlobUtil.fs.unlink).toHaveBeenCalledWith( + expect.stringMatching(/^\/cache\/download-/), + ); + } finally { + jest.useRealTimers(); + } + }); + + test('alerts a generic failure when the write fails', async () => { ReactNativeBlobUtil.fs.writeFile.mockRejectedValueOnce( new Error('disk full'), ); await handleDownload(message()); - expect(ReactNativeBlobUtil.fs.unlink).toHaveBeenCalled(); expect(Alert.alert).toHaveBeenCalledWith( 'download.failed_title', - 'disk full', + 'download.failed_message', ); }); + + test('ignores messages on non-iOS platforms', async () => { + Platform.OS = 'android'; + + await handleDownload(message()); + + expect(ReactNativeBlobUtil.fs.mkdir).not.toHaveBeenCalled(); + expect(ReactNativeBlobUtil.fs.writeFile).not.toHaveBeenCalled(); + expect(Share.share).not.toHaveBeenCalled(); + }); }); diff --git a/js/lib/handleDownload.js b/js/lib/handleDownload.js index 7cff55fac..e1367d71f 100644 --- a/js/lib/handleDownload.js +++ b/js/lib/handleDownload.js @@ -1,12 +1,14 @@ /* @flow */ 'use strict'; -import { Alert, Platform, Share, ToastAndroid } from 'react-native'; +import { Alert, Platform, Share } from 'react-native'; import ReactNativeBlobUtil from 'react-native-blob-util'; import i18n from 'i18n-js'; export const MAX_BRIDGED_DOWNLOAD_BYTES = 25 * 1024 * 1024; +const CLEANUP_DELAY = 20_000; + let downloadSequence = 0; export function sanitizeFilename(name) { @@ -14,16 +16,7 @@ export function sanitizeFilename(name) { return 'download'; } - const cleaned = name - .normalize('NFC') - .replace(/[\p{Cc}\u202a-\u202e\u2066-\u2069]/gu, '') - .replace(/^\.+/, '') - .replace(/[. ]+$/, '') - .replace(/[/\\:]+/g, '_') - .replace(/_+(\.[^.]*)$/, '$1') - .trim() - .slice(0, 180); - + const cleaned = name.replace(/[/\\:\0]+/g, '_').trim(); return cleaned || 'download'; } @@ -37,89 +30,82 @@ export function validateDownloadMessage(message) { !message || typeof message !== 'object' || message.type !== 'download' || - message.version !== 1 || - message.encoding !== 'base64' || typeof message.filename !== 'string' || - typeof message.mimeType !== 'string' || - typeof message.data !== 'string' || - !Number.isSafeInteger(message.byteLength) + typeof message.data !== 'string' ) { throw new Error('Invalid download request'); } if ( - message.byteLength < 0 || - message.byteLength > MAX_BRIDGED_DOWNLOAD_BYTES || - message.data.length > Math.ceil((MAX_BRIDGED_DOWNLOAD_BYTES * 4) / 3) + 2 - ) { - throw new Error('Download is too large'); - } - - if ( - !/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/i.test(message.mimeType) || !/^[a-z0-9+/]*={0,2}$/i.test(message.data) || - message.data.length % 4 !== 0 || - base64ByteLength(message.data) !== message.byteLength + message.data.length % 4 !== 0 ) { throw new Error('Invalid download data'); } + if (base64ByteLength(message.data) > MAX_BRIDGED_DOWNLOAD_BYTES) { + throw new Error('Download is too large'); + } return { filename: sanitizeFilename(message.filename), - mimeType: message.mimeType, + mimeType: + typeof message.mimeType === 'string' + ? message.mimeType + : 'application/octet-stream', data: message.data, }; } -function temporaryPath(filename) { +function temporaryDirectory() { downloadSequence += 1; - const uniquePart = `${Date.now()}-${downloadSequence}-${Math.random() + const unique = `${Date.now()}-${downloadSequence}-${Math.random() .toString(36) .slice(2, 10)}`; - return `${ReactNativeBlobUtil.fs.dirs.CacheDir}/${uniquePart}-${filename}`; + return `${ReactNativeBlobUtil.fs.dirs.CacheDir}/download-${unique}`; } -async function saveAndroidDownload(path, { filename, mimeType }) { - if (ReactNativeBlobUtil.MediaCollection?.copyToMediaStore) { - await ReactNativeBlobUtil.MediaCollection.copyToMediaStore( - { name: filename, parentFolder: '', mimeType }, - 'Download', - path, - ); - ToastAndroid.show( - i18n.t('download.saved', { filename }), - ToastAndroid.LONG, +function scheduleCleanup(dir) { + setTimeout(() => { + ReactNativeBlobUtil.fs.unlink(dir).catch(() => {}); + }, CLEANUP_DELAY); +} + +// The web side only selects the "bridge" download strategy on iOS (see +// discourse/discourse's attachmentDownloadStrategy). Android's WebView has +// handled downloads via the system DownloadManager since react-native-webview +// 2.15, so it should never send us a `type: "download"` message — this guard +// is defense-in-depth in case a stale web build or app version mismatch +// still fires one. +export default async function handleDownload(message) { + if (Platform.OS !== 'ios') { + console.warn( + 'handleDownload: ignoring download message on non-iOS platform', ); return; } - throw new Error(i18n.t('download.unsupported')); -} - -export default async function handleDownload(message) { - let path; + let dir; try { const download = validateDownloadMessage(message); - path = temporaryPath(download.filename); + dir = temporaryDirectory(); + await ReactNativeBlobUtil.fs.mkdir(dir); + const path = `${dir}/${download.filename}`; await ReactNativeBlobUtil.fs.writeFile(path, download.data, 'base64'); - if (Platform.OS === 'ios') { - await Share.share({ - url: `file://${path}`, - title: download.filename, - }); - } else { - await saveAndroidDownload(path, download); - } + await Share.share({ + url: `file://${path}`, + title: download.filename, + }); } catch (error) { + console.warn('handleDownload failed', error); Alert.alert( i18n.t('download.failed_title'), - error instanceof Error ? error.message : String(error), + i18n.t('download.failed_message'), ); } finally { - if (path) { - await ReactNativeBlobUtil.fs.unlink(path).catch(() => {}); + if (dir) { + scheduleCleanup(dir); } } } diff --git a/js/locale/en.json b/js/locale/en.json index f34d6f0c7..84734650c 100644 --- a/js/locale/en.json +++ b/js/locale/en.json @@ -1,8 +1,7 @@ { "download": { "failed_title": "Download failed", - "saved": "Saved %{filename} to Downloads", - "unsupported": "Saving downloads is not supported on this device" + "failed_message": "Something went wrong while preparing this download. Please try again." }, "add_site": "Add Site", "add_sites": "Get started and follow the Discourse communities you love!", diff --git a/js/screens/WebViewScreenComponents/WebViewComponent.js b/js/screens/WebViewScreenComponents/WebViewComponent.js index 2775f2806..4cfac043a 100644 --- a/js/screens/WebViewScreenComponents/WebViewComponent.js +++ b/js/screens/WebViewScreenComponents/WebViewComponent.js @@ -199,6 +199,11 @@ class WebViewComponent extends React.Component { allowsLinkPreview={true} hideKeyboardAccessoryView={!Platform.isPad} webviewDebuggingEnabled={true} + injectedJavaScriptBeforeContentLoaded={ + Platform.OS === 'ios' + ? 'window.__discourseHubDownloadBridge = true; true;' + : undefined + } onLoadEnd={() => { this.webview.requestFocus(); }} From 72f2efc91076d65660ee42adbeaaa6f22248186d Mon Sep 17 00:00:00 2001 From: Bannon Tanner Date: Fri, 28 Aug 2026 12:48:10 -0500 Subject: [PATCH 3/5] DEV: Fix cocoapods and webview versions --- ios/Podfile.lock | 146 +++++++++++++++++++++++------------------------ 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index bbb25f591..be5529982 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1805,7 +1805,7 @@ PODS: - react-native-segmented-control (2.2.2): - React-Core - react-native-vector-icons-fontawesome5 (12.2.0) - - react-native-webview (13.16.0): + - react-native-webview (13.17.0): - boost - DoubleConversion - fast_float @@ -3013,88 +3013,88 @@ SPEC CHECKSUMS: RCTTypeSafety: cb974efcdc6695deedf7bf1eb942f2a0603a063f React: e7a4655b09d0e17e54be188cc34c2f3e2087318a React-callinvoker: 62192daaa2f30c3321fc531e4f776f7b09cf892b - React-Core: c400b068fdb6172177f3b3fae00c10d1077244d7 - React-CoreModules: 8e911a5a504b45824374eec240a78de7a6db8ca2 - React-cxxreact: 06a91f55ac5f842219d6ca47e0f77187a5b5f4ac + React-Core: b23cdaaa9d76389d958c06af3c57aa6ad611c542 + React-CoreModules: 8e0f562e5695991e455abbebe1e968af71d52553 + React-cxxreact: 6ccbe0cc2c652b29409b14b23cfb3cd74e084691 React-debug: 1834225a63b420b16e9b8b01ba5870aee96d0610 - React-defaultsnativemodule: 260aa990a9617c58df46c00321f396ad6ea7cc7f - React-domnativemodule: 9b3456a614c325da986867f27ca0eb34cb86828c - React-Fabric: fc7bcbac28989e6025ca6ae0988bff61bb78e5d3 - React-FabricComponents: ae4a9c82bedf7c95bace1b215caf8685bcb32e23 - React-FabricImage: c9cd4786180c150bb2a3841d65d360fd52be9ef8 - React-featureflags: 534cd678e05848fbfc8c7288d4b14bcd8894b696 - React-featureflagsnativemodule: bf7419f4d81226a3c4dd792445a03a6d703ce9a4 - React-graphics: 18296c3559d54a42baaf7f2ae9c137a2e0fe9d51 - React-hermes: b6e33fcd21aa7523dc76e62acd7a547e68c28a5b - React-idlecallbacksnativemodule: da8696a714ab16adb56bbfc9e0dfb4de7a713340 - React-ImageManager: 052ccce122e4fd4e09c5d4f30e56381704dac439 - React-jserrorhandler: 4c037384a32f57332abfa64181aeea915f9e0f0d - React-jsi: 3fde19aaf675c0607a0824c4d6002a4943820fd9 - React-jsiexecutor: 4f898228240cf261a02568e985dfa7e1d7ad1dfb - React-jsinspector: 4ad0cdfa25a45d1362e2ddd06c78727d7964b34f - React-jsinspectorcdp: a649cc98a448e0fd8d54ac2a9e3e53177a1d8bd3 - React-jsinspectornetwork: 2d701b6b152be202342f8269223046ec664c7d47 - React-jsinspectortracing: cd898b3d7ea89f3e0ae10020fe3504bb4b327dd8 - React-jsitooling: feca163583c69ba642cebb6b8ccd2f5e6732fed8 - React-jsitracing: 1965307a468987b20d2a020f8fe782efa591ded7 - React-logger: ea80169d826e0cd112fa4d68f58b2b3b968f1ecb - React-Mapbuffer: a5d550d1add940ed2bc65b20dc1413407bf1a63f - React-microtasksnativemodule: 5d00fefc19f0bc9a6432e5533683d6fc9c3da4e1 - react-native-blob-util: 598a68ff8f2402eac074c54eac9a539238a5a222 - react-native-blur: 736888dd75460ec75605254214caded098066cfc - react-native-key-pair: 5492baed2d7bbff57341ece66ecf5f5f340da439 - react-native-safari-view: 955d7160d159241b8e9395d12d10ea0ef863dcdd - react-native-safari-web-auth: 73f4f00a0f1b6b1ad247e7e2b4ff7df5fe87a997 - react-native-safe-area-context: fd72bd1eb562ff2a0fda20e9a7828c4b0cabf5a4 - react-native-segmented-control: 65df6cd0619b780b3843d574a72d4c7cec396097 + React-defaultsnativemodule: dd88d445d542d58ab61a8a29a7c1d2272dfed577 + React-domnativemodule: fc3c24f4d3bb92770727ea48b4133dab77ded7f7 + React-Fabric: 00fe76339e568da0d0497cc72daeeb01e463871a + React-FabricComponents: 7bb179ee55db68f88c007800b0ac62c930115a85 + React-FabricImage: 21e01118011dd1e4ff3cdab20dbf57839cff52ee + React-featureflags: 6e67f2e252bc8ebb1d538c2ae8c14df432fe5fc0 + React-featureflagsnativemodule: eff5216a5cde5df5d09243d15db1bc401474deef + React-graphics: 8539372da8754118a565251ed08a88fc70f69340 + React-hermes: cc8c77acee1406c258622cd8abbee9049f6b5761 + React-idlecallbacksnativemodule: 7349675d1ccbec876c29b0e206ac08c762baaa36 + React-ImageManager: 4089d8ad52c86a8ae1d7591282fff1665ff5518b + React-jserrorhandler: 89a7a5fa8d04791e729119d1db03bf0ee85a9e29 + React-jsi: ea5c640ea63c127080f158dac7f4f393d13d415c + React-jsiexecutor: cf7920f82e46fe9a484c15c9f31e67d7179aa826 + React-jsinspector: 69e974b6313dbbb635ba503f2f4f2c389b30edbf + React-jsinspectorcdp: 231ddd5b7164c37589dcde3b8b6960136c891d6d + React-jsinspectornetwork: ff74911f79cf0a407a7f0ad0eeb0be64687ed815 + React-jsinspectortracing: df2aa2d944bb3fa280d9c920b9a06664bca8a7e8 + React-jsitooling: 77849c27e374a028ed8106e434a35267f6c6600b + React-jsitracing: 0dc6978e5b38c6e5e01e6aed484e4aec3f5f581b + React-logger: 7cfc7b1ae1f8e5fe5097f9c746137cc3a8fad4ce + React-Mapbuffer: 7018c5b7da5b13ed22fe55dae51d50187a00b2d7 + React-microtasksnativemodule: 8ff9cb220a8efa625b5885996bd69e69db9edf02 + react-native-blob-util: 5181e5a619545573b57bcb82efad01bf46e41402 + react-native-blur: 477fac85a42b5506232ef1d2b82aad359feadd26 + react-native-key-pair: f69098449fa3f63baac4f4280910d4acb5ec3d89 + react-native-safari-view: 07dc856a2663fef31eaca6beb79b111b8f6cf1f2 + react-native-safari-web-auth: 63e920416493c35c795bae6b70fbda283497705e + react-native-safe-area-context: 8ed800930eb7d0b6651218f69656e823d37b6ef7 + react-native-segmented-control: be570ddc1678933470a39471e0ebd824f23e39a8 react-native-vector-icons-fontawesome5: 1199fa2cf3323fac0b9b52b2aecb1329aa3602a5 - react-native-webview: fe1b2ebf06ca1ba26e22caf7228abce5d9de4ec9 - React-NativeModulesApple: b22e6abb44d78270dfdfc7d85efe29e35e0333a7 + react-native-webview: a685ccea981f1410f67245117d44bfb91c6d7df6 + React-NativeModulesApple: 37c08c3c54db55854de816b0df0f3683832be35a React-oscompat: 56d6de59f9ae95cd006a1c40be2cde83bc06a4e1 - React-perflogger: 0633844e495d8b34798c9bf0cb32ce315f1d5c9f - React-performancetimeline: a04dae9154c32eda1891fcfa51cb2680a0421b3e + React-perflogger: 4008bd05a8b6c157b06608c0ea0b8bd5d9c5e6c9 + React-performancetimeline: 9321ba7605abcfb3a2b497fd7cbaf5cfd8c7cf67 React-RCTActionSheet: 49138012280ec3bbb35193d8d09adb8bc61c982e - React-RCTAnimation: c7ed4a9d5a4e43c9b10f68bb43cd238c4a2e7e89 - React-RCTAppDelegate: ea2ab6f4aef1489f72025b7128d8ab645b40eafb - React-RCTBlob: c052799460b245e1fffe3d1dddea36fa88e998a0 - React-RCTFabric: e7acf005f8ed58d09f755b980ff83703b3af9fcf - React-RCTFBReactNativeSpec: ffb22c3ee3d359ae9245ca94af203845da9371ec - React-RCTImage: 59fc2571f4f109a77139924f5babee8f9cd639c9 - React-RCTLinking: a045cb58c08188dce6c6f4621de105114b1b16ce - React-RCTNetwork: fc7115a2f5e15ae0aa05e9a9be726817feefb482 - React-RCTRuntime: a7bca9be4f571586b2a9d4b57cf605421ffb6335 - React-RCTSettings: 30d7dd7eae66290467a1e72bf42d927fa78c3884 - React-RCTText: 755d59284e66c7d33bb4f0ccc428fe69110c3e74 - React-RCTVibration: ffe019e588815df226f6f8ccdc65979f8b2bc440 + React-RCTAnimation: ebfe7c62016d4c17b56b2cab3a221908ae46288d + React-RCTAppDelegate: 0108657ba9a19f6a1cd62dcd19c2c0485b3fc251 + React-RCTBlob: 6cc309d1623f3c2679125a04a7425685b7219e6b + React-RCTFabric: 0a9ff5c9d1e1d7fc026bda6671180cbf56861c15 + React-RCTFBReactNativeSpec: ff3e37e2456afc04211334e86d07bf20488df0ae + React-RCTImage: bb98a59aeed953a48be3f917b9b745b213b340ab + React-RCTLinking: d6e9795d4d75d154c1dd821fd0746cc3e05d6670 + React-RCTNetwork: 5c8a7a2dd26728323189362f149e788548ac72bc + React-RCTRuntime: 96808e8fdce300a26c82d8c24174e33ba5210a7c + React-RCTSettings: b6a02d545ce10dd936b39914b32674db6e865307 + React-RCTText: c7d9232da0e9b5082a99a617483d9164a9cd46e9 + React-RCTVibration: fe636c985c1bf25e4a5b5b4d9315a3b882468a72 React-rendererconsistency: d20fcb77173861cc7d8356239823e3b36966fc31 - React-renderercss: 63c720c32aaabd4788ac4136a071d49a052d8002 - React-rendererdebug: a25ddddc73cabf50d814d8dfbc60d257b3d854c4 + React-renderercss: 56461d1e18db6a325048fdd04a51d68bd7ddb5a8 + React-rendererdebug: fcd44d3eb8a02d74beee778bb142e724016c7375 React-rncore: bafb76fc01b78757a9592e92dbc227f9260bf0ac - React-RuntimeApple: 45f8ef1b220a91b4fa4a79820b81990bffd95aa5 - React-RuntimeCore: a0e095493b22ee3f6c639df4258cc5185674f0b8 + React-RuntimeApple: 01e3ad08793efaa54cf85276457fa4a1f103d5b4 + React-RuntimeCore: 5c4bec5bf402a99b134e55972f2f4e676c70b9ab React-runtimeexecutor: b35de9cb7f5d19c66ea9b067235f95b947697ba5 - React-RuntimeHermes: 5b8126fffd1531475861dc0294a10b5f9793271a - React-runtimescheduler: 44fa97351d105afd0ffaecc4ed11cadad562deb6 + React-RuntimeHermes: ba549a5834a6592d243b9a605530ecd7b6f5e79c + React-runtimescheduler: 9a9914d58caec7976aaae381cd2d997408f2260f React-timing: 4f97958cc918f0af9444f93e4a7083415e6f5daf - React-utils: 3c4b0b7788e4dc132d1bf918bc0615e2b21f36b3 - ReactAppDependencyProvider: 6c9197c1f6643633012ab646d2bfedd1b0d25989 - ReactCodegen: 9ea66ee246511816b72e9d6e380f884b7b3b99d7 - ReactCommon: 7aca047f2f453a7d7f0adeccb63810d61829235a - RNBackgroundFetch: 534f5bba01d1e45559aa7d53f42e00124378db93 - RNCAsyncStorage: 8e9406ce79bba50a7ab965e1c05a93044f230de6 - RNCPushNotificationIOS: 64218f3c776c03d7408284a819b2abfda1834bc8 - RNDeviceInfo: 29e01d5ae94bdb5a0f6c11a4c438132545b4df80 - RNFlashList: 995a7cad345dcb6f203db0f50bcf4b83cc04f3c9 - RNGestureHandler: cdca641e24d0ab743dcd90a24de4e6259e6aa0de - RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 - RNReanimated: 3d16d7db5b36d76df0477df0723d28d2235c97f8 - RNScreens: 45e1adbc3b4bab2900bf9bb9decfcfb59d0eed8c - RNSiriShortcuts: 17f5bcb1322eb9e9387101110db1cdb6ed82707c - RNSVG: 432ca012e24410cab11449afef353ce20573a59f - RNWorklets: a45d7a51e3c1f50e4d5cc73a1d11d34b76c843b4 + React-utils: f491e2726eb8ced8af13893e1f77317f0fa9a954 + ReactAppDependencyProvider: 8df342c127fd0c1e30e8b9f71ff814c22414a7c0 + ReactCodegen: 439c427ccc115d71d16cc84256e5fbdc7fcef57a + ReactCommon: 592ef441605638b95e533653259254b4bd35ff4f + RNBackgroundFetch: e44c9e85d7fb3122c37d8a806278f62c7682d7ea + RNCAsyncStorage: 579cd39b67a6eac3ffc4ebb45ad169b11b0f6fe7 + RNCPushNotificationIOS: 6c4ca3388c7434e4a662b92e4dfeeee858e6f440 + RNDeviceInfo: ae26ae45db3f9937f038a284bcd0a1db8d70db96 + RNFlashList: e3c782f37970179e6b3c64f0f6007eb87d725f95 + RNGestureHandler: db29d3e7edf41a11d133cc3ffd4029762376ed1a + RNLocalize: 8bf466de4c92d4721b254aabe1ff0a1456e7b9f4 + RNReanimated: 5479b2116dcbbd2ba1eefb5561d5b322da306884 + RNScreens: 26bb60cdb2ef2ca06fd87feefc495072f25982a7 + RNSiriShortcuts: 149966f5de0cac63bd9fd8c2d5c51235439d1760 + RNSVG: d407e5f2ede12bbcd92e4d23a405ad5a5f7294a3 + RNWorklets: 83e051db94475b21fa729851b0d3aa523cd6f67a SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: a742cc68e8366fcfc681808162492bc0aa7a9498 PODFILE CHECKSUM: 8125ac004090fc2c53beddf13d18c2becd0d99e9 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 From f89d4a34c4fe8d8a733defb8d2525eb1cbaf6694 Mon Sep 17 00:00:00 2001 From: Bannon Tanner Date: Wed, 16 Sep 2026 11:59:42 -0500 Subject: [PATCH 4/5] DEV: Bump version + add project file --- fastlane/Fastfile | 4 +-- ios/Discourse.xcodeproj/project.pbxproj | 37 +++++++++++++++---------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index a9baea6b2..0a4f00c02 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -2,8 +2,8 @@ fastlane_require "base64" fastlane_require "fileutils" fastlane_require "json" -IOS_APP_VERSION = "2.2.3" -ANDROID_APP_VERSION = "2.2.0" # run `fastlane bootstrap` after editing this +IOS_APP_VERSION = "2.2.4" +ANDROID_APP_VERSION = "2.2.1" # run `fastlane bootstrap` after editing this PROJECT_NAME = "Discourse" IOS_TEAM_ID = "6T3LU73T8S" KEYS_REPOSITORY = "git@github.com:discourse-org/discourse-mobile-keys.git" diff --git a/ios/Discourse.xcodeproj/project.pbxproj b/ios/Discourse.xcodeproj/project.pbxproj index 7641b07be..eb53ae5ae 100644 --- a/ios/Discourse.xcodeproj/project.pbxproj +++ b/ios/Discourse.xcodeproj/project.pbxproj @@ -383,6 +383,7 @@ "${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/react-native-blob-util/ReactNativeBlobUtilPrivacyInfo.bundle", "${PODS_ROOT}/../../node_modules/@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Brands.ttf", "${PODS_ROOT}/../../node_modules/@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Regular.ttf", "${PODS_ROOT}/../../node_modules/@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Solid.ttf", @@ -398,6 +399,7 @@ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ReactNativeBlobUtilPrivacyInfo.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Brands.ttf", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Regular.ttf", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Solid.ttf", @@ -468,12 +470,12 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Discourse/Discourse.entitlements; - CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 700; + CURRENT_PROJECT_VERSION = 705; DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = 6T3LU73T8S; + DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 6T3LU73T8S; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -497,8 +499,8 @@ ); PRODUCT_BUNDLE_IDENTIFIER = org.discourse.DiscourseApp; PRODUCT_NAME = Discourse; - PROVISIONING_PROFILE_SPECIFIER = "match AdHoc org.discourse.DiscourseApp"; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AdHoc org.discourse.DiscourseApp 1784743760"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AdHoc org.discourse.DiscourseApp"; SWIFT_OBJC_BRIDGING_HEADER = "Discourse-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -516,7 +518,7 @@ CODE_SIGN_ENTITLEMENTS = Discourse/Discourse.entitlements; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 700; + CURRENT_PROJECT_VERSION = 705; DEVELOPMENT_TEAM = 6T3LU73T8S; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 6T3LU73T8S; ENABLE_BITCODE = NO; @@ -543,7 +545,7 @@ PRODUCT_NAME = Discourse; PROVISIONING_PROFILE = "8a5dde79-abbd-4707-a921-2b4412ef65ad"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore org.discourse.DiscourseApp"; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore org.discourse.DiscourseApp 1776778136"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore org.discourse.DiscourseApp"; SWIFT_OBJC_BRIDGING_HEADER = "Discourse-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -605,7 +607,10 @@ LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)"; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; - OTHER_LDFLAGS = "$(inherited) "; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; @@ -661,7 +666,10 @@ IPHONEOS_DEPLOYMENT_TARGET = 10.0; LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)"; MTL_ENABLE_DEBUG_INFO = NO; - OTHER_LDFLAGS = "$(inherited) "; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; @@ -681,10 +689,11 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; - CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = 6T3LU73T8S; + DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 6T3LU73T8S; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; @@ -695,8 +704,8 @@ MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = org.discourse.DiscourseApp.ShareExtension; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = "match AdHoc org.discourse.DiscourseApp.ShareExtension"; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AdHoc org.discourse.DiscourseApp.ShareExtension 1784743763"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AdHoc org.discourse.DiscourseApp.ShareExtension"; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -731,7 +740,7 @@ PRODUCT_BUNDLE_IDENTIFIER = org.discourse.DiscourseApp.ShareExtension; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AdHoc org.discourse.DiscourseApp.ShareExtension"; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AdHoc org.discourse.DiscourseApp.ShareExtension 1776778146"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore org.discourse.DiscourseApp.ShareExtension"; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; SWIFT_VERSION = 5.0; From 1da602589bf83ea44175e7e4e5f9f1b01a8d1157 Mon Sep 17 00:00:00 2001 From: Bannon Tanner Date: Wed, 16 Sep 2026 13:22:02 -0500 Subject: [PATCH 5/5] DEV: Update project files after build --- fastlane/Fastfile | 8 ++++---- ios/Discourse.xcodeproj/project.pbxproj | 16 ++++++---------- ios/Discourse/Info.plist | 4 ++-- ios/Discourse/PrivacyInfo.xcprivacy | 2 ++ ios/ShareExtension/Info.plist | 4 ++-- 5 files changed, 16 insertions(+), 18 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 0a4f00c02..7776fbafe 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -172,8 +172,8 @@ platform :ios do export_method: "app-store", export_options: { provisioningProfiles: { - "org.discourse.DiscourseApp" => "match AppStore org.discourse.DiscourseApp 1776778136", - "org.discourse.DiscourseApp.ShareExtension" => "match AppStore org.discourse.DiscourseApp.ShareExtension 1776778138" + "org.discourse.DiscourseApp" => "match AppStore org.discourse.DiscourseApp", + "org.discourse.DiscourseApp.ShareExtension" => "match AppStore org.discourse.DiscourseApp.ShareExtension" } }, clean: true, @@ -194,8 +194,8 @@ platform :ios do export_method: "ad-hoc", export_options: { provisioningProfiles: { - "org.discourse.DiscourseApp" => "match AdHoc org.discourse.DiscourseApp 1776778144", - "org.discourse.DiscourseApp.ShareExtension" => "match AdHoc org.discourse.DiscourseApp.ShareExtension 1776778146" + "org.discourse.DiscourseApp" => "match AdHoc org.discourse.DiscourseApp", + "org.discourse.DiscourseApp.ShareExtension" => "match AdHoc org.discourse.DiscourseApp.ShareExtension" } }, clean: true, diff --git a/ios/Discourse.xcodeproj/project.pbxproj b/ios/Discourse.xcodeproj/project.pbxproj index eb53ae5ae..3afa5210d 100644 --- a/ios/Discourse.xcodeproj/project.pbxproj +++ b/ios/Discourse.xcodeproj/project.pbxproj @@ -473,7 +473,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 705; + CURRENT_PROJECT_VERSION = 709; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 6T3LU73T8S; @@ -484,6 +484,7 @@ ); HEADER_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = Discourse/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Discourse; IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LIBRARY_SEARCH_PATHS = ( @@ -518,7 +519,7 @@ CODE_SIGN_ENTITLEMENTS = Discourse/Discourse.entitlements; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 705; + CURRENT_PROJECT_VERSION = 709; DEVELOPMENT_TEAM = 6T3LU73T8S; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 6T3LU73T8S; ENABLE_BITCODE = NO; @@ -528,6 +529,7 @@ ); HEADER_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = Discourse/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Discourse; IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LIBRARY_SEARCH_PATHS = ( @@ -607,10 +609,7 @@ LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)"; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; - OTHER_LDFLAGS = ( - "$(inherited)", - " ", - ); + OTHER_LDFLAGS = "$(inherited) "; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; @@ -666,10 +665,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 10.0; LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)"; MTL_ENABLE_DEBUG_INFO = NO; - OTHER_LDFLAGS = ( - "$(inherited)", - " ", - ); + OTHER_LDFLAGS = "$(inherited) "; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/ios/Discourse/Info.plist b/ios/Discourse/Info.plist index f7e7bc850..bb95d9b52 100644 --- a/ios/Discourse/Info.plist +++ b/ios/Discourse/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2.2.3 + 2.2.4 CFBundleSignature ???? CFBundleURLTypes @@ -37,7 +37,7 @@ CFBundleVersion - 705 + 709 Fabric APIKey diff --git a/ios/Discourse/PrivacyInfo.xcprivacy b/ios/Discourse/PrivacyInfo.xcprivacy index 3338c9ff4..6959894a4 100644 --- a/ios/Discourse/PrivacyInfo.xcprivacy +++ b/ios/Discourse/PrivacyInfo.xcprivacy @@ -18,6 +18,7 @@ NSPrivacyAccessedAPITypeReasons C617.1 + 0A2A.1 @@ -34,6 +35,7 @@ NSPrivacyAccessedAPITypeReasons 85F4.1 + E174.1 diff --git a/ios/ShareExtension/Info.plist b/ios/ShareExtension/Info.plist index d69056d19..2d134be74 100644 --- a/ios/ShareExtension/Info.plist +++ b/ios/ShareExtension/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 2.2.3 + 2.2.4 CFBundleVersion - 705 + 709 NSExtension NSExtensionAttributes