diff --git a/.github/workflows/native.yml b/.github/workflows/native.yml index 326bc99d6..e948921da 100644 --- a/.github/workflows/native.yml +++ b/.github/workflows/native.yml @@ -103,7 +103,7 @@ jobs: xcodebuild -version - name: Check the property lists - run: plutil -lint client/ios/App/App/Info.plist client/ios/ExportOptions.plist client/ios/App/NotificationService/Info.plist client/ios/App/NotificationService/NotificationService.entitlements client/ios/App/App/App.entitlements client/ios/App/TREKWidgets/Info.plist client/ios/App/TREKWidgets/TREKWidgets.entitlements client/ios/App/TREKWidgets/*.lproj/Localizable.strings + run: plutil -lint client/ios/App/App/Info.plist client/ios/ExportOptions.plist client/ios/App/NotificationService/Info.plist client/ios/App/NotificationService/NotificationService.entitlements client/ios/App/App/App.entitlements client/ios/App/TREKWidgets/Info.plist client/ios/App/TREKWidgets/TREKWidgets.entitlements client/ios/App/TREKWidgets/*.lproj/Localizable.strings client/ios/App/ShareExtension/Info.plist client/ios/App/ShareExtension/ShareExtension.entitlements client/ios/App/ShareExtension/*.lproj/Localizable.strings - uses: actions/setup-node@v6 with: diff --git a/client/fastlane/Fastfile b/client/fastlane/Fastfile index 11dd1da92..6b296e7cf 100644 --- a/client/fastlane/Fastfile +++ b/client/fastlane/Fastfile @@ -25,6 +25,7 @@ SIGNED_TARGETS = { "App" => APP_IDENTIFIER, "NotificationService" => "com.liketrek.trek.notifications", "TREKWidgets" => "com.liketrek.trek.widgets", + "ShareExtension" => "com.liketrek.trek.share", } platform :ios do diff --git a/client/ios/App/App.xcodeproj/project.pbxproj b/client/ios/App/App.xcodeproj/project.pbxproj index d0609efcd..dab077318 100644 --- a/client/ios/App/App.xcodeproj/project.pbxproj +++ b/client/ios/App/App.xcodeproj/project.pbxproj @@ -8,12 +8,16 @@ /* Begin PBXBuildFile section */ 01FD437981FC32C2C2C5BDCE /* TREKWidgets.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 702D0DF4A7E323CEA94CA03F /* TREKWidgets.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 0520A7C30D8D3FACCBB784BE /* ShareItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB2A15008B5664C48B639F94 /* ShareItem.swift */; }; 0C2D60A4950D7293E1F282CB /* TripDayAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 337775CD9091673D0ADDF325 /* TripDayAttributes.swift */; }; + 157A7B737E3FFB610A218D47 /* AtlasContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D004544572B0E85DE29A7B8 /* AtlasContent.swift */; }; + 1A0A6E71365C0A1178B85CB8 /* TrekStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15CAD7E43FE81EA6E2DD2585 /* TrekStyle.swift */; }; 2162CD349AFFDF070C16A58A /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E901288FC198E4D33BE754F /* SwiftUI.framework */; }; 2CFE220A73ACB585F3D4CB8F /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2EE43D236E661AA975612A4B /* UserNotifications.framework */; }; 2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; }; 32797E66C09CF7579AC323FD /* TREKWidgetsBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C2E47D0B005674C2B4ADA56 /* TREKWidgetsBundle.swift */; }; 3FB1872A25267A8E88FCBE3C /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BC64330AC19625949E8DC05A /* WidgetKit.framework */; }; + 45B990B86488D5CC2B5222B5 /* WorldGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AC751335E0136865E08990 /* WorldGrid.swift */; }; 492FA24FBB57059B0B5007CE /* TripDayAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 337775CD9091673D0ADDF325 /* TripDayAttributes.swift */; }; 4D22ABE92AF431CB00220026 /* CapApp-SPM in Frameworks */ = {isa = PBXBuildFile; productRef = 4D22ABE82AF431CB00220026 /* CapApp-SPM */; }; 50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; }; @@ -22,28 +26,32 @@ 504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; }; 504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; }; 50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; }; + 512499AF65E091B2BBC9A7BB /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E45050B942C96E1ACCAB7CE9 /* UniformTypeIdentifiers.framework */; }; + 5480CCC12F42E9605044AB6B /* ShareHandoff.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56050CCA8DE4BEB4680D9E0B /* ShareHandoff.swift */; }; + 5A4D344328829E0F0B4D2B94 /* BookingsContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6D2F4930E5F152C8090C4F8 /* BookingsContent.swift */; }; + 5BBE9EE02A47EC67C31F5E21 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D0A4B2C6C93971E884561CFA /* Localizable.strings */; }; + 612BB9680DC27BB8BF553060 /* TrekFamily.swift in Sources */ = {isa = PBXBuildFile; fileRef = A030A06726542A2D7F61C54B /* TrekFamily.swift */; }; 7A1E00032C0000000000A003 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 7A1E00022C0000000000A002 /* PrivacyInfo.xcprivacy */; }; 7A1E00052C0000000000A005 /* TrekBridgeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1E00042C0000000000A004 /* TrekBridgeViewController.swift */; }; 7A1E00072C0000000000A007 /* TrekShellPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1E00062C0000000000A006 /* TrekShellPlugin.swift */; }; 7FB730DC35C234816EAE65AF /* WidgetSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF6D74B7888714BF264E0E6 /* WidgetSnapshot.swift */; }; - D3C699E2258FBA6A11C1DDE4 /* TripIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8442B4B8988AF5D1BFA3ADE3 /* TripIntent.swift */; }; - 157A7B737E3FFB610A218D47 /* AtlasContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D004544572B0E85DE29A7B8 /* AtlasContent.swift */; }; - FEB91385917AC44D5CDAA59C /* PackingContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04C0771C8582246A26E0C98E /* PackingContent.swift */; }; - EA5EDDD4E5F9BC9BC46C563F /* CollabContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D5041DDDAFF5B491B053999 /* CollabContent.swift */; }; - 45B990B86488D5CC2B5222B5 /* WorldGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AC751335E0136865E08990 /* WorldGrid.swift */; }; - 1A0A6E71365C0A1178B85CB8 /* TrekStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15CAD7E43FE81EA6E2DD2585 /* TrekStyle.swift */; }; - 612BB9680DC27BB8BF553060 /* TrekFamily.swift in Sources */ = {isa = PBXBuildFile; fileRef = A030A06726542A2D7F61C54B /* TrekFamily.swift */; }; - 8FC5873BE6A8CB2AFB31A46A /* TripContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE6873D246AC6B8E2CB6C8B4 /* TripContent.swift */; }; - 9A175689CFCC630BE9BB9EE1 /* TodayContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24B175DE91E1E6E850DB0285 /* TodayContent.swift */; }; - 5A4D344328829E0F0B4D2B94 /* BookingsContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6D2F4930E5F152C8090C4F8 /* BookingsContent.swift */; }; - F1E29B18C5B1E75A5BC01B57 /* TodosContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEF7DCAA1AEE67F1291668D1 /* TodosContent.swift */; }; 84EB79E4B677229481809933 /* NotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = A03A384FE54FAD1ED68E12DC /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 8FC5873BE6A8CB2AFB31A46A /* TripContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE6873D246AC6B8E2CB6C8B4 /* TripContent.swift */; }; 9582B6832FE993A70072D4E8 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9582B6822FE993A50072D4E8 /* SceneDelegate.swift */; }; + 96DBDCE4766AC9F13F100A06 /* SharePage.js in Resources */ = {isa = PBXBuildFile; fileRef = 9DC696E1A1FD3A1A1695F9EA /* SharePage.js */; }; + 9A175689CFCC630BE9BB9EE1 /* TodayContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24B175DE91E1E6E850DB0285 /* TodayContent.swift */; }; + A0E8821CA7CF51AF2D6D8422 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 248A72D1C511B4969D89333A /* Localizable.strings */; }; + A2B048E183B19BB17E264FDE /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED561D7B7219D23A99FAEB11 /* ShareViewController.swift */; }; A8FDD4DBE3F078BF935CC4B9 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA8802204810ED580DE46F3 /* NotificationService.swift */; }; B4C0FC386F71D849E141C91E /* NextTripWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7064A9DA92FCC8ACAA4F22A /* NextTripWidget.swift */; }; B65B7CFC62EEFF443491EA64 /* ActivityKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 192E69C7E66646BC39566925 /* ActivityKit.framework */; }; + B8D1816D155C8D52DFCAE25B /* ShareView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6831E849332626BE8D54CEF /* ShareView.swift */; }; + BE2DBB926DB04D6CEEFD8B43 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 694631A1018DCE5678F29930 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + D3C699E2258FBA6A11C1DDE4 /* TripIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8442B4B8988AF5D1BFA3ADE3 /* TripIntent.swift */; }; DF633789F2F667C4CF264C9E /* TripDayLiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3259F6C40216CECEBAC6F307 /* TripDayLiveActivity.swift */; }; - A0E8821CA7CF51AF2D6D8422 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 248A72D1C511B4969D89333A /* Localizable.strings */; }; + EA5EDDD4E5F9BC9BC46C563F /* CollabContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D5041DDDAFF5B491B053999 /* CollabContent.swift */; }; + F1E29B18C5B1E75A5BC01B57 /* TodosContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEF7DCAA1AEE67F1291668D1 /* TodosContent.swift */; }; + FEB91385917AC44D5CDAA59C /* PackingContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04C0771C8582246A26E0C98E /* PackingContent.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -54,6 +62,13 @@ remoteGlobalIDString = 9480DFBC2C2A112973DA468A; remoteInfo = NotificationService; }; + 64B91219C04CABCE204C7CCC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 504EC2FC1FED79650016851F /* Project object */; + proxyType = 1; + remoteGlobalIDString = 01A63CD6CFBC1463CAA8FFF0; + remoteInfo = ShareExtension; + }; B8BBE7F7A64678D780A7F3D1 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 504EC2FC1FED79650016851F /* Project object */; @@ -72,6 +87,7 @@ files = ( 84EB79E4B677229481809933 /* NotificationService.appex in Embed Foundation Extensions */, 01FD437981FC32C2C2C5BDCE /* TREKWidgets.appex in Embed Foundation Extensions */, + BE2DBB926DB04D6CEEFD8B43 /* ShareExtension.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -79,13 +95,36 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 037DA05706AE051CEFCA7D75 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; + 04C0771C8582246A26E0C98E /* PackingContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PackingContent.swift; sourceTree = ""; }; + 090D74588DA8436DEC275567 /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = ""; }; + 095A4E5D36F6E22070842EC1 /* nl */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; + 0A7C4491A7ABDDBEBE00E5BF /* sk */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = ""; }; + 0BA66BF6D1F4FB57EFF31981 /* ShareExtension.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = ShareExtension.entitlements; sourceTree = ""; }; + 114C13FB91B554AE8DC4372B /* et */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/Localizable.strings; sourceTree = ""; }; + 12DE24E0B7E2E5F3564D952B /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/Localizable.strings; sourceTree = ""; }; + 15884CA899CAD3FFD98BA927 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = ""; }; + 15CAD7E43FE81EA6E2DD2585 /* TrekStyle.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TrekStyle.swift; sourceTree = ""; }; 192E69C7E66646BC39566925 /* ActivityKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ActivityKit.framework; path = System/Library/Frameworks/ActivityKit.framework; sourceTree = SDKROOT; }; + 1947CC62E5A6FF3483BEB578 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; + 1D8F2B5132A2F1B7089E767F /* vi */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = ""; }; 1E7522315D47F086145DC4B8 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 1ED347C91E03E51FDF8ABC1A /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = ""; }; + 22A904A2E55946892F488B1A /* th */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Localizable.strings; sourceTree = ""; }; + 241C37D114DDD53C7E915CB8 /* id */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = ""; }; + 24B175DE91E1E6E850DB0285 /* TodayContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TodayContent.swift; sourceTree = ""; }; 2EE43D236E661AA975612A4B /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; }; 2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = ""; }; 3259F6C40216CECEBAC6F307 /* TripDayLiveActivity.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TripDayLiveActivity.swift; sourceTree = ""; }; + 32A62EE30F8AD514D7DB2CAA /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; 337775CD9091673D0ADDF325 /* TripDayAttributes.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TripDayAttributes.swift; sourceTree = ""; }; + 38C632E7EE9C0DF1CA0BF0A0 /* cs */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; + 3CB565FC61AB5AEA87A3F615 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = ""; }; 3DF4F4305878F949DD09193F /* NotificationService.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = NotificationService.entitlements; sourceTree = ""; }; + 405B04DFF3A978D1046147C6 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/Localizable.strings; sourceTree = ""; }; + 408F0E543D49BFCCF6B19194 /* es */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; + 4167040E2C29E44D55580916 /* ru */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; + 4D004544572B0E85DE29A7B8 /* AtlasContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AtlasContent.swift; sourceTree = ""; }; 50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = ""; }; 504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; }; 504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -94,61 +133,75 @@ 504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = ""; }; + 56050CCA8DE4BEB4680D9E0B /* ShareHandoff.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ShareHandoff.swift; sourceTree = ""; }; + 5C0109EA628FA897B7BFDD3E /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Localizable.strings; sourceTree = ""; }; + 608F00CBD96957EEA888E50A /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; + 60EB00F3670B41B898765DF9 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; + 611A99D899DC374A5B7E1560 /* tr */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; + 63D3A4166431A36A5C30F942 /* az */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = az; path = az.lproj/Localizable.strings; sourceTree = ""; }; + 66E7B341F942576F4BF6E4BA /* zh-Hans */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; + 683725459244601B71D92E2D /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; + 6932A417DC4BC05BF07D80E2 /* it */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; + 694631A1018DCE5678F29930 /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 6E901288FC198E4D33BE754F /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; 702D0DF4A7E323CEA94CA03F /* TREKWidgets.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = TREKWidgets.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 7328217E23761C22814FEC25 /* az */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = az; path = az.lproj/Localizable.strings; sourceTree = ""; }; + 77A0112CA76E5C1787073AEB /* hu */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = ""; }; + 77C217524DA98A6A92F422F4 /* zh-Hant */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = ""; }; 7A1E00012C0000000000A001 /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = ""; }; 7A1E00022C0000000000A002 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 7A1E00042C0000000000A004 /* TrekBridgeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrekBridgeViewController.swift; sourceTree = ""; }; 7A1E00062C0000000000A006 /* TrekShellPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrekShellPlugin.swift; sourceTree = ""; }; + 7D5041DDDAFF5B491B053999 /* CollabContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CollabContent.swift; sourceTree = ""; }; + 7D525040492AFBC121FD4CC6 /* ar */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; }; + 8442B4B8988AF5D1BFA3ADE3 /* TripIntent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TripIntent.swift; sourceTree = ""; }; + 84CB5C3B6BD4E1DE1A8BD2B4 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + 8A0289CB321AE008D8815A19 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; 8C2E47D0B005674C2B4ADA56 /* TREKWidgetsBundle.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TREKWidgetsBundle.swift; sourceTree = ""; }; + 8D361F7B14AE9F5ACE3BFE78 /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/Localizable.strings; sourceTree = ""; }; 9582B6822FE993A50072D4E8 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 958DCC722DB07C7200EA8C5F /* debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = debug.xcconfig; path = ../debug.xcconfig; sourceTree = SOURCE_ROOT; }; + 9A3FAA9DBC67D12389EEF0CF /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; + 9D673E1909A5B750376ECB41 /* en */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + 9DC696E1A1FD3A1A1695F9EA /* SharePage.js */ = {isa = PBXFileReference; includeInIndex = 1; path = SharePage.js; sourceTree = ""; }; + 9F756414EFBB3542F65C14CE /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = ""; }; + A030A06726542A2D7F61C54B /* TrekFamily.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TrekFamily.swift; sourceTree = ""; }; A03A384FE54FAD1ED68E12DC /* NotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + A06BBDA4CF70465F2E3F21D3 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; + A2F3670F49C3D74C3D98E457 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = ""; }; + A479036A85683EFF2922D096 /* el */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = ""; }; + A5C182FC253CBEE8D5439536 /* ca */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/Localizable.strings; sourceTree = ""; }; + AA02F5CBC22E08415CC1C9BE /* pl */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; + AEF7DCAA1AEE67F1291668D1 /* TodosContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TodosContent.swift; sourceTree = ""; }; + B0450E66BFCF00CC4097330A /* he */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/Localizable.strings; sourceTree = ""; }; + B680CBF1268D2EE1982B165C /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = ""; }; + B7F33B7A5C27C126DF29DCA4 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = ""; }; + BB2A15008B5664C48B639F94 /* ShareItem.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ShareItem.swift; sourceTree = ""; }; BBB2E19CD540AB30B44BECCA /* TREKWidgets.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = TREKWidgets.entitlements; sourceTree = ""; }; BC64330AC19625949E8DC05A /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; BE41D7212DCB0C78E00A8462 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; C7064A9DA92FCC8ACAA4F22A /* NextTripWidget.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NextTripWidget.swift; sourceTree = ""; }; - DBF6D74B7888714BF264E0E6 /* WidgetSnapshot.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WidgetSnapshot.swift; sourceTree = ""; }; - 8442B4B8988AF5D1BFA3ADE3 /* TripIntent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TripIntent.swift; sourceTree = ""; }; - 4D004544572B0E85DE29A7B8 /* AtlasContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AtlasContent.swift; sourceTree = ""; }; - 04C0771C8582246A26E0C98E /* PackingContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PackingContent.swift; sourceTree = ""; }; - 7D5041DDDAFF5B491B053999 /* CollabContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CollabContent.swift; sourceTree = ""; }; + C996A3060A2D20A4B5C9C377 /* ko */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = ""; }; + CC077989456FB9DB5F4FF676 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = ""; }; + CD6763B965A87DDB6FCC2D05 /* de */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; + CE79157F1170A47CF7FC6FE0 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D0AC751335E0136865E08990 /* WorldGrid.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WorldGrid.swift; sourceTree = ""; }; - 15CAD7E43FE81EA6E2DD2585 /* TrekStyle.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TrekStyle.swift; sourceTree = ""; }; - A030A06726542A2D7F61C54B /* TrekFamily.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TrekFamily.swift; sourceTree = ""; }; - DE6873D246AC6B8E2CB6C8B4 /* TripContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TripContent.swift; sourceTree = ""; }; - 24B175DE91E1E6E850DB0285 /* TodayContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TodayContent.swift; sourceTree = ""; }; - E6D2F4930E5F152C8090C4F8 /* BookingsContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BookingsContent.swift; sourceTree = ""; }; - AEF7DCAA1AEE67F1291668D1 /* TodosContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TodosContent.swift; sourceTree = ""; }; - FFA8802204810ED580DE46F3 /* NotificationService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; - 84CB5C3B6BD4E1DE1A8BD2B4 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; - EE30A6DF943052C6ADE92D63 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; - ECC80E1314B31ED2D2347411 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; }; - 7328217E23761C22814FEC25 /* az */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = az; path = az.lproj/Localizable.strings; sourceTree = ""; }; - CC077989456FB9DB5F4FF676 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt-BR; path = pt-BR.lproj/Localizable.strings; sourceTree = ""; }; - 405B04DFF3A978D1046147C6 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/Localizable.strings; sourceTree = ""; }; + D84D92C18CC022646692CB9B /* sv */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = ""; }; + DBF6D74B7888714BF264E0E6 /* WidgetSnapshot.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WidgetSnapshot.swift; sourceTree = ""; }; + DC0C9AB4D77D77A7BC2BA85B /* ja */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; DD341749E47B7DF89BA9A46D /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; - 9A3FAA9DBC67D12389EEF0CF /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; - 8D361F7B14AE9F5ACE3BFE78 /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/Localizable.strings; sourceTree = ""; }; - 60EB00F3670B41B898765DF9 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; - B680CBF1268D2EE1982B165C /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = ""; }; - 12DE24E0B7E2E5F3564D952B /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/Localizable.strings; sourceTree = ""; }; - 15884CA899CAD3FFD98BA927 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = ""; }; - A2F3670F49C3D74C3D98E457 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = ""; }; - 1947CC62E5A6FF3483BEB578 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; - 8A0289CB321AE008D8815A19 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; - B7F33B7A5C27C126DF29DCA4 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = ""; }; - 683725459244601B71D92E2D /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; - 32A62EE30F8AD514D7DB2CAA /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; - 037DA05706AE051CEFCA7D75 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; - 090D74588DA8436DEC275567 /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = ""; }; - 9F756414EFBB3542F65C14CE /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = ""; }; - 5C0109EA628FA897B7BFDD3E /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Localizable.strings; sourceTree = ""; }; + DE6873D246AC6B8E2CB6C8B4 /* TripContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TripContent.swift; sourceTree = ""; }; + DE8B592FE52EC6FFB589751E /* fr */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; + E2CFBDBCC474A7A72E053A8E /* uk */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; + E45050B942C96E1ACCAB7CE9 /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; }; E5800E4ECECED54A47CDABAA /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; - A06BBDA4CF70465F2E3F21D3 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; - 3CB565FC61AB5AEA87A3F615 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = ""; }; - 608F00CBD96957EEA888E50A /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh-Hans; path = zh-Hans.lproj/Localizable.strings; sourceTree = ""; }; - 1ED347C91E03E51FDF8ABC1A /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh-Hant; path = zh-Hant.lproj/Localizable.strings; sourceTree = ""; }; + E6831E849332626BE8D54CEF /* ShareView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ShareView.swift; sourceTree = ""; }; + E6D2F4930E5F152C8090C4F8 /* BookingsContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BookingsContent.swift; sourceTree = ""; }; + EAFBDD97043B34E4B64FDA86 /* pt-BR */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = ""; }; + ECC80E1314B31ED2D2347411 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; }; + ED561D7B7219D23A99FAEB11 /* ShareViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; + EE30A6DF943052C6ADE92D63 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; + FFA8802204810ED580DE46F3 /* NotificationService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -178,6 +231,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DB289D9657376A832309DE5D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 512499AF65E091B2BBC9A7BB /* UniformTypeIdentifiers.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -197,6 +258,7 @@ 5198C9768EB9762A60701C92 /* Frameworks */, 74C1FC6022E562372A71EEC9 /* NotificationService */, FB9226BC82835538BCE3DC0A /* TREKWidgets */, + DFD2010504102AC0E682D00F /* ShareExtension */, ); sourceTree = ""; }; @@ -206,6 +268,7 @@ 504EC3041FED79650016851F /* App.app */, A03A384FE54FAD1ED68E12DC /* NotificationService.appex */, 702D0DF4A7E323CEA94CA03F /* TREKWidgets.appex */, + 694631A1018DCE5678F29930 /* ShareExtension.appex */, ); name = Products; sourceTree = ""; @@ -238,6 +301,7 @@ BC64330AC19625949E8DC05A /* WidgetKit.framework */, 6E901288FC198E4D33BE754F /* SwiftUI.framework */, 192E69C7E66646BC39566925 /* ActivityKit.framework */, + E45050B942C96E1ACCAB7CE9 /* UniformTypeIdentifiers.framework */, ); name = Frameworks; sourceTree = ""; @@ -253,6 +317,22 @@ path = NotificationService; sourceTree = ""; }; + DFD2010504102AC0E682D00F /* ShareExtension */ = { + isa = PBXGroup; + children = ( + CE79157F1170A47CF7FC6FE0 /* Info.plist */, + 0BA66BF6D1F4FB57EFF31981 /* ShareExtension.entitlements */, + 56050CCA8DE4BEB4680D9E0B /* ShareHandoff.swift */, + BB2A15008B5664C48B639F94 /* ShareItem.swift */, + 9DC696E1A1FD3A1A1695F9EA /* SharePage.js */, + E6831E849332626BE8D54CEF /* ShareView.swift */, + ED561D7B7219D23A99FAEB11 /* ShareViewController.swift */, + D0A4B2C6C93971E884561CFA /* Localizable.strings */, + ); + name = ShareExtension; + path = ShareExtension; + sourceTree = ""; + }; FB9226BC82835538BCE3DC0A /* TREKWidgets */ = { isa = PBXGroup; children = ( @@ -283,6 +363,23 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 01A63CD6CFBC1463CAA8FFF0 /* ShareExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1369CF6148320C447EBF3B4A /* Build configuration list for PBXNativeTarget "ShareExtension" */; + buildPhases = ( + E0B13DA0CF9C1103058FE869 /* Sources */, + DB289D9657376A832309DE5D /* Frameworks */, + EB430D4F409D7961AF5603C9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ShareExtension; + productName = ShareExtension; + productReference = 694631A1018DCE5678F29930 /* ShareExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; 504EC3031FED79650016851F /* App */ = { isa = PBXNativeTarget; buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */; @@ -297,6 +394,7 @@ dependencies = ( E23A0A46599B8EE7A5B74F45 /* PBXTargetDependency */, 4CD7F5AF02CB806E4189489A /* PBXTargetDependency */, + 855AE76937B942D62DFFD7E2 /* PBXTargetDependency */, ); name = App; packageProductDependencies = ( @@ -407,6 +505,7 @@ 504EC3031FED79650016851F /* App */, 9480DFBC2C2A112973DA468A /* NotificationService */, A3BE5653156025C97CA62C68 /* TREKWidgets */, + 01A63CD6CFBC1463CAA8FFF0 /* ShareExtension */, ); }; /* End PBXProject section */ @@ -441,6 +540,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + EB430D4F409D7961AF5603C9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 96DBDCE4766AC9F13F100A06 /* SharePage.js in Resources */, + 5BBE9EE02A47EC67C31F5E21 /* Localizable.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -487,6 +595,17 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + E0B13DA0CF9C1103058FE869 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5480CCC12F42E9605044AB6B /* ShareHandoff.swift in Sources */, + 0520A7C30D8D3FACCBB784BE /* ShareItem.swift in Sources */, + B8D1816D155C8D52DFCAE25B /* ShareView.swift in Sources */, + A2B048E183B19BB17E264FDE /* ShareViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -496,6 +615,12 @@ target = A3BE5653156025C97CA62C68 /* TREKWidgets */; targetProxy = B8BBE7F7A64678D780A7F3D1 /* PBXContainerItemProxy */; }; + 855AE76937B942D62DFFD7E2 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = ShareExtension; + target = 01A63CD6CFBC1463CAA8FFF0 /* ShareExtension */; + targetProxy = 64B91219C04CABCE204C7CCC /* PBXContainerItemProxy */; + }; E23A0A46599B8EE7A5B74F45 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = NotificationService; @@ -556,9 +681,69 @@ name = LaunchScreen.storyboard; sourceTree = ""; }; + D0A4B2C6C93971E884561CFA /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 7D525040492AFBC121FD4CC6 /* ar */, + 63D3A4166431A36A5C30F942 /* az */, + A5C182FC253CBEE8D5439536 /* ca */, + 38C632E7EE9C0DF1CA0BF0A0 /* cs */, + CD6763B965A87DDB6FCC2D05 /* de */, + A479036A85683EFF2922D096 /* el */, + 9D673E1909A5B750376ECB41 /* en */, + 408F0E543D49BFCCF6B19194 /* es */, + 114C13FB91B554AE8DC4372B /* et */, + DE8B592FE52EC6FFB589751E /* fr */, + B0450E66BFCF00CC4097330A /* he */, + 77A0112CA76E5C1787073AEB /* hu */, + 241C37D114DDD53C7E915CB8 /* id */, + 6932A417DC4BC05BF07D80E2 /* it */, + DC0C9AB4D77D77A7BC2BA85B /* ja */, + C996A3060A2D20A4B5C9C377 /* ko */, + 095A4E5D36F6E22070842EC1 /* nl */, + AA02F5CBC22E08415CC1C9BE /* pl */, + EAFBDD97043B34E4B64FDA86 /* pt-BR */, + 4167040E2C29E44D55580916 /* ru */, + 0A7C4491A7ABDDBEBE00E5BF /* sk */, + D84D92C18CC022646692CB9B /* sv */, + 22A904A2E55946892F488B1A /* th */, + 611A99D899DC374A5B7E1560 /* tr */, + E2CFBDBCC474A7A72E053A8E /* uk */, + 1D8F2B5132A2F1B7089E767F /* vi */, + 66E7B341F942576F4BF6E4BA /* zh-Hans */, + 77C217524DA98A6A92F422F4 /* zh-Hant */, + ); + name = Localizable.strings; + sourceTree = ""; + }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 31CC9E868E52D06AD12E83D6 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = NO; + CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = ShareExtension/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.liketrek.trek.share; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; 3ECF3537A139DB5CAC23E502 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -739,6 +924,32 @@ }; name = Release; }; + 5779B79420A5ECC25B0E8ACC /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = NO; + CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = ShareExtension/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.liketrek.trek.share; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; 686FA31AEC6FE5BA3EF95E99 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -819,6 +1030,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 1369CF6148320C447EBF3B4A /* Build configuration list for PBXNativeTarget "ShareExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5779B79420A5ECC25B0E8ACC /* Release */, + 31CC9E868E52D06AD12E83D6 /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 4E3E5FA93A15425251E22D48 /* Build configuration list for PBXNativeTarget "NotificationService" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/client/ios/App/App/SceneDelegate.swift b/client/ios/App/App/SceneDelegate.swift index f81a497d6..f8c5f7479 100644 --- a/client/ios/App/App/SceneDelegate.swift +++ b/client/ios/App/App/SceneDelegate.swift @@ -26,6 +26,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { func sceneDidBecomeActive(_ scene: UIScene) { TrekPrivacyCover.hide() + // Whatever the share sheet left, even if it could not bring the app forward itself. + TrekSharedFiles.receiveFromShareExtension() } func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { @@ -33,7 +35,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // else (the sign-in callback) stays with Capacitor. let files = URLContexts.filter { $0.url.isFileURL } for context in files { TrekSharedFiles.receive(context.url) } - let others = URLContexts.subtracting(files) + // com.liketrek.trek://share: the share sheet left an item for the import. + let handoffs = URLContexts.filter { $0.url.scheme == "com.liketrek.trek" && $0.url.host == "share" } + if !handoffs.isEmpty { TrekSharedFiles.receiveFromShareExtension() } + let others = URLContexts.subtracting(files).subtracting(handoffs) if !others.isEmpty { SceneDelegateProxy.shared.scene(scene, openURLContexts: others) } } @@ -90,6 +95,25 @@ enum TrekSharedFiles { onReceive?(file) } + /// The item the share extension stored (ShareExtension/ShareHandoff.swift), with + /// the trip picked for it there. Taken once: the keychain is cleared right away. + static func receiveFromShareExtension() { + guard let meta = TrekSharedKeychain.read("share.pending.meta"), + let data = TrekSharedKeychain.read("share.pending.data") else { return } + TrekSharedKeychain.delete("share.pending.meta") + TrekSharedKeychain.delete("share.pending.data") + guard let info = try? JSONSerialization.jsonObject(with: meta) as? [String: Any], + let name = info["name"] as? String, data.count <= maxBytes else { return } + var file: [String: Any] = [ + "name": name, + "mimeType": info["mimeType"] as? String ?? mimeType(for: (name as NSString).pathExtension.lowercased()), + "data": data.base64EncodedString(), + ] + if let tripId = info["tripId"] as? Int { file["tripId"] = tripId } + pending = file + onReceive?(file) + } + static func take() -> [String: Any]? { defer { pending = nil } return pending diff --git a/client/ios/App/App/TrekShellPlugin.swift b/client/ios/App/App/TrekShellPlugin.swift index 2a2d0526b..65ffe10c6 100644 --- a/client/ios/App/App/TrekShellPlugin.swift +++ b/client/ios/App/App/TrekShellPlugin.swift @@ -51,7 +51,7 @@ public class TrekShellPlugin: CAPPlugin, CAPBridgedPlugin, ASWebAuthenticationPr /// and a server can require a minimum (minNativeShellApi in /api/health/features). static let shellApi = 2 /// The native features by name, so the UI can turn each on by itself. - static let features: [String] = ["capabilities", "edgeBack", "tileCache", "biometricLock", "geolocation", "sharedFiles", "nativePush", "widgets", "liveActivity", "widgetTrips", "storeKit"] + static let features: [String] = ["capabilities", "edgeBack", "tileCache", "biometricLock", "geolocation", "sharedFiles", "nativePush", "widgets", "liveActivity", "widgetTrips", "storeKit", "shareSheet"] // MARK: - Shared booking documents @@ -716,6 +716,10 @@ enum TrekSharedKeychain { return SecItemCopyMatching(q as CFDictionary, &result) == errSecSuccess ? result as? Data : nil } + static func delete(_ account: String) { + SecItemDelete(query(account) as CFDictionary) + } + @discardableResult static func write(_ account: String, _ data: Data) -> Bool { SecItemDelete(query(account) as CFDictionary) diff --git a/client/ios/App/ShareExtension/Info.plist b/client/ios/App/ShareExtension/Info.plist new file mode 100644 index 000000000..13787fb68 --- /dev/null +++ b/client/ios/App/ShareExtension/Info.plist @@ -0,0 +1,53 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + TREK + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSExtension + + NSExtensionAttributes + + NSExtensionActivationRule + SUBQUERY ( + extensionItems, + $extensionItem, + SUBQUERY ( + $extensionItem.attachments, + $attachment, + ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.apple.pkpass" + || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.adobe.pdf" + || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.email-message" + || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.apple.mail.email" + || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.html" + || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url" + || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.plain-text" + || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.image" + ).@count == 1 +).@count == 1 + NSExtensionJavaScriptPreprocessingFile + SharePage + + NSExtensionPointIdentifier + com.apple.share-services + NSExtensionPrincipalClass + ShareViewController + + + diff --git a/client/ios/App/ShareExtension/ShareExtension.entitlements b/client/ios/App/ShareExtension/ShareExtension.entitlements new file mode 100644 index 000000000..6b6566872 --- /dev/null +++ b/client/ios/App/ShareExtension/ShareExtension.entitlements @@ -0,0 +1,10 @@ + + + + + keychain-access-groups + + $(AppIdentifierPrefix)com.liketrek.trek.shared + + + diff --git a/client/ios/App/ShareExtension/ShareHandoff.swift b/client/ios/App/ShareExtension/ShareHandoff.swift new file mode 100644 index 000000000..0eaf6db2c --- /dev/null +++ b/client/ios/App/ShareExtension/ShareHandoff.swift @@ -0,0 +1,93 @@ +import Foundation +import Security + +/// The trips the share sheet offers, and the hand-over of the shared item to the +/// app. Both go through the keychain group the app and its extensions share +/// (TrekSharedKeychain in the app): the app keeps its trip list there for the +/// widgets already, and the app reads the item back the moment it opens +/// (TrekSharedFiles.receiveFromShareExtension). +enum ShareHandoff { + private static let service = "com.liketrek.trek.push" + static let metaAccount = "share.pending.meta" + static let dataAccount = "share.pending.data" + + struct Trip: Identifiable, Equatable { + var id: Int + var title: String + var start: Date? + var end: Date? + var place: String? + /// A small cover photo, from the trip's widget snapshot when the app wrote one. + var cover: Data? + } + + private struct TripRef: Decodable { + var id: Int + var title: String + var start: String + var end: String + } + + private struct Snapshot: Decodable { + struct Trip: Decodable { + var place: String? + var cover: String? + } + var trip: Trip? + } + + /// The trips that have not ended yet, soonest first, then the most recent past ones. + static func trips(now: Date = Date()) -> [Trip] { + guard let raw = read("widget.trips"), let refs = try? JSONDecoder().decode([TripRef].self, from: raw) else { return [] } + let today = Calendar.current.startOfDay(for: now) + let all = refs.map { ref -> Trip in + let snapshot = read("widget.trip.\(ref.id)").flatMap { try? JSONDecoder().decode(Snapshot.self, from: $0) } + return Trip(id: ref.id, title: ref.title, start: day(ref.start), end: day(ref.end), + place: snapshot?.trip?.place, cover: snapshot?.trip?.cover.flatMap { Data(base64Encoded: $0) }) + } + let ahead = all.filter { ($0.end ?? .distantFuture) >= today }.sorted { ($0.start ?? .distantFuture) < ($1.start ?? .distantFuture) } + let past = all.filter { ($0.end ?? .distantFuture) < today }.sorted { ($0.end ?? .distantPast) > ($1.end ?? .distantPast) } + return ahead + past.prefix(6) + } + + /// Leaves the item for the app, with the trip it goes to (nil: the app asks). + static func store(_ item: ShareItem, tripId: Int?) -> Bool { + var meta: [String: Any] = ["name": item.name, "mimeType": item.mimeType] + if let tripId { meta["tripId"] = tripId } + guard let metaData = try? JSONSerialization.data(withJSONObject: meta) else { return false } + return write(dataAccount, item.data) && write(metaAccount, metaData) + } + + private static func day(_ iso: String) -> Date? { + let formatter = DateFormatter() + formatter.calendar = Calendar(identifier: .gregorian) + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.dateFormat = "yyyy-MM-dd" + return formatter.date(from: String(iso.prefix(10))) + } + + // The default access group is the first one in the entitlement, the shared one. + private static func query(_ account: String) -> [String: Any] { + [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: account, + ] + } + + private static func read(_ account: String) -> Data? { + var q = query(account) + q[kSecReturnData as String] = true + var result: CFTypeRef? + guard SecItemCopyMatching(q as CFDictionary, &result) == errSecSuccess else { return nil } + return result as? Data + } + + private static func write(_ account: String, _ data: Data) -> Bool { + SecItemDelete(query(account) as CFDictionary) + var q = query(account) + q[kSecValueData as String] = data + q[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock + return SecItemAdd(q as CFDictionary, nil) == errSecSuccess + } +} diff --git a/client/ios/App/ShareExtension/ShareItem.swift b/client/ios/App/ShareExtension/ShareItem.swift new file mode 100644 index 000000000..bdea45ca2 --- /dev/null +++ b/client/ios/App/ShareExtension/ShareItem.swift @@ -0,0 +1,211 @@ +import Foundation +import UIKit +import UniformTypeIdentifiers + +/// What was shared to TREK, read into a file the booking import takes: a Wallet +/// pass, a PDF, a mail, a web page as Safari rendered it, a piece of text or a +/// photo. The app's booking import (BookingImportModal) decides what it can read; +/// this only carries the bytes over with a name and a type. +struct ShareItem { + enum Kind: String { + case pass, pdf, mail, page, text, image, file + + var symbol: String { + switch self { + case .pass: return "wallet.pass.fill" + case .pdf: return "doc.richtext.fill" + case .mail: return "envelope.fill" + case .page: return "safari.fill" + case .text: return "text.alignleft" + case .image: return "photo.fill" + case .file: return "doc.fill" + } + } + } + + var kind: Kind + /// The file name the import sees. + var name: String + var mimeType: String + var data: Data + /// What the sheet shows: a page title, a file name, the first line of a text. + var title: String + /// A host, for a page. + var source: String? + + var sizeLabel: String { + ByteCountFormatter.string(fromByteCount: Int64(data.count), countStyle: .file) + } +} + +enum ShareLoadError: Error { + case unsupported + case tooLarge +} + +enum ShareLoader { + /// The keychain carries the item to the app, so it has to stay modest; a + /// booking confirmation is far below this. + static let maxBytes = 10 * 1024 * 1024 + + private static let passType = "com.apple.pkpass" + private static let mailTypes = ["com.apple.mail.email", "public.email-message"] + + static func load(_ items: [NSExtensionItem]) async throws -> ShareItem { + let providers = items.flatMap { $0.attachments ?? [] } + guard !providers.isEmpty else { throw ShareLoadError.unsupported } + + // Safari hands over the page as it is rendered (SharePage.js), which also + // covers confirmation pages behind a sign-in that a fresh download would miss. + for provider in providers where provider.hasItemConformingToTypeIdentifier(UTType.propertyList.identifier) { + if let page = try? await pageFromSafari(provider) { return page } + } + for provider in providers where provider.hasItemConformingToTypeIdentifier(passType) { + return try await file(provider, type: passType, kind: .pass, ext: "pkpass", mime: "application/vnd.apple.pkpass") + } + for provider in providers where provider.hasItemConformingToTypeIdentifier(UTType.pdf.identifier) { + return try await file(provider, type: UTType.pdf.identifier, kind: .pdf, ext: "pdf", mime: "application/pdf") + } + for type in mailTypes { + for provider in providers where provider.hasItemConformingToTypeIdentifier(type) { + return try await file(provider, type: type, kind: .mail, ext: "eml", mime: "message/rfc822") + } + } + for provider in providers where provider.hasItemConformingToTypeIdentifier(UTType.html.identifier) { + return try await file(provider, type: UTType.html.identifier, kind: .page, ext: "html", mime: "text/html") + } + for provider in providers where provider.hasItemConformingToTypeIdentifier(UTType.image.identifier) { + return try await image(provider) + } + for provider in providers where provider.hasItemConformingToTypeIdentifier(UTType.url.identifier) { + if let url = try await provider.loadShared(UTType.url.identifier) as? URL { + if url.isFileURL { return try fromFile(url) } + return try await page(at: url) + } + } + for provider in providers where provider.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) { + let item = try await provider.loadShared(UTType.plainText.identifier) + let text = (item as? String) ?? (item as? Data).flatMap { String(data: $0, encoding: .utf8) } ?? "" + let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { throw ShareLoadError.unsupported } + let firstLine = trimmed.split(whereSeparator: \.isNewline).first.map(String.init) ?? trimmed + return try checked(ShareItem(kind: .text, name: "shared-text.txt", mimeType: "text/plain", + data: Data(trimmed.utf8), title: String(firstLine.prefix(80)), source: nil)) + } + throw ShareLoadError.unsupported + } + + // MARK: - Kinds + + private static func file(_ provider: NSItemProvider, type: String, kind: ShareItem.Kind, ext: String, mime: String) async throws -> ShareItem { + let item = try await provider.loadShared(type) + var name = provider.suggestedName.map { $0.hasSuffix(".\(ext)") ? $0 : "\($0).\(ext)" } ?? "booking.\(ext)" + let data: Data + if let url = item as? URL { + data = try read(url) + name = url.lastPathComponent + } else if let raw = item as? Data { + data = raw + } else { + throw ShareLoadError.unsupported + } + return try checked(ShareItem(kind: kind, name: name, mimeType: mime, data: data, + title: (name as NSString).deletingPathExtension, source: nil)) + } + + private static func image(_ provider: NSItemProvider) async throws -> ShareItem { + let item = try await provider.loadShared(UTType.image.identifier) + var picture: UIImage? + if let url = item as? URL { picture = UIImage(data: try read(url)) } + if let data = item as? Data { picture = UIImage(data: data) } + if let shown = item as? UIImage { picture = shown } + guard let picture, let jpeg = picture.jpegData(compressionQuality: 0.85) else { throw ShareLoadError.unsupported } + let name = (provider.suggestedName ?? "photo") + ".jpg" + return try checked(ShareItem(kind: .image, name: name, mimeType: "image/jpeg", data: jpeg, + title: provider.suggestedName ?? name, source: nil)) + } + + private static func pageFromSafari(_ provider: NSItemProvider) async throws -> ShareItem? { + let item = try await provider.loadShared(UTType.propertyList.identifier) + guard let dict = item as? NSDictionary, + let results = dict[NSExtensionJavaScriptPreprocessingResultsKey] as? [String: Any], + let html = results["html"] as? String, !html.isEmpty else { return nil } + let url = (results["url"] as? String).flatMap(URL.init(string:)) + let title = (results["title"] as? String).flatMap { $0.isEmpty ? nil : $0 } ?? url?.host ?? "Page" + return try checked(ShareItem(kind: .page, name: "\(slug(title)).html", mimeType: "text/html", + data: Data(html.utf8), title: title, source: url?.host)) + } + + /// A link shared from an app other than Safari: the page as the server sends it. + private static func page(at url: URL) async throws -> ShareItem { + guard url.scheme == "https" || url.scheme == "http" else { throw ShareLoadError.unsupported } + var request = URLRequest(url: url, timeoutInterval: 10) + request.setValue("text/html", forHTTPHeaderField: "Accept") + let (data, response) = try await URLSession.shared.data(for: request) + guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode), + (http.mimeType ?? "").contains("html") else { throw ShareLoadError.unsupported } + let html = String(decoding: data, as: UTF8.self) + let title = pageTitle(html) ?? url.host ?? "Page" + return try checked(ShareItem(kind: .page, name: "\(slug(title)).html", mimeType: "text/html", + data: data, title: title, source: url.host)) + } + + private static func fromFile(_ url: URL) throws -> ShareItem { + let data = try read(url) + let ext = url.pathExtension.lowercased() + let (kind, mime): (ShareItem.Kind, String) = { + switch ext { + case "pkpass": return (.pass, "application/vnd.apple.pkpass") + case "pdf": return (.pdf, "application/pdf") + case "eml": return (.mail, "message/rfc822") + case "html", "htm": return (.page, "text/html") + case "txt": return (.text, "text/plain") + default: return (.file, "application/octet-stream") + } + }() + return try checked(ShareItem(kind: kind, name: url.lastPathComponent, mimeType: mime, data: data, + title: url.deletingPathExtension().lastPathComponent, source: nil)) + } + + // MARK: - Helpers + + private static func read(_ url: URL) throws -> Data { + let scoped = url.startAccessingSecurityScopedResource() + defer { if scoped { url.stopAccessingSecurityScopedResource() } } + if let size = try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize, size > maxBytes { + throw ShareLoadError.tooLarge + } + return try Data(contentsOf: url) + } + + private static func checked(_ item: ShareItem) throws -> ShareItem { + guard !item.data.isEmpty else { throw ShareLoadError.unsupported } + guard item.data.count <= maxBytes else { throw ShareLoadError.tooLarge } + return item + } + + private static func pageTitle(_ html: String) -> String? { + guard let open = html.range(of: "", range: open.upperBound..", options: .caseInsensitive, range: start.upperBound.. String { + let cleaned = title.lowercased().components(separatedBy: CharacterSet.alphanumerics.inverted).filter { !$0.isEmpty } + let joined = cleaned.prefix(8).joined(separator: "-") + return joined.isEmpty ? "page" : joined + } +} + +private extension NSItemProvider { + func loadShared(_ type: String) async throws -> NSSecureCoding? { + try await withCheckedThrowingContinuation { continuation in + loadItem(forTypeIdentifier: type, options: nil) { item, error in + if let error { continuation.resume(throwing: error) } else { continuation.resume(returning: item) } + } + } + } +} diff --git a/client/ios/App/ShareExtension/SharePage.js b/client/ios/App/ShareExtension/SharePage.js new file mode 100644 index 000000000..f8318652c --- /dev/null +++ b/client/ios/App/ShareExtension/SharePage.js @@ -0,0 +1,17 @@ +// Safari runs this in the page before the share sheet opens (Info.plist, +// NSExtensionJavaScriptPreprocessingFile). It hands over the page as the user +// sees it, so a booking confirmation behind a sign-in reaches TREK whole. +var SharePage = function () {}; + +SharePage.prototype = { + run: function (args) { + args.completionFunction({ + html: document.documentElement.outerHTML, + title: document.title, + url: document.URL, + }); + }, + finalize: function () {}, +}; + +var ExtensionPreprocessingJS = new SharePage(); diff --git a/client/ios/App/ShareExtension/ShareView.swift b/client/ios/App/ShareExtension/ShareView.swift new file mode 100644 index 000000000..75fabfbde --- /dev/null +++ b/client/ios/App/ShareExtension/ShareView.swift @@ -0,0 +1,427 @@ +import SwiftUI + +/// The sheet: what was shared, the trip it goes to, and one button. It rises over +/// the app that shared, in the system material, and answers the tap with a +/// checkmark before TREK comes forward. +struct ShareView: View { + @ObservedObject var model: ShareModel + @Environment(\.colorScheme) private var scheme + @State private var shown = false + + var body: some View { + ZStack(alignment: .bottom) { + Color.black.opacity(shown ? 0.28 : 0) + .ignoresSafeArea() + .onTapGesture { dismiss() } + + card + .offset(y: shown ? 0 : 520) + } + .onAppear { + withAnimation(.spring(response: 0.5, dampingFraction: 0.86)) { shown = true } + } + } + + private func dismiss() { + withAnimation(.easeIn(duration: 0.2)) { shown = false } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { model.onCancel() } + } + + // MARK: - Card + + private var card: some View { + VStack(spacing: 0) { + Capsule() + .fill(ShareStyle.faint(scheme).opacity(0.5)) + .frame(width: 38, height: 5) + .padding(.top, 8) + + header + .padding(.horizontal, 20) + .padding(.top, 14) + + Group { + switch model.phase { + case .failed(let message): + failure(message) + default: + content + } + } + .padding(.top, 16) + } + .padding(.bottom, 12) + .frame(maxWidth: 560) + .background( + RoundedRectangle(cornerRadius: 32, style: .continuous) + .fill(.regularMaterial) + .overlay( + RoundedRectangle(cornerRadius: 32, style: .continuous) + .strokeBorder(ShareStyle.edge(scheme), lineWidth: 1) + ) + .shadow(color: .black.opacity(0.25), radius: 30, y: 10) + .ignoresSafeArea(edges: .bottom) + ) + .padding(.horizontal, 6) + } + + private var header: some View { + HStack(spacing: 12) { + ZStack { + RoundedRectangle(cornerRadius: 11, style: .continuous) + .fill(ShareStyle.ink(scheme)) + Text("T") + .font(.system(size: 20, weight: .heavy, design: .rounded)) + .foregroundColor(ShareStyle.paper(scheme)) + } + .frame(width: 38, height: 38) + + VStack(alignment: .leading, spacing: 1) { + Text("TREK") + .font(.system(size: 12, weight: .bold)) + .tracking(2.4) + .foregroundColor(ShareStyle.muted(scheme)) + Text(L("share.title")) + .font(.system(size: 19, weight: .bold)) + .foregroundColor(ShareStyle.ink(scheme)) + } + Spacer() + Button(action: dismiss) { + Image(systemName: "xmark") + .font(.system(size: 13, weight: .bold)) + .foregroundColor(ShareStyle.muted(scheme)) + .frame(width: 32, height: 32) + .background(Circle().fill(ShareStyle.track(scheme))) + } + .accessibilityLabel(L("share.cancel")) + } + } + + @ViewBuilder + private var content: some View { + VStack(alignment: .leading, spacing: 18) { + itemCard + .padding(.horizontal, 20) + + VStack(alignment: .leading, spacing: 10) { + Text(L("share.trip").uppercased()) + .font(.system(size: 12, weight: .bold)) + .tracking(1.2) + .foregroundColor(ShareStyle.faint(scheme)) + .padding(.horizontal, 22) + if model.trips.isEmpty { + Text(L("share.noTrips")) + .font(.system(size: 14)) + .foregroundColor(ShareStyle.muted(scheme)) + .padding(.horizontal, 22) + } else { + tripRail + } + } + + Text(L("share.hint")) + .font(.system(size: 13)) + .foregroundColor(ShareStyle.faint(scheme)) + .frame(maxWidth: .infinity) + .multilineTextAlignment(.center) + .padding(.horizontal, 24) + + addButton + .padding(.horizontal, 20) + } + } + + // MARK: - The shared item + + private var itemCard: some View { + HStack(spacing: 14) { + ZStack { + RoundedRectangle(cornerRadius: 14, style: .continuous) + .fill(LinearGradient(colors: ShareStyle.kindColors(model.item?.kind ?? .file), + startPoint: .topLeading, endPoint: .bottomTrailing)) + Image(systemName: (model.item?.kind ?? .file).symbol) + .font(.system(size: 22, weight: .semibold)) + .foregroundColor(.white) + } + .frame(width: 54, height: 54) + + VStack(alignment: .leading, spacing: 6) { + Text(model.item?.title ?? L("share.reading")) + .font(.system(size: 16, weight: .semibold)) + .foregroundColor(ShareStyle.ink(scheme)) + .lineLimit(2) + HStack(spacing: 6) { + if let item = model.item { + badge(L("share.kind.\(item.kind.rawValue)")) + if let source = item.source { badge(source) } + badge(item.sizeLabel) + } else { + ProgressView().scaleEffect(0.8) + } + } + } + Spacer(minLength: 0) + } + .padding(14) + .background( + RoundedRectangle(cornerRadius: 20, style: .continuous) + .fill(ShareStyle.raised(scheme)) + ) + .redacted(reason: model.item == nil ? .placeholder : []) + } + + private func badge(_ text: String) -> some View { + Text(text) + .font(.system(size: 11, weight: .semibold)) + .foregroundColor(ShareStyle.muted(scheme)) + .lineLimit(1) + .padding(.horizontal, 8) + .padding(.vertical, 3) + .background(Capsule().fill(ShareStyle.track(scheme))) + } + + // MARK: - Trips + + private var tripRail: some View { + ScrollViewReader { proxy in + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 10) { + ForEach(model.trips) { trip in + tripCard(trip) + .id(trip.id) + } + laterCard + } + .padding(.horizontal, 20) + .padding(.vertical, 4) + } + .onAppear { + if let id = model.selectedTrip { proxy.scrollTo(id, anchor: .leading) } + } + } + } + + private func tripCard(_ trip: ShareHandoff.Trip) -> some View { + let selected = model.selectedTrip == trip.id + return Button { + UISelectionFeedbackGenerator().selectionChanged() + withAnimation(.spring(response: 0.32, dampingFraction: 0.7)) { model.selectedTrip = trip.id } + } label: { + ZStack(alignment: .bottomLeading) { + cover(trip) + LinearGradient(colors: [.clear, .black.opacity(0.7)], startPoint: .center, endPoint: .bottom) + VStack(alignment: .leading, spacing: 2) { + Text(trip.title) + .font(.system(size: 15, weight: .bold)) + .foregroundColor(.white) + .lineLimit(2) + .multilineTextAlignment(.leading) + if let place = trip.place { + Text(place) + .font(.system(size: 11, weight: .medium)) + .foregroundColor(.white.opacity(0.8)) + .lineLimit(1) + } + } + .padding(11) + } + .frame(width: 156, height: 118) + .overlay(alignment: .topLeading) { + if let when = relative(trip) { + Text(when) + .font(.system(size: 10, weight: .bold)) + .foregroundColor(.white) + .padding(.horizontal, 7) + .padding(.vertical, 3) + .background(Capsule().fill(.black.opacity(0.35))) + .padding(9) + } + } + .overlay(alignment: .topTrailing) { + if selected { + Image(systemName: "checkmark.circle.fill") + .font(.system(size: 22, weight: .semibold)) + .symbolRenderingMode(.palette) + .foregroundStyle(ShareStyle.ink(.light), .white) + .padding(7) + .transition(.scale.combined(with: .opacity)) + } + } + .clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous)) + .overlay( + RoundedRectangle(cornerRadius: 18, style: .continuous) + .strokeBorder(selected ? ShareStyle.ink(scheme) : .clear, lineWidth: 2.5) + ) + .scaleEffect(selected ? 1 : 0.96) + .opacity(selected || model.selectedTrip == nil ? 1 : 0.78) + } + .buttonStyle(.plain) + .accessibilityAddTraits(selected ? .isSelected : []) + } + + @ViewBuilder + private func cover(_ trip: ShareHandoff.Trip) -> some View { + if let data = trip.cover, let image = UIImage(data: data) { + Image(uiImage: image).resizable().scaledToFill() + } else { + ZStack { + LinearGradient(colors: ShareStyle.tripColors(trip.id), startPoint: .topLeading, endPoint: .bottomTrailing) + Image(systemName: "airplane") + .font(.system(size: 34, weight: .semibold)) + .foregroundColor(.white.opacity(0.22)) + .rotationEffect(.degrees(-20)) + .offset(x: 40, y: -18) + } + } + } + + private var laterCard: some View { + let selected = model.selectedTrip == nil + return Button { + UISelectionFeedbackGenerator().selectionChanged() + withAnimation(.spring(response: 0.32, dampingFraction: 0.7)) { model.selectedTrip = nil } + } label: { + VStack(spacing: 8) { + Image(systemName: "rectangle.stack.badge.plus") + .font(.system(size: 22, weight: .semibold)) + Text(L("share.later")) + .font(.system(size: 13, weight: .semibold)) + .multilineTextAlignment(.center) + } + .foregroundColor(ShareStyle.muted(scheme)) + .frame(width: 118, height: 118) + .background( + RoundedRectangle(cornerRadius: 18, style: .continuous) + .strokeBorder(selected ? ShareStyle.ink(scheme) : ShareStyle.faint(scheme).opacity(0.5), + style: StrokeStyle(lineWidth: selected ? 2.5 : 1.5, dash: selected ? [] : [5, 4])) + ) + .scaleEffect(selected ? 1 : 0.96) + } + .buttonStyle(.plain) + } + + private func relative(_ trip: ShareHandoff.Trip) -> String? { + let calendar = Calendar.current + let today = calendar.startOfDay(for: Date()) + guard let start = trip.start else { return nil } + let end = trip.end ?? start + if today >= calendar.startOfDay(for: start), today <= calendar.startOfDay(for: end) { return L("share.now") } + if today > calendar.startOfDay(for: end) { return L("share.past") } + let days = calendar.dateComponents([.day], from: today, to: calendar.startOfDay(for: start)).day ?? 0 + if days == 1 { return L("share.tomorrow") } + return String(format: L("share.inDays"), days) + } + + // MARK: - Button and failure + + private var addButton: some View { + Button(action: model.add) { + ZStack { + switch model.phase { + case .saving: + ProgressView().tint(ShareStyle.paper(scheme)) + case .done: + Label(L("share.added"), systemImage: "checkmark") + .font(.system(size: 17, weight: .bold)) + .foregroundColor(.white) + .transition(.scale(scale: 0.6).combined(with: .opacity)) + default: + Text(buttonTitle) + .font(.system(size: 17, weight: .bold)) + .foregroundColor(ShareStyle.paper(scheme)) + .lineLimit(1) + .minimumScaleFactor(0.8) + .padding(.horizontal, 12) + } + } + .frame(maxWidth: .infinity) + .frame(height: 54) + .background( + RoundedRectangle(cornerRadius: 17, style: .continuous) + .fill(model.phase == .done ? ShareStyle.success : ShareStyle.ink(scheme)) + ) + .scaleEffect(model.phase == .done ? 1.02 : 1) + } + .buttonStyle(PressStyle()) + .disabled(model.phase != .ready) + .opacity(model.phase == .loading ? 0.5 : 1) + } + + private var buttonTitle: String { + if let title = model.selectedTitle { return String(format: L("share.addTo"), title) } + return L("share.add") + } + + private func failure(_ message: String) -> some View { + VStack(spacing: 14) { + Image(systemName: "exclamationmark.triangle.fill") + .font(.system(size: 30, weight: .semibold)) + .foregroundColor(ShareStyle.warning) + Text(message) + .font(.system(size: 15, weight: .medium)) + .foregroundColor(ShareStyle.ink(scheme)) + .multilineTextAlignment(.center) + .padding(.horizontal, 28) + Button(action: dismiss) { + Text(L("share.close")) + .font(.system(size: 16, weight: .semibold)) + .foregroundColor(ShareStyle.ink(scheme)) + .frame(maxWidth: .infinity) + .frame(height: 50) + .background(RoundedRectangle(cornerRadius: 16, style: .continuous).fill(ShareStyle.track(scheme))) + } + .buttonStyle(PressStyle()) + .padding(.horizontal, 20) + } + .padding(.vertical, 10) + } +} + +/// A press that gives a little under the thumb. +private struct PressStyle: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.label + .scaleEffect(configuration.isPressed ? 0.97 : 1) + .animation(.spring(response: 0.25, dampingFraction: 0.7), value: configuration.isPressed) + } +} + +/// The app's neutral palette (the same values as TREKWidgets/TrekStyle.swift). +enum ShareStyle { + static func ink(_ scheme: ColorScheme) -> Color { scheme == .dark ? Color(hex: 0xF5F5F7) : Color(hex: 0x101013) } + static func paper(_ scheme: ColorScheme) -> Color { scheme == .dark ? Color(hex: 0x101013) : Color(hex: 0xFFFFFF) } + static func muted(_ scheme: ColorScheme) -> Color { scheme == .dark ? Color(hex: 0x9C9CA3) : Color(hex: 0x68686F) } + static func faint(_ scheme: ColorScheme) -> Color { scheme == .dark ? Color(hex: 0x6E6E76) : Color(hex: 0x9A9AA1) } + static func track(_ scheme: ColorScheme) -> Color { scheme == .dark ? Color.white.opacity(0.1) : Color(hex: 0x101013).opacity(0.06) } + static func raised(_ scheme: ColorScheme) -> Color { scheme == .dark ? Color.white.opacity(0.06) : Color.white.opacity(0.7) } + static func edge(_ scheme: ColorScheme) -> Color { scheme == .dark ? Color.white.opacity(0.08) : Color.black.opacity(0.05) } + static let success = Color(hex: 0x2FA37A) + static let warning = Color(hex: 0xE8A13A) + + static func kindColors(_ kind: ShareItem.Kind) -> [Color] { + switch kind { + case .pass: return [Color(hex: 0x1F1F24), Color(hex: 0x3A3A44)] + case .pdf: return [Color(hex: 0xE5484D), Color(hex: 0xB4232A)] + case .mail: return [Color(hex: 0x4A9DFF), Color(hex: 0x1F6FEB)] + case .page: return [Color(hex: 0x38B2F5), Color(hex: 0x1A7FD6)] + case .text: return [Color(hex: 0x8E8E96), Color(hex: 0x5D5D66)] + case .image: return [Color(hex: 0xF2A93B), Color(hex: 0xE0662E)] + case .file: return [Color(hex: 0x7C7C86), Color(hex: 0x4D4D57)] + } + } + + /// A trip without a cover gets a gradient of its own, steady across opens. + static func tripColors(_ id: Int) -> [Color] { + let base = Double((id &* 47) % 360) / 360 + return [ + Color(hue: base, saturation: 0.6, brightness: 0.78), + Color(hue: (base + 0.11).truncatingRemainder(dividingBy: 1), saturation: 0.7, brightness: 0.42), + ] + } +} + +private extension Color { + init(hex: UInt32) { + self.init(red: Double((hex >> 16) & 0xFF) / 255, green: Double((hex >> 8) & 0xFF) / 255, blue: Double(hex & 0xFF) / 255) + } +} diff --git a/client/ios/App/ShareExtension/ShareViewController.swift b/client/ios/App/ShareExtension/ShareViewController.swift new file mode 100644 index 000000000..1f2ce2baf --- /dev/null +++ b/client/ios/App/ShareExtension/ShareViewController.swift @@ -0,0 +1,110 @@ +import SwiftUI +import UIKit + +/// The share sheet's entry point ("TREK" in the share sheet of Mail, Safari, +/// Files, Wallet and any app that shares a document or a link). It reads what +/// was shared, lets the user pick the trip it belongs to, leaves both for the +/// app (ShareHandoff) and opens TREK, where the booking import takes over. +@objc(ShareViewController) +final class ShareViewController: UIViewController { + private let model = ShareModel() + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .clear + + model.onCancel = { [weak self] in + self?.extensionContext?.cancelRequest(withError: NSError(domain: "com.liketrek.trek.share", code: 0)) + } + model.onDone = { [weak self] in + self?.openApp() + self?.extensionContext?.completeRequest(returningItems: nil) + } + + let host = UIHostingController(rootView: ShareView(model: model)) + host.view.backgroundColor = .clear + addChild(host) + host.view.frame = view.bounds + host.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] + view.addSubview(host.view) + host.didMove(toParent: self) + + let items = extensionContext?.inputItems.compactMap { $0 as? NSExtensionItem } ?? [] + Task { await model.load(items) } + } + + /// Extensions cannot reach UIApplication.shared; the application is still + /// up the responder chain, and opening our own scheme brings TREK forward. + /// Should that ever fail, the app picks the item up the next time it opens. + private func openApp() { + guard let url = URL(string: "com.liketrek.trek://share") else { return } + var responder: UIResponder? = self + while let current = responder { + if let application = current as? UIApplication { + application.open(url, options: [:], completionHandler: nil) + return + } + responder = current.next + } + } +} + +@MainActor +final class ShareModel: ObservableObject { + enum Phase: Equatable { + case loading + case ready + case saving + case done + case failed(String) + } + + @Published var phase: Phase = .loading + @Published var item: ShareItem? + @Published var trips: [ShareHandoff.Trip] = [] + /// nil: the app asks which trip once it is open. + @Published var selectedTrip: Int? + + var onCancel: () -> Void = {} + var onDone: () -> Void = {} + + func load(_ items: [NSExtensionItem]) async { + trips = ShareHandoff.trips() + selectedTrip = trips.first?.id + do { + item = try await ShareLoader.load(items) + phase = .ready + } catch ShareLoadError.tooLarge { + phase = .failed(L("share.tooLarge")) + } catch { + phase = .failed(L("share.unsupported")) + } + } + + func add() { + guard let item, phase == .ready else { return } + phase = .saving + let haptics = UINotificationFeedbackGenerator() + haptics.prepare() + Task { + guard ShareHandoff.store(item, tripId: selectedTrip) else { + haptics.notificationOccurred(.error) + phase = .failed(L("share.failed")) + return + } + haptics.notificationOccurred(.success) + withAnimation(.spring(response: 0.42, dampingFraction: 0.72)) { phase = .done } + try? await Task.sleep(nanoseconds: 900_000_000) + onDone() + } + } + + var selectedTitle: String? { + trips.first { $0.id == selectedTrip }?.title + } +} + +/// The extension's own strings (ShareExtension/.lproj/Localizable.strings). +func L(_ key: String) -> String { + NSLocalizedString(key, comment: "") +} diff --git a/client/ios/App/ShareExtension/ar.lproj/Localizable.strings b/client/ios/App/ShareExtension/ar.lproj/Localizable.strings new file mode 100644 index 000000000..eba3fd887 --- /dev/null +++ b/client/ios/App/ShareExtension/ar.lproj/Localizable.strings @@ -0,0 +1,26 @@ +/* The share sheet ("TREK" in the share menu): ShareExtension/ShareView.swift. */ +"share.title" = "إضافة إلى TREK"; +"share.cancel" = "إلغاء"; +"share.close" = "إغلاق"; +"share.trip" = "الرحلة"; +"share.noTrips" = "افتح TREK مرة واحدة وستظهر رحلاتك هنا."; +"share.hint" = "يقرأ TREK الحجز ويضيفه إلى الرحلة."; +"share.later" = "الاختيار لاحقًا"; +"share.add" = "إضافة"; +"share.addTo" = "إضافة إلى %@"; +"share.added" = "تمت الإضافة"; +"share.reading" = "جارٍ القراءة…"; +"share.tooLarge" = "هذا الملف أكبر من أن يُستورد."; +"share.unsupported" = "لا يستطيع TREK استيراد هذا."; +"share.failed" = "تعذّر تمرير هذا إلى TREK. حاول مرة أخرى."; +"share.now" = "الآن"; +"share.past" = "انتهت"; +"share.tomorrow" = "غدًا"; +"share.inDays" = "بعد %d يومًا"; +"share.kind.pass" = "بطاقة Wallet"; +"share.kind.pdf" = "PDF"; +"share.kind.mail" = "بريد"; +"share.kind.page" = "صفحة ويب"; +"share.kind.text" = "نص"; +"share.kind.image" = "صورة"; +"share.kind.file" = "ملف"; diff --git a/client/ios/App/ShareExtension/az.lproj/Localizable.strings b/client/ios/App/ShareExtension/az.lproj/Localizable.strings new file mode 100644 index 000000000..4ea949a85 --- /dev/null +++ b/client/ios/App/ShareExtension/az.lproj/Localizable.strings @@ -0,0 +1,26 @@ +/* The share sheet ("TREK" in the share menu): ShareExtension/ShareView.swift. */ +"share.title" = "TREK-ə əlavə et"; +"share.cancel" = "Ləğv et"; +"share.close" = "Bağla"; +"share.trip" = "Səyahət"; +"share.noTrips" = "TREK-i bir dəfə açın, səyahətləriniz burada görünəcək."; +"share.hint" = "TREK rezervasiyanı oxuyur və səyahətə əlavə edir."; +"share.later" = "Sonra seç"; +"share.add" = "Əlavə et"; +"share.addTo" = "%@ səyahətinə əlavə et"; +"share.added" = "Əlavə edildi"; +"share.reading" = "Oxunur…"; +"share.tooLarge" = "Bu fayl idxal üçün çox böyükdür."; +"share.unsupported" = "TREK bunu idxal edə bilmir."; +"share.failed" = "Bunu TREK-ə ötürmək alınmadı. Yenidən cəhd edin."; +"share.now" = "İndi"; +"share.past" = "Keçib"; +"share.tomorrow" = "Sabah"; +"share.inDays" = "%d gün sonra"; +"share.kind.pass" = "Wallet kartı"; +"share.kind.pdf" = "PDF"; +"share.kind.mail" = "E-poçt"; +"share.kind.page" = "Veb səhifə"; +"share.kind.text" = "Mətn"; +"share.kind.image" = "Foto"; +"share.kind.file" = "Fayl"; diff --git a/client/ios/App/ShareExtension/ca.lproj/Localizable.strings b/client/ios/App/ShareExtension/ca.lproj/Localizable.strings new file mode 100644 index 000000000..e87806673 --- /dev/null +++ b/client/ios/App/ShareExtension/ca.lproj/Localizable.strings @@ -0,0 +1,26 @@ +/* The share sheet ("TREK" in the share menu): ShareExtension/ShareView.swift. */ +"share.title" = "Afegeix a TREK"; +"share.cancel" = "Cancel·la"; +"share.close" = "Tanca"; +"share.trip" = "Viatge"; +"share.noTrips" = "Obre TREK una vegada i aquí apareixeran els teus viatges."; +"share.hint" = "TREK llegeix la reserva i l’afegeix al viatge."; +"share.later" = "Tria més tard"; +"share.add" = "Afegeix"; +"share.addTo" = "Afegeix a %@"; +"share.added" = "Afegit"; +"share.reading" = "Llegint…"; +"share.tooLarge" = "Aquest fitxer és massa gran per importar-lo."; +"share.unsupported" = "TREK no pot importar això."; +"share.failed" = "No s’ha pogut passar a TREK. Torna-ho a provar."; +"share.now" = "Ara"; +"share.past" = "Passat"; +"share.tomorrow" = "Demà"; +"share.inDays" = "D’aquí a %d dies"; +"share.kind.pass" = "Passi de Wallet"; +"share.kind.pdf" = "PDF"; +"share.kind.mail" = "Correu"; +"share.kind.page" = "Pàgina web"; +"share.kind.text" = "Text"; +"share.kind.image" = "Foto"; +"share.kind.file" = "Fitxer"; diff --git a/client/ios/App/ShareExtension/cs.lproj/Localizable.strings b/client/ios/App/ShareExtension/cs.lproj/Localizable.strings new file mode 100644 index 000000000..d7cc3658c --- /dev/null +++ b/client/ios/App/ShareExtension/cs.lproj/Localizable.strings @@ -0,0 +1,26 @@ +/* The share sheet ("TREK" in the share menu): ShareExtension/ShareView.swift. */ +"share.title" = "Přidat do TREK"; +"share.cancel" = "Zrušit"; +"share.close" = "Zavřít"; +"share.trip" = "Cesta"; +"share.noTrips" = "Jednou otevřete TREK a vaše cesty se zobrazí zde."; +"share.hint" = "TREK přečte rezervaci a přidá ji k cestě."; +"share.later" = "Vybrat později"; +"share.add" = "Přidat"; +"share.addTo" = "Přidat do %@"; +"share.added" = "Přidáno"; +"share.reading" = "Načítání…"; +"share.tooLarge" = "Tento soubor je pro import příliš velký."; +"share.unsupported" = "Tohle TREK neumí importovat."; +"share.failed" = "Nepodařilo se to předat do TREK. Zkuste to znovu."; +"share.now" = "Teď"; +"share.past" = "Proběhla"; +"share.tomorrow" = "Zítra"; +"share.inDays" = "Za %d dní"; +"share.kind.pass" = "Karta Wallet"; +"share.kind.pdf" = "PDF"; +"share.kind.mail" = "E-mail"; +"share.kind.page" = "Webová stránka"; +"share.kind.text" = "Text"; +"share.kind.image" = "Fotka"; +"share.kind.file" = "Soubor"; diff --git a/client/ios/App/ShareExtension/de.lproj/Localizable.strings b/client/ios/App/ShareExtension/de.lproj/Localizable.strings new file mode 100644 index 000000000..cf044809b --- /dev/null +++ b/client/ios/App/ShareExtension/de.lproj/Localizable.strings @@ -0,0 +1,26 @@ +/* The share sheet ("TREK" in the share menu): ShareExtension/ShareView.swift. */ +"share.title" = "Zu TREK hinzufügen"; +"share.cancel" = "Abbrechen"; +"share.close" = "Schließen"; +"share.trip" = "Reise"; +"share.noTrips" = "Öffne TREK einmal, dann erscheinen hier deine Reisen."; +"share.hint" = "TREK liest die Buchung und trägt sie in die Reise ein."; +"share.later" = "Später wählen"; +"share.add" = "Hinzufügen"; +"share.addTo" = "Zu %@ hinzufügen"; +"share.added" = "Hinzugefügt"; +"share.reading" = "Wird gelesen…"; +"share.tooLarge" = "Diese Datei ist zu groß für den Import."; +"share.unsupported" = "Das kann TREK nicht importieren."; +"share.failed" = "Das konnte nicht an TREK übergeben werden. Bitte versuch es erneut."; +"share.now" = "Läuft"; +"share.past" = "Vorbei"; +"share.tomorrow" = "Morgen"; +"share.inDays" = "In %d Tagen"; +"share.kind.pass" = "Wallet-Pass"; +"share.kind.pdf" = "PDF"; +"share.kind.mail" = "E-Mail"; +"share.kind.page" = "Webseite"; +"share.kind.text" = "Text"; +"share.kind.image" = "Foto"; +"share.kind.file" = "Datei"; diff --git a/client/ios/App/ShareExtension/el.lproj/Localizable.strings b/client/ios/App/ShareExtension/el.lproj/Localizable.strings new file mode 100644 index 000000000..ae264a2fa --- /dev/null +++ b/client/ios/App/ShareExtension/el.lproj/Localizable.strings @@ -0,0 +1,26 @@ +/* The share sheet ("TREK" in the share menu): ShareExtension/ShareView.swift. */ +"share.title" = "Προσθήκη στο TREK"; +"share.cancel" = "Ακύρωση"; +"share.close" = "Κλείσιμο"; +"share.trip" = "Ταξίδι"; +"share.noTrips" = "Ανοίξτε το TREK μία φορά και τα ταξίδια σας θα εμφανιστούν εδώ."; +"share.hint" = "Το TREK διαβάζει την κράτηση και την προσθέτει στο ταξίδι."; +"share.later" = "Επιλογή αργότερα"; +"share.add" = "Προσθήκη"; +"share.addTo" = "Προσθήκη στο %@"; +"share.added" = "Προστέθηκε"; +"share.reading" = "Ανάγνωση…"; +"share.tooLarge" = "Το αρχείο είναι πολύ μεγάλο για εισαγωγή."; +"share.unsupported" = "Το TREK δεν μπορεί να το εισαγάγει."; +"share.failed" = "Δεν ήταν δυνατή η μεταφορά στο TREK. Δοκιμάστε ξανά."; +"share.now" = "Τώρα"; +"share.past" = "Παρελθόν"; +"share.tomorrow" = "Αύριο"; +"share.inDays" = "Σε %d ημέρες"; +"share.kind.pass" = "Κάρτα Wallet"; +"share.kind.pdf" = "PDF"; +"share.kind.mail" = "Email"; +"share.kind.page" = "Ιστοσελίδα"; +"share.kind.text" = "Κείμενο"; +"share.kind.image" = "Φωτογραφία"; +"share.kind.file" = "Αρχείο"; diff --git a/client/ios/App/ShareExtension/en.lproj/Localizable.strings b/client/ios/App/ShareExtension/en.lproj/Localizable.strings new file mode 100644 index 000000000..5b2588531 --- /dev/null +++ b/client/ios/App/ShareExtension/en.lproj/Localizable.strings @@ -0,0 +1,26 @@ +/* The share sheet ("TREK" in the share menu): ShareExtension/ShareView.swift. */ +"share.title" = "Add to TREK"; +"share.cancel" = "Cancel"; +"share.close" = "Close"; +"share.trip" = "Trip"; +"share.noTrips" = "Open TREK once and your trips show up here."; +"share.hint" = "TREK reads the booking and adds it to the trip."; +"share.later" = "Choose later"; +"share.add" = "Add"; +"share.addTo" = "Add to %@"; +"share.added" = "Added"; +"share.reading" = "Reading…"; +"share.tooLarge" = "This file is too large to import."; +"share.unsupported" = "TREK cannot import this."; +"share.failed" = "This could not be handed to TREK. Please try again."; +"share.now" = "Now"; +"share.past" = "Past"; +"share.tomorrow" = "Tomorrow"; +"share.inDays" = "In %d days"; +"share.kind.pass" = "Wallet pass"; +"share.kind.pdf" = "PDF"; +"share.kind.mail" = "Email"; +"share.kind.page" = "Web page"; +"share.kind.text" = "Text"; +"share.kind.image" = "Photo"; +"share.kind.file" = "File"; diff --git a/client/ios/App/ShareExtension/es.lproj/Localizable.strings b/client/ios/App/ShareExtension/es.lproj/Localizable.strings new file mode 100644 index 000000000..20d326af4 --- /dev/null +++ b/client/ios/App/ShareExtension/es.lproj/Localizable.strings @@ -0,0 +1,26 @@ +/* The share sheet ("TREK" in the share menu): ShareExtension/ShareView.swift. */ +"share.title" = "Añadir a TREK"; +"share.cancel" = "Cancelar"; +"share.close" = "Cerrar"; +"share.trip" = "Viaje"; +"share.noTrips" = "Abre TREK una vez y tus viajes aparecerán aquí."; +"share.hint" = "TREK lee la reserva y la añade al viaje."; +"share.later" = "Elegir después"; +"share.add" = "Añadir"; +"share.addTo" = "Añadir a %@"; +"share.added" = "Añadido"; +"share.reading" = "Leyendo…"; +"share.tooLarge" = "Este archivo es demasiado grande para importarlo."; +"share.unsupported" = "TREK no puede importar esto."; +"share.failed" = "No se pudo pasar a TREK. Inténtalo de nuevo."; +"share.now" = "En curso"; +"share.past" = "Pasado"; +"share.tomorrow" = "Mañana"; +"share.inDays" = "En %d días"; +"share.kind.pass" = "Pase de Wallet"; +"share.kind.pdf" = "PDF"; +"share.kind.mail" = "Correo"; +"share.kind.page" = "Página web"; +"share.kind.text" = "Texto"; +"share.kind.image" = "Foto"; +"share.kind.file" = "Archivo"; diff --git a/client/ios/App/ShareExtension/et.lproj/Localizable.strings b/client/ios/App/ShareExtension/et.lproj/Localizable.strings new file mode 100644 index 000000000..daabacc86 --- /dev/null +++ b/client/ios/App/ShareExtension/et.lproj/Localizable.strings @@ -0,0 +1,26 @@ +/* The share sheet ("TREK" in the share menu): ShareExtension/ShareView.swift. */ +"share.title" = "Lisa TREK-i"; +"share.cancel" = "Tühista"; +"share.close" = "Sulge"; +"share.trip" = "Reis"; +"share.noTrips" = "Ava TREK korra ja sinu reisid ilmuvad siia."; +"share.hint" = "TREK loeb broneeringu ja lisab selle reisile."; +"share.later" = "Vali hiljem"; +"share.add" = "Lisa"; +"share.addTo" = "Lisa reisile %@"; +"share.added" = "Lisatud"; +"share.reading" = "Loen…"; +"share.tooLarge" = "See fail on importimiseks liiga suur."; +"share.unsupported" = "TREK ei saa seda importida."; +"share.failed" = "Seda ei õnnestunud TREK-ile üle anda. Proovi uuesti."; +"share.now" = "Praegu"; +"share.past" = "Möödas"; +"share.tomorrow" = "Homme"; +"share.inDays" = "%d päeva pärast"; +"share.kind.pass" = "Wallet’i pass"; +"share.kind.pdf" = "PDF"; +"share.kind.mail" = "E-kiri"; +"share.kind.page" = "Veebileht"; +"share.kind.text" = "Tekst"; +"share.kind.image" = "Foto"; +"share.kind.file" = "Fail"; diff --git a/client/ios/App/ShareExtension/fr.lproj/Localizable.strings b/client/ios/App/ShareExtension/fr.lproj/Localizable.strings new file mode 100644 index 000000000..121e21910 --- /dev/null +++ b/client/ios/App/ShareExtension/fr.lproj/Localizable.strings @@ -0,0 +1,26 @@ +/* The share sheet ("TREK" in the share menu): ShareExtension/ShareView.swift. */ +"share.title" = "Ajouter à TREK"; +"share.cancel" = "Annuler"; +"share.close" = "Fermer"; +"share.trip" = "Voyage"; +"share.noTrips" = "Ouvrez TREK une fois et vos voyages apparaîtront ici."; +"share.hint" = "TREK lit la réservation et l’ajoute au voyage."; +"share.later" = "Choisir plus tard"; +"share.add" = "Ajouter"; +"share.addTo" = "Ajouter à %@"; +"share.added" = "Ajouté"; +"share.reading" = "Lecture…"; +"share.tooLarge" = "Ce fichier est trop volumineux pour être importé."; +"share.unsupported" = "TREK ne peut pas importer ceci."; +"share.failed" = "Impossible de transmettre ceci à TREK. Réessayez."; +"share.now" = "En cours"; +"share.past" = "Passé"; +"share.tomorrow" = "Demain"; +"share.inDays" = "Dans %d jours"; +"share.kind.pass" = "Carte Wallet"; +"share.kind.pdf" = "PDF"; +"share.kind.mail" = "E-mail"; +"share.kind.page" = "Page web"; +"share.kind.text" = "Texte"; +"share.kind.image" = "Photo"; +"share.kind.file" = "Fichier"; diff --git a/client/ios/App/ShareExtension/he.lproj/Localizable.strings b/client/ios/App/ShareExtension/he.lproj/Localizable.strings new file mode 100644 index 000000000..fb6649cf6 --- /dev/null +++ b/client/ios/App/ShareExtension/he.lproj/Localizable.strings @@ -0,0 +1,26 @@ +/* The share sheet ("TREK" in the share menu): ShareExtension/ShareView.swift. */ +"share.title" = "הוספה ל-TREK"; +"share.cancel" = "ביטול"; +"share.close" = "סגירה"; +"share.trip" = "טיול"; +"share.noTrips" = "פתחו את TREK פעם אחת והטיולים שלכם יופיעו כאן."; +"share.hint" = "TREK קורא את ההזמנה ומוסיף אותה לטיול."; +"share.later" = "לבחור אחר כך"; +"share.add" = "הוספה"; +"share.addTo" = "הוספה אל %@"; +"share.added" = "נוסף"; +"share.reading" = "קורא…"; +"share.tooLarge" = "הקובץ גדול מדי לייבוא."; +"share.unsupported" = "TREK לא יכול לייבא את זה."; +"share.failed" = "לא ניתן היה להעביר את זה ל-TREK. אנא נסו שוב."; +"share.now" = "עכשיו"; +"share.past" = "עבר"; +"share.tomorrow" = "מחר"; +"share.inDays" = "בעוד %d ימים"; +"share.kind.pass" = "כרטיס Wallet"; +"share.kind.pdf" = "PDF"; +"share.kind.mail" = "דוא״ל"; +"share.kind.page" = "דף אינטרנט"; +"share.kind.text" = "טקסט"; +"share.kind.image" = "תמונה"; +"share.kind.file" = "קובץ"; diff --git a/client/ios/App/ShareExtension/hu.lproj/Localizable.strings b/client/ios/App/ShareExtension/hu.lproj/Localizable.strings new file mode 100644 index 000000000..78b5dc5fe --- /dev/null +++ b/client/ios/App/ShareExtension/hu.lproj/Localizable.strings @@ -0,0 +1,26 @@ +/* The share sheet ("TREK" in the share menu): ShareExtension/ShareView.swift. */ +"share.title" = "Hozzáadás a TREK-hez"; +"share.cancel" = "Mégse"; +"share.close" = "Bezárás"; +"share.trip" = "Utazás"; +"share.noTrips" = "Nyisd meg egyszer a TREK-et, és itt megjelennek az utazásaid."; +"share.hint" = "A TREK beolvassa a foglalást, és hozzáadja az utazáshoz."; +"share.later" = "Később választok"; +"share.add" = "Hozzáadás"; +"share.addTo" = "Hozzáadás ehhez: %@"; +"share.added" = "Hozzáadva"; +"share.reading" = "Olvasás…"; +"share.tooLarge" = "Ez a fájl túl nagy az importáláshoz."; +"share.unsupported" = "Ezt a TREK nem tudja importálni."; +"share.failed" = "Nem sikerült átadni a TREK-nek. Próbáld újra."; +"share.now" = "Most"; +"share.past" = "Elmúlt"; +"share.tomorrow" = "Holnap"; +"share.inDays" = "%d nap múlva"; +"share.kind.pass" = "Wallet-kártya"; +"share.kind.pdf" = "PDF"; +"share.kind.mail" = "E-mail"; +"share.kind.page" = "Weboldal"; +"share.kind.text" = "Szöveg"; +"share.kind.image" = "Fotó"; +"share.kind.file" = "Fájl"; diff --git a/client/ios/App/ShareExtension/id.lproj/Localizable.strings b/client/ios/App/ShareExtension/id.lproj/Localizable.strings new file mode 100644 index 000000000..7d4fc9790 --- /dev/null +++ b/client/ios/App/ShareExtension/id.lproj/Localizable.strings @@ -0,0 +1,26 @@ +/* The share sheet ("TREK" in the share menu): ShareExtension/ShareView.swift. */ +"share.title" = "Tambahkan ke TREK"; +"share.cancel" = "Batal"; +"share.close" = "Tutup"; +"share.trip" = "Perjalanan"; +"share.noTrips" = "Buka TREK sekali dan perjalanan Anda akan muncul di sini."; +"share.hint" = "TREK membaca pemesanan dan menambahkannya ke perjalanan."; +"share.later" = "Pilih nanti"; +"share.add" = "Tambahkan"; +"share.addTo" = "Tambahkan ke %@"; +"share.added" = "Ditambahkan"; +"share.reading" = "Membaca…"; +"share.tooLarge" = "File ini terlalu besar untuk diimpor."; +"share.unsupported" = "TREK tidak dapat mengimpor ini."; +"share.failed" = "Tidak dapat diteruskan ke TREK. Silakan coba lagi."; +"share.now" = "Sekarang"; +"share.past" = "Lewat"; +"share.tomorrow" = "Besok"; +"share.inDays" = "Dalam %d hari"; +"share.kind.pass" = "Pass Wallet"; +"share.kind.pdf" = "PDF"; +"share.kind.mail" = "Email"; +"share.kind.page" = "Halaman web"; +"share.kind.text" = "Teks"; +"share.kind.image" = "Foto"; +"share.kind.file" = "File"; diff --git a/client/ios/App/ShareExtension/it.lproj/Localizable.strings b/client/ios/App/ShareExtension/it.lproj/Localizable.strings new file mode 100644 index 000000000..432c172da --- /dev/null +++ b/client/ios/App/ShareExtension/it.lproj/Localizable.strings @@ -0,0 +1,26 @@ +/* The share sheet ("TREK" in the share menu): ShareExtension/ShareView.swift. */ +"share.title" = "Aggiungi a TREK"; +"share.cancel" = "Annulla"; +"share.close" = "Chiudi"; +"share.trip" = "Viaggio"; +"share.noTrips" = "Apri TREK una volta e qui compariranno i tuoi viaggi."; +"share.hint" = "TREK legge la prenotazione e la aggiunge al viaggio."; +"share.later" = "Scegli dopo"; +"share.add" = "Aggiungi"; +"share.addTo" = "Aggiungi a %@"; +"share.added" = "Aggiunto"; +"share.reading" = "Lettura…"; +"share.tooLarge" = "Questo file è troppo grande da importare."; +"share.unsupported" = "TREK non può importare questo contenuto."; +"share.failed" = "Impossibile passarlo a TREK. Riprova."; +"share.now" = "In corso"; +"share.past" = "Passato"; +"share.tomorrow" = "Domani"; +"share.inDays" = "Tra %d giorni"; +"share.kind.pass" = "Pass di Wallet"; +"share.kind.pdf" = "PDF"; +"share.kind.mail" = "E-mail"; +"share.kind.page" = "Pagina web"; +"share.kind.text" = "Testo"; +"share.kind.image" = "Foto"; +"share.kind.file" = "File"; diff --git a/client/ios/App/ShareExtension/ja.lproj/Localizable.strings b/client/ios/App/ShareExtension/ja.lproj/Localizable.strings new file mode 100644 index 000000000..4c0722c88 --- /dev/null +++ b/client/ios/App/ShareExtension/ja.lproj/Localizable.strings @@ -0,0 +1,26 @@ +/* The share sheet ("TREK" in the share menu): ShareExtension/ShareView.swift. */ +"share.title" = "TREKに追加"; +"share.cancel" = "キャンセル"; +"share.close" = "閉じる"; +"share.trip" = "旅行"; +"share.noTrips" = "一度TREKを開くと、ここに旅行が表示されます。"; +"share.hint" = "TREKが予約を読み取り、旅行に追加します。"; +"share.later" = "あとで選ぶ"; +"share.add" = "追加"; +"share.addTo" = "%@に追加"; +"share.added" = "追加しました"; +"share.reading" = "読み込み中…"; +"share.tooLarge" = "このファイルは大きすぎて読み込めません。"; +"share.unsupported" = "TREKではこれを読み込めません。"; +"share.failed" = "TREKに渡せませんでした。もう一度お試しください。"; +"share.now" = "旅行中"; +"share.past" = "終了"; +"share.tomorrow" = "明日"; +"share.inDays" = "%d日後"; +"share.kind.pass" = "Walletパス"; +"share.kind.pdf" = "PDF"; +"share.kind.mail" = "メール"; +"share.kind.page" = "Webページ"; +"share.kind.text" = "テキスト"; +"share.kind.image" = "写真"; +"share.kind.file" = "ファイル"; diff --git a/client/ios/App/ShareExtension/ko.lproj/Localizable.strings b/client/ios/App/ShareExtension/ko.lproj/Localizable.strings new file mode 100644 index 000000000..7605d6545 --- /dev/null +++ b/client/ios/App/ShareExtension/ko.lproj/Localizable.strings @@ -0,0 +1,26 @@ +/* The share sheet ("TREK" in the share menu): ShareExtension/ShareView.swift. */ +"share.title" = "TREK에 추가"; +"share.cancel" = "취소"; +"share.close" = "닫기"; +"share.trip" = "여행"; +"share.noTrips" = "TREK을 한 번 열면 여기에 여행이 표시됩니다."; +"share.hint" = "TREK이 예약을 읽고 여행에 추가합니다."; +"share.later" = "나중에 선택"; +"share.add" = "추가"; +"share.addTo" = "%@에 추가"; +"share.added" = "추가됨"; +"share.reading" = "읽는 중…"; +"share.tooLarge" = "이 파일은 가져오기에는 너무 큽니다."; +"share.unsupported" = "TREK에서 가져올 수 없는 항목입니다."; +"share.failed" = "TREK에 전달하지 못했습니다. 다시 시도하세요."; +"share.now" = "진행 중"; +"share.past" = "지난 여행"; +"share.tomorrow" = "내일"; +"share.inDays" = "%d일 후"; +"share.kind.pass" = "Wallet 패스"; +"share.kind.pdf" = "PDF"; +"share.kind.mail" = "이메일"; +"share.kind.page" = "웹 페이지"; +"share.kind.text" = "텍스트"; +"share.kind.image" = "사진"; +"share.kind.file" = "파일"; diff --git a/client/ios/App/ShareExtension/nl.lproj/Localizable.strings b/client/ios/App/ShareExtension/nl.lproj/Localizable.strings new file mode 100644 index 000000000..de1d27b72 --- /dev/null +++ b/client/ios/App/ShareExtension/nl.lproj/Localizable.strings @@ -0,0 +1,26 @@ +/* The share sheet ("TREK" in the share menu): ShareExtension/ShareView.swift. */ +"share.title" = "Toevoegen aan TREK"; +"share.cancel" = "Annuleer"; +"share.close" = "Sluit"; +"share.trip" = "Reis"; +"share.noTrips" = "Open TREK één keer, dan verschijnen je reizen hier."; +"share.hint" = "TREK leest de boeking en zet hem in de reis."; +"share.later" = "Later kiezen"; +"share.add" = "Toevoegen"; +"share.addTo" = "Toevoegen aan %@"; +"share.added" = "Toegevoegd"; +"share.reading" = "Lezen…"; +"share.tooLarge" = "Dit bestand is te groot om te importeren."; +"share.unsupported" = "Dit kan TREK niet importeren."; +"share.failed" = "Dit kon niet aan TREK worden doorgegeven. Probeer het opnieuw."; +"share.now" = "Nu"; +"share.past" = "Voorbij"; +"share.tomorrow" = "Morgen"; +"share.inDays" = "Over %d dagen"; +"share.kind.pass" = "Wallet-pas"; +"share.kind.pdf" = "PDF"; +"share.kind.mail" = "E-mail"; +"share.kind.page" = "Webpagina"; +"share.kind.text" = "Tekst"; +"share.kind.image" = "Foto"; +"share.kind.file" = "Bestand"; diff --git a/client/ios/App/ShareExtension/pl.lproj/Localizable.strings b/client/ios/App/ShareExtension/pl.lproj/Localizable.strings new file mode 100644 index 000000000..91b62e3b4 --- /dev/null +++ b/client/ios/App/ShareExtension/pl.lproj/Localizable.strings @@ -0,0 +1,26 @@ +/* The share sheet ("TREK" in the share menu): ShareExtension/ShareView.swift. */ +"share.title" = "Dodaj do TREK"; +"share.cancel" = "Anuluj"; +"share.close" = "Zamknij"; +"share.trip" = "Podróż"; +"share.noTrips" = "Otwórz TREK raz, a Twoje podróże pojawią się tutaj."; +"share.hint" = "TREK odczyta rezerwację i doda ją do podróży."; +"share.later" = "Wybierz później"; +"share.add" = "Dodaj"; +"share.addTo" = "Dodaj do %@"; +"share.added" = "Dodano"; +"share.reading" = "Odczytywanie…"; +"share.tooLarge" = "Ten plik jest za duży do zaimportowania."; +"share.unsupported" = "TREK nie może tego zaimportować."; +"share.failed" = "Nie udało się przekazać tego do TREK. Spróbuj ponownie."; +"share.now" = "Teraz"; +"share.past" = "Minęła"; +"share.tomorrow" = "Jutro"; +"share.inDays" = "Za %d dni"; +"share.kind.pass" = "Karta Wallet"; +"share.kind.pdf" = "PDF"; +"share.kind.mail" = "E-mail"; +"share.kind.page" = "Strona"; +"share.kind.text" = "Tekst"; +"share.kind.image" = "Zdjęcie"; +"share.kind.file" = "Plik"; diff --git a/client/ios/App/ShareExtension/pt-BR.lproj/Localizable.strings b/client/ios/App/ShareExtension/pt-BR.lproj/Localizable.strings new file mode 100644 index 000000000..45abe88b0 --- /dev/null +++ b/client/ios/App/ShareExtension/pt-BR.lproj/Localizable.strings @@ -0,0 +1,26 @@ +/* The share sheet ("TREK" in the share menu): ShareExtension/ShareView.swift. */ +"share.title" = "Adicionar ao TREK"; +"share.cancel" = "Cancelar"; +"share.close" = "Fechar"; +"share.trip" = "Viagem"; +"share.noTrips" = "Abra o TREK uma vez e suas viagens aparecerão aqui."; +"share.hint" = "O TREK lê a reserva e a adiciona à viagem."; +"share.later" = "Escolher depois"; +"share.add" = "Adicionar"; +"share.addTo" = "Adicionar a %@"; +"share.added" = "Adicionado"; +"share.reading" = "Lendo…"; +"share.tooLarge" = "Este arquivo é grande demais para importar."; +"share.unsupported" = "O TREK não consegue importar isto."; +"share.failed" = "Não foi possível passar isto ao TREK. Tente novamente."; +"share.now" = "Agora"; +"share.past" = "Passada"; +"share.tomorrow" = "Amanhã"; +"share.inDays" = "Em %d dias"; +"share.kind.pass" = "Passe do Wallet"; +"share.kind.pdf" = "PDF"; +"share.kind.mail" = "E-mail"; +"share.kind.page" = "Página web"; +"share.kind.text" = "Texto"; +"share.kind.image" = "Foto"; +"share.kind.file" = "Arquivo"; diff --git a/client/ios/App/ShareExtension/ru.lproj/Localizable.strings b/client/ios/App/ShareExtension/ru.lproj/Localizable.strings new file mode 100644 index 000000000..8d7d0997a --- /dev/null +++ b/client/ios/App/ShareExtension/ru.lproj/Localizable.strings @@ -0,0 +1,26 @@ +/* The share sheet ("TREK" in the share menu): ShareExtension/ShareView.swift. */ +"share.title" = "Добавить в TREK"; +"share.cancel" = "Отменить"; +"share.close" = "Закрыть"; +"share.trip" = "Поездка"; +"share.noTrips" = "Откройте TREK один раз, и ваши поездки появятся здесь."; +"share.hint" = "TREK прочитает бронь и добавит её в поездку."; +"share.later" = "Выбрать позже"; +"share.add" = "Добавить"; +"share.addTo" = "Добавить в «%@»"; +"share.added" = "Добавлено"; +"share.reading" = "Чтение…"; +"share.tooLarge" = "Файл слишком большой для импорта."; +"share.unsupported" = "TREK не может это импортировать."; +"share.failed" = "Не удалось передать это в TREK. Повторите попытку."; +"share.now" = "Сейчас"; +"share.past" = "Прошла"; +"share.tomorrow" = "Завтра"; +"share.inDays" = "Через %d дн."; +"share.kind.pass" = "Карта Wallet"; +"share.kind.pdf" = "PDF"; +"share.kind.mail" = "Письмо"; +"share.kind.page" = "Веб-страница"; +"share.kind.text" = "Текст"; +"share.kind.image" = "Фото"; +"share.kind.file" = "Файл"; diff --git a/client/ios/App/ShareExtension/sk.lproj/Localizable.strings b/client/ios/App/ShareExtension/sk.lproj/Localizable.strings new file mode 100644 index 000000000..3c8aef3ab --- /dev/null +++ b/client/ios/App/ShareExtension/sk.lproj/Localizable.strings @@ -0,0 +1,26 @@ +/* The share sheet ("TREK" in the share menu): ShareExtension/ShareView.swift. */ +"share.title" = "Pridať do TREK"; +"share.cancel" = "Zrušiť"; +"share.close" = "Zavrieť"; +"share.trip" = "Cesta"; +"share.noTrips" = "Raz otvorte TREK a vaše cesty sa zobrazia tu."; +"share.hint" = "TREK prečíta rezerváciu a pridá ju k ceste."; +"share.later" = "Vybrať neskôr"; +"share.add" = "Pridať"; +"share.addTo" = "Pridať do %@"; +"share.added" = "Pridané"; +"share.reading" = "Načítava sa…"; +"share.tooLarge" = "Tento súbor je na import príliš veľký."; +"share.unsupported" = "Toto TREK nevie importovať."; +"share.failed" = "Nepodarilo sa to odovzdať do TREK. Skúste to znova."; +"share.now" = "Teraz"; +"share.past" = "Prebehla"; +"share.tomorrow" = "Zajtra"; +"share.inDays" = "O %d dní"; +"share.kind.pass" = "Karta Wallet"; +"share.kind.pdf" = "PDF"; +"share.kind.mail" = "E-mail"; +"share.kind.page" = "Webová stránka"; +"share.kind.text" = "Text"; +"share.kind.image" = "Fotka"; +"share.kind.file" = "Súbor"; diff --git a/client/ios/App/ShareExtension/sv.lproj/Localizable.strings b/client/ios/App/ShareExtension/sv.lproj/Localizable.strings new file mode 100644 index 000000000..edd814d85 --- /dev/null +++ b/client/ios/App/ShareExtension/sv.lproj/Localizable.strings @@ -0,0 +1,26 @@ +/* The share sheet ("TREK" in the share menu): ShareExtension/ShareView.swift. */ +"share.title" = "Lägg till i TREK"; +"share.cancel" = "Avbryt"; +"share.close" = "Stäng"; +"share.trip" = "Resa"; +"share.noTrips" = "Öppna TREK en gång så visas dina resor här."; +"share.hint" = "TREK läser bokningen och lägger till den i resan."; +"share.later" = "Välj senare"; +"share.add" = "Lägg till"; +"share.addTo" = "Lägg till i %@"; +"share.added" = "Tillagd"; +"share.reading" = "Läser…"; +"share.tooLarge" = "Filen är för stor för att importeras."; +"share.unsupported" = "Det här kan TREK inte importera."; +"share.failed" = "Det gick inte att lämna över till TREK. Försök igen."; +"share.now" = "Nu"; +"share.past" = "Förbi"; +"share.tomorrow" = "I morgon"; +"share.inDays" = "Om %d dagar"; +"share.kind.pass" = "Wallet-kort"; +"share.kind.pdf" = "PDF"; +"share.kind.mail" = "Mejl"; +"share.kind.page" = "Webbsida"; +"share.kind.text" = "Text"; +"share.kind.image" = "Foto"; +"share.kind.file" = "Fil"; diff --git a/client/ios/App/ShareExtension/th.lproj/Localizable.strings b/client/ios/App/ShareExtension/th.lproj/Localizable.strings new file mode 100644 index 000000000..a56a9d8a1 --- /dev/null +++ b/client/ios/App/ShareExtension/th.lproj/Localizable.strings @@ -0,0 +1,26 @@ +/* The share sheet ("TREK" in the share menu): ShareExtension/ShareView.swift. */ +"share.title" = "เพิ่มลงใน TREK"; +"share.cancel" = "ยกเลิก"; +"share.close" = "ปิด"; +"share.trip" = "ทริป"; +"share.noTrips" = "เปิด TREK หนึ่งครั้ง แล้วทริปของคุณจะแสดงที่นี่"; +"share.hint" = "TREK จะอ่านการจองและเพิ่มลงในทริป"; +"share.later" = "เลือกภายหลัง"; +"share.add" = "เพิ่ม"; +"share.addTo" = "เพิ่มลงใน %@"; +"share.added" = "เพิ่มแล้ว"; +"share.reading" = "กำลังอ่าน…"; +"share.tooLarge" = "ไฟล์นี้ใหญ่เกินไปสำหรับการนำเข้า"; +"share.unsupported" = "TREK นำเข้ารายการนี้ไม่ได้"; +"share.failed" = "ส่งต่อให้ TREK ไม่สำเร็จ โปรดลองอีกครั้ง"; +"share.now" = "ตอนนี้"; +"share.past" = "ผ่านไปแล้ว"; +"share.tomorrow" = "พรุ่งนี้"; +"share.inDays" = "อีก %d วัน"; +"share.kind.pass" = "บัตร Wallet"; +"share.kind.pdf" = "PDF"; +"share.kind.mail" = "อีเมล"; +"share.kind.page" = "หน้าเว็บ"; +"share.kind.text" = "ข้อความ"; +"share.kind.image" = "รูปภาพ"; +"share.kind.file" = "ไฟล์"; diff --git a/client/ios/App/ShareExtension/tr.lproj/Localizable.strings b/client/ios/App/ShareExtension/tr.lproj/Localizable.strings new file mode 100644 index 000000000..68664386b --- /dev/null +++ b/client/ios/App/ShareExtension/tr.lproj/Localizable.strings @@ -0,0 +1,26 @@ +/* The share sheet ("TREK" in the share menu): ShareExtension/ShareView.swift. */ +"share.title" = "TREK’e ekle"; +"share.cancel" = "Vazgeç"; +"share.close" = "Kapat"; +"share.trip" = "Seyahat"; +"share.noTrips" = "TREK’i bir kez açın, seyahatleriniz burada görünür."; +"share.hint" = "TREK rezervasyonu okur ve seyahate ekler."; +"share.later" = "Sonra seç"; +"share.add" = "Ekle"; +"share.addTo" = "%@ seyahatine ekle"; +"share.added" = "Eklendi"; +"share.reading" = "Okunuyor…"; +"share.tooLarge" = "Bu dosya içe aktarmak için çok büyük."; +"share.unsupported" = "TREK bunu içe aktaramaz."; +"share.failed" = "Bu TREK’e aktarılamadı. Lütfen tekrar deneyin."; +"share.now" = "Şimdi"; +"share.past" = "Geçti"; +"share.tomorrow" = "Yarın"; +"share.inDays" = "%d gün sonra"; +"share.kind.pass" = "Wallet kartı"; +"share.kind.pdf" = "PDF"; +"share.kind.mail" = "E-posta"; +"share.kind.page" = "Web sayfası"; +"share.kind.text" = "Metin"; +"share.kind.image" = "Fotoğraf"; +"share.kind.file" = "Dosya"; diff --git a/client/ios/App/ShareExtension/uk.lproj/Localizable.strings b/client/ios/App/ShareExtension/uk.lproj/Localizable.strings new file mode 100644 index 000000000..d40a06c36 --- /dev/null +++ b/client/ios/App/ShareExtension/uk.lproj/Localizable.strings @@ -0,0 +1,26 @@ +/* The share sheet ("TREK" in the share menu): ShareExtension/ShareView.swift. */ +"share.title" = "Додати до TREK"; +"share.cancel" = "Скасувати"; +"share.close" = "Закрити"; +"share.trip" = "Подорож"; +"share.noTrips" = "Відкрийте TREK один раз, і ваші подорожі з’являться тут."; +"share.hint" = "TREK прочитає бронювання й додасть його до подорожі."; +"share.later" = "Вибрати пізніше"; +"share.add" = "Додати"; +"share.addTo" = "Додати до «%@»"; +"share.added" = "Додано"; +"share.reading" = "Читання…"; +"share.tooLarge" = "Файл завеликий для імпорту."; +"share.unsupported" = "TREK не може це імпортувати."; +"share.failed" = "Не вдалося передати це до TREK. Повторіть спробу."; +"share.now" = "Зараз"; +"share.past" = "Минула"; +"share.tomorrow" = "Завтра"; +"share.inDays" = "Через %d дн."; +"share.kind.pass" = "Картка Wallet"; +"share.kind.pdf" = "PDF"; +"share.kind.mail" = "Лист"; +"share.kind.page" = "Вебсторінка"; +"share.kind.text" = "Текст"; +"share.kind.image" = "Фото"; +"share.kind.file" = "Файл"; diff --git a/client/ios/App/ShareExtension/vi.lproj/Localizable.strings b/client/ios/App/ShareExtension/vi.lproj/Localizable.strings new file mode 100644 index 000000000..8def7b4d2 --- /dev/null +++ b/client/ios/App/ShareExtension/vi.lproj/Localizable.strings @@ -0,0 +1,26 @@ +/* The share sheet ("TREK" in the share menu): ShareExtension/ShareView.swift. */ +"share.title" = "Thêm vào TREK"; +"share.cancel" = "Hủy"; +"share.close" = "Đóng"; +"share.trip" = "Chuyến đi"; +"share.noTrips" = "Mở TREK một lần, các chuyến đi của bạn sẽ hiện ở đây."; +"share.hint" = "TREK đọc đặt chỗ và thêm vào chuyến đi."; +"share.later" = "Chọn sau"; +"share.add" = "Thêm"; +"share.addTo" = "Thêm vào %@"; +"share.added" = "Đã thêm"; +"share.reading" = "Đang đọc…"; +"share.tooLarge" = "Tệp này quá lớn để nhập."; +"share.unsupported" = "TREK không thể nhập nội dung này."; +"share.failed" = "Không thể chuyển sang TREK. Vui lòng thử lại."; +"share.now" = "Đang diễn ra"; +"share.past" = "Đã qua"; +"share.tomorrow" = "Ngày mai"; +"share.inDays" = "Sau %d ngày"; +"share.kind.pass" = "Thẻ Wallet"; +"share.kind.pdf" = "PDF"; +"share.kind.mail" = "Email"; +"share.kind.page" = "Trang web"; +"share.kind.text" = "Văn bản"; +"share.kind.image" = "Ảnh"; +"share.kind.file" = "Tệp"; diff --git a/client/ios/App/ShareExtension/zh-Hans.lproj/Localizable.strings b/client/ios/App/ShareExtension/zh-Hans.lproj/Localizable.strings new file mode 100644 index 000000000..926b0c3d6 --- /dev/null +++ b/client/ios/App/ShareExtension/zh-Hans.lproj/Localizable.strings @@ -0,0 +1,26 @@ +/* The share sheet ("TREK" in the share menu): ShareExtension/ShareView.swift. */ +"share.title" = "添加到 TREK"; +"share.cancel" = "取消"; +"share.close" = "关闭"; +"share.trip" = "行程"; +"share.noTrips" = "打开一次 TREK,你的行程就会显示在这里。"; +"share.hint" = "TREK 会读取预订并添加到行程中。"; +"share.later" = "稍后选择"; +"share.add" = "添加"; +"share.addTo" = "添加到 %@"; +"share.added" = "已添加"; +"share.reading" = "正在读取…"; +"share.tooLarge" = "此文件太大,无法导入。"; +"share.unsupported" = "TREK 无法导入此内容。"; +"share.failed" = "无法传给 TREK。请重试。"; +"share.now" = "进行中"; +"share.past" = "已结束"; +"share.tomorrow" = "明天"; +"share.inDays" = "%d 天后"; +"share.kind.pass" = "Wallet 卡券"; +"share.kind.pdf" = "PDF"; +"share.kind.mail" = "邮件"; +"share.kind.page" = "网页"; +"share.kind.text" = "文本"; +"share.kind.image" = "照片"; +"share.kind.file" = "文件"; diff --git a/client/ios/App/ShareExtension/zh-Hant.lproj/Localizable.strings b/client/ios/App/ShareExtension/zh-Hant.lproj/Localizable.strings new file mode 100644 index 000000000..7f5ca7ddd --- /dev/null +++ b/client/ios/App/ShareExtension/zh-Hant.lproj/Localizable.strings @@ -0,0 +1,26 @@ +/* The share sheet ("TREK" in the share menu): ShareExtension/ShareView.swift. */ +"share.title" = "加入 TREK"; +"share.cancel" = "取消"; +"share.close" = "關閉"; +"share.trip" = "行程"; +"share.noTrips" = "開啟一次 TREK,你的行程就會顯示在這裡。"; +"share.hint" = "TREK 會讀取訂位並加入行程。"; +"share.later" = "稍後選擇"; +"share.add" = "加入"; +"share.addTo" = "加入 %@"; +"share.added" = "已加入"; +"share.reading" = "正在讀取…"; +"share.tooLarge" = "此檔案太大,無法匯入。"; +"share.unsupported" = "TREK 無法匯入此內容。"; +"share.failed" = "無法傳給 TREK。請重試。"; +"share.now" = "進行中"; +"share.past" = "已結束"; +"share.tomorrow" = "明天"; +"share.inDays" = "%d 天後"; +"share.kind.pass" = "Wallet 票卡"; +"share.kind.pdf" = "PDF"; +"share.kind.mail" = "郵件"; +"share.kind.page" = "網頁"; +"share.kind.text" = "文字"; +"share.kind.image" = "照片"; +"share.kind.file" = "檔案"; diff --git a/client/ios/scripts/add-extension.rb b/client/ios/scripts/add-extension.rb index 40f8798c6..71ab18891 100644 --- a/client/ios/scripts/add-extension.rb +++ b/client/ios/scripts/add-extension.rb @@ -2,15 +2,15 @@ # # ruby ios/scripts/add-extension.rb [extra frameworks...] # -# type is "notification" (Notification Service Extension) or "widget" (WidgetKit, -# which also carries the Live Activities). The extension's sources live in +# type is "notification" (Notification Service Extension), "widget" (WidgetKit, +# which also carries the Live Activities) or "share" (the share sheet entry). The extension's sources live in # ios/App// with an Info.plist and .entitlements next to them. The # project file stays checked in, so the targets show in Xcode like any other; # this script is how they got there, and running it again changes nothing. require 'xcodeproj' name, bundle_id, type, *frameworks = ARGV -abort 'usage: add-extension.rb [frameworks...]' unless name && bundle_id && type +abort 'usage: add-extension.rb [frameworks...]' unless name && bundle_id && type project_path = File.expand_path('../App/App.xcodeproj', __dir__) project = Xcodeproj::Project.open(project_path) @@ -34,9 +34,25 @@ group = project.main_group.find_subpath(name, true) group.set_source_tree('') group.set_path(name) sources = Dir.children(File.expand_path("../App/#{name}", __dir__)).sort -sources.each do |file| +# Localizations go in as one variant group, so Xcode copies each language's +# strings into the extension bundle; JavaScript (Safari's page preprocessing) is +# a resource as well. +lprojs, files = sources.partition { |file| file.end_with?('.lproj') } +files.each do |file| ref = group.new_reference(file) target.add_file_references([ref]) if file.end_with?('.swift') + target.add_resources([ref]) if file.end_with?('.js') +end +unless lprojs.empty? + variant = group.new_variant_group('Localizable.strings') + lprojs.each do |dir| + lang = dir.delete_suffix('.lproj') + ref = variant.new_reference("#{dir}/Localizable.strings") + ref.name = lang + ref.last_known_file_type = 'text.plist.strings' + project.root_object.known_regions |= [lang] + end + target.add_resources([variant]) end frameworks.each do |framework| diff --git a/client/src/mobile/components/NativeImportSheet.test.tsx b/client/src/mobile/components/NativeImportSheet.test.tsx index dea53dd9d..63395e961 100644 --- a/client/src/mobile/components/NativeImportSheet.test.tsx +++ b/client/src/mobile/components/NativeImportSheet.test.tsx @@ -38,6 +38,17 @@ describe('NativeImportSheet', () => { expect(navigate).toHaveBeenCalledWith('/trips/2'); }); + it('goes straight to the trip the share sheet picked, without asking', () => { + render(); + act(() => { + useSharedImportStore.getState().setFile(new File(['x'], 'booking.pdf', { type: 'application/pdf' })); + useSharedImportStore.getState().chooseTrip(2); + }); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + expect(navigate).toHaveBeenCalledWith('/trips/2'); + expect(navigate).toHaveBeenCalledTimes(1); + }); + it('stays closed with nothing shared', () => { render(); expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); diff --git a/client/src/mobile/components/NativeImportSheet.tsx b/client/src/mobile/components/NativeImportSheet.tsx index 81487f3de..447e5a690 100644 --- a/client/src/mobile/components/NativeImportSheet.tsx +++ b/client/src/mobile/components/NativeImportSheet.tsx @@ -5,7 +5,7 @@ */ import { ChevronRight, FileText } from 'lucide-react'; import React, { useEffect, useState } from 'react'; -import { useNavigate } from 'react-router'; +import { useLocation, useNavigate } from 'react-router'; import { useTranslation } from '../../i18n'; import { useSharedImportStore } from '../../native/sharedFiles'; import { tripRepo } from '../../repo/tripRepo'; @@ -22,6 +22,7 @@ interface TripRow { export default function NativeImportSheet(): React.ReactElement | null { const { t } = useTranslation(); const navigate = useNavigate(); + const { pathname } = useLocation(); const file = useSharedImportStore((s) => s.file); const tripId = useSharedImportStore((s) => s.tripId); const chooseTrip = useSharedImportStore((s) => s.chooseTrip); @@ -46,10 +47,14 @@ export default function NativeImportSheet(): React.ReactElement | null { ); }, [open]); - const pick = (id: number) => { - chooseTrip(id); - void navigate(`/trips/${id}`); - }; + // Once a trip is picked (here, or in the share sheet before the app opened), go to + // it; its planner opens the booking import with the file (MTripShell). + useEffect(() => { + if (!file || tripId == null || pathname.startsWith(`/trips/${tripId}`)) return; + void navigate(`/trips/${tripId}`); + }, [file, tripId, pathname, navigate]); + + const pick = (id: number) => chooseTrip(id); return ( setFile(null)} variant="bottom" ariaLabel={t('native.share.title')}> diff --git a/client/src/native/sharedFiles.test.ts b/client/src/native/sharedFiles.test.ts index d3dc5d3ce..d1ba69ad7 100644 --- a/client/src/native/sharedFiles.test.ts +++ b/client/src/native/sharedFiles.test.ts @@ -43,6 +43,13 @@ describe('shared booking documents', () => { expect(useSharedImportStore.getState()).toMatchObject({ file: null, tripId: null }); }); + it('takes the trip the share sheet already picked', async () => { + vi.mocked(TrekShell.takeSharedFile).mockResolvedValue({ file: { ...PDF, tripId: 12 } }); + await startSharedFiles(); + expect(useSharedImportStore.getState()).toMatchObject({ tripId: 12 }); + expect(useSharedImportStore.getState().file?.name).toBe('booking.pdf'); + }); + it('does nothing in a binary without the feature', async () => { vi.mocked(hasNativeFeature).mockReturnValue(false); vi.mocked(TrekShell.addListener).mockClear(); diff --git a/client/src/native/sharedFiles.ts b/client/src/native/sharedFiles.ts index 830b9c00f..e7065bd57 100644 --- a/client/src/native/sharedFiles.ts +++ b/client/src/native/sharedFiles.ts @@ -44,7 +44,11 @@ export async function startSharedFiles(): Promise { await loadNativeCapabilities(); if (!hasNativeFeature('sharedFiles')) return; const accept = (payload: SharedFilePayload | null) => { - if (payload?.data) useSharedImportStore.getState().setFile(toFile(payload)); + if (!payload?.data) return; + const store = useSharedImportStore.getState(); + store.setFile(toFile(payload)); + // The share sheet asked for the trip already; NativeImportSheet opens it. + if (typeof payload.tripId === 'number') store.chooseTrip(payload.tripId); }; void TrekShell.addListener('sharedFile', accept); try { diff --git a/client/src/native/trekShell.ts b/client/src/native/trekShell.ts index fdf567b3a..4bf1f93c2 100644 --- a/client/src/native/trekShell.ts +++ b/client/src/native/trekShell.ts @@ -95,6 +95,8 @@ export interface SharedFilePayload { mimeType: string; /** base64 */ data: string; + /** Set when the share sheet already asked which trip it belongs to (feature shareSheet). */ + tripId?: number; } /** A position, or an error shaped like GeolocationPositionError (1 denied, 2 unavailable). */