feat(native): TREK in the share sheet, with the trip picked right there

A share extension puts TREK in the share sheet of Mail, Safari, Files, Wallet
and any app that shares a document, a link, text or a photo. It shows what came
in (a Wallet pass, a PDF, a mail, the page as Safari rendered it, text, a photo)
and the user's trips with their covers, the next one already picked, and hands
both to the app, which opens that trip's booking import with the file in place.
"Choose later" leaves the trip question to the app's import sheet as before.

- ShareExtension target (add-extension.rb learns "share": JavaScript and the
  localizations go in as resources), bundle com.liketrek.trek.share, signed in
  the release lane, its plists linted in native.yml
- Safari hands over the rendered page (SharePage.js), so a confirmation behind
  a sign-in arrives whole; other apps' links are fetched
- The item travels through the shared keychain group the widgets already use;
  the app takes it on com.liketrek.trek://share or when it next becomes active
- Feature shareSheet; strings in the app's 28 languages
This commit is contained in:
Maurice
2026-11-07 10:05:37 +01:00
parent 792e55b2ca
commit 7b8bd4b166
46 changed files with 2006 additions and 63 deletions
+1 -1
View File
@@ -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:
+1
View File
@@ -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
+270 -50
View File
@@ -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 = "<group>"; };
04C0771C8582246A26E0C98E /* PackingContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PackingContent.swift; sourceTree = "<group>"; };
090D74588DA8436DEC275567 /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = "<group>"; };
095A4E5D36F6E22070842EC1 /* nl */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
0A7C4491A7ABDDBEBE00E5BF /* sk */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = "<group>"; };
0BA66BF6D1F4FB57EFF31981 /* ShareExtension.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = ShareExtension.entitlements; sourceTree = "<group>"; };
114C13FB91B554AE8DC4372B /* et */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/Localizable.strings; sourceTree = "<group>"; };
12DE24E0B7E2E5F3564D952B /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/Localizable.strings; sourceTree = "<group>"; };
15884CA899CAD3FFD98BA927 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = "<group>"; };
15CAD7E43FE81EA6E2DD2585 /* TrekStyle.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TrekStyle.swift; sourceTree = "<group>"; };
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 = "<group>"; };
1D8F2B5132A2F1B7089E767F /* vi */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = "<group>"; };
1E7522315D47F086145DC4B8 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
1ED347C91E03E51FDF8ABC1A /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = "<group>"; };
22A904A2E55946892F488B1A /* th */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Localizable.strings; sourceTree = "<group>"; };
241C37D114DDD53C7E915CB8 /* id */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
24B175DE91E1E6E850DB0285 /* TodayContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TodayContent.swift; sourceTree = "<group>"; };
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 = "<group>"; };
3259F6C40216CECEBAC6F307 /* TripDayLiveActivity.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TripDayLiveActivity.swift; sourceTree = "<group>"; };
32A62EE30F8AD514D7DB2CAA /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = "<group>"; };
337775CD9091673D0ADDF325 /* TripDayAttributes.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TripDayAttributes.swift; sourceTree = "<group>"; };
38C632E7EE9C0DF1CA0BF0A0 /* cs */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
3CB565FC61AB5AEA87A3F615 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = "<group>"; };
3DF4F4305878F949DD09193F /* NotificationService.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = NotificationService.entitlements; sourceTree = "<group>"; };
405B04DFF3A978D1046147C6 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/Localizable.strings; sourceTree = "<group>"; };
408F0E543D49BFCCF6B19194 /* es */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; };
4167040E2C29E44D55580916 /* ru */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
4D004544572B0E85DE29A7B8 /* AtlasContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AtlasContent.swift; sourceTree = "<group>"; };
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
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 = "<group>"; };
@@ -94,61 +133,75 @@
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
56050CCA8DE4BEB4680D9E0B /* ShareHandoff.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ShareHandoff.swift; sourceTree = "<group>"; };
5C0109EA628FA897B7BFDD3E /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Localizable.strings; sourceTree = "<group>"; };
608F00CBD96957EEA888E50A /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
60EB00F3670B41B898765DF9 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
611A99D899DC374A5B7E1560 /* tr */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = "<group>"; };
63D3A4166431A36A5C30F942 /* az */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = az; path = az.lproj/Localizable.strings; sourceTree = "<group>"; };
66E7B341F942576F4BF6E4BA /* zh-Hans */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
683725459244601B71D92E2D /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
6932A417DC4BC05BF07D80E2 /* it */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; };
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 = "<group>"; };
77A0112CA76E5C1787073AEB /* hu */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = "<group>"; };
77C217524DA98A6A92F422F4 /* zh-Hant */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = "<group>"; };
7A1E00012C0000000000A001 /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = "<group>"; };
7A1E00022C0000000000A002 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
7A1E00042C0000000000A004 /* TrekBridgeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrekBridgeViewController.swift; sourceTree = "<group>"; };
7A1E00062C0000000000A006 /* TrekShellPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrekShellPlugin.swift; sourceTree = "<group>"; };
7D5041DDDAFF5B491B053999 /* CollabContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CollabContent.swift; sourceTree = "<group>"; };
7D525040492AFBC121FD4CC6 /* ar */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = "<group>"; };
8442B4B8988AF5D1BFA3ADE3 /* TripIntent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TripIntent.swift; sourceTree = "<group>"; };
84CB5C3B6BD4E1DE1A8BD2B4 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
8A0289CB321AE008D8815A19 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
8C2E47D0B005674C2B4ADA56 /* TREKWidgetsBundle.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TREKWidgetsBundle.swift; sourceTree = "<group>"; };
8D361F7B14AE9F5ACE3BFE78 /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/Localizable.strings; sourceTree = "<group>"; };
9582B6822FE993A50072D4E8 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
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 = "<group>"; };
9D673E1909A5B750376ECB41 /* en */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
9DC696E1A1FD3A1A1695F9EA /* SharePage.js */ = {isa = PBXFileReference; includeInIndex = 1; path = SharePage.js; sourceTree = "<group>"; };
9F756414EFBB3542F65C14CE /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = "<group>"; };
A030A06726542A2D7F61C54B /* TrekFamily.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TrekFamily.swift; sourceTree = "<group>"; };
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 = "<group>"; };
A2F3670F49C3D74C3D98E457 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
A479036A85683EFF2922D096 /* el */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = "<group>"; };
A5C182FC253CBEE8D5439536 /* ca */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/Localizable.strings; sourceTree = "<group>"; };
AA02F5CBC22E08415CC1C9BE /* pl */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = "<group>"; };
AEF7DCAA1AEE67F1291668D1 /* TodosContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TodosContent.swift; sourceTree = "<group>"; };
B0450E66BFCF00CC4097330A /* he */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/Localizable.strings; sourceTree = "<group>"; };
B680CBF1268D2EE1982B165C /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = "<group>"; };
B7F33B7A5C27C126DF29DCA4 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = "<group>"; };
BB2A15008B5664C48B639F94 /* ShareItem.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ShareItem.swift; sourceTree = "<group>"; };
BBB2E19CD540AB30B44BECCA /* TREKWidgets.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = TREKWidgets.entitlements; sourceTree = "<group>"; };
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 = "<group>"; };
C7064A9DA92FCC8ACAA4F22A /* NextTripWidget.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NextTripWidget.swift; sourceTree = "<group>"; };
DBF6D74B7888714BF264E0E6 /* WidgetSnapshot.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WidgetSnapshot.swift; sourceTree = "<group>"; };
8442B4B8988AF5D1BFA3ADE3 /* TripIntent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TripIntent.swift; sourceTree = "<group>"; };
4D004544572B0E85DE29A7B8 /* AtlasContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AtlasContent.swift; sourceTree = "<group>"; };
04C0771C8582246A26E0C98E /* PackingContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PackingContent.swift; sourceTree = "<group>"; };
7D5041DDDAFF5B491B053999 /* CollabContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CollabContent.swift; sourceTree = "<group>"; };
C996A3060A2D20A4B5C9C377 /* ko */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = "<group>"; };
CC077989456FB9DB5F4FF676 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = "<group>"; };
CD6763B965A87DDB6FCC2D05 /* de */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
CE79157F1170A47CF7FC6FE0 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D0AC751335E0136865E08990 /* WorldGrid.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WorldGrid.swift; sourceTree = "<group>"; };
15CAD7E43FE81EA6E2DD2585 /* TrekStyle.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TrekStyle.swift; sourceTree = "<group>"; };
A030A06726542A2D7F61C54B /* TrekFamily.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TrekFamily.swift; sourceTree = "<group>"; };
DE6873D246AC6B8E2CB6C8B4 /* TripContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TripContent.swift; sourceTree = "<group>"; };
24B175DE91E1E6E850DB0285 /* TodayContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TodayContent.swift; sourceTree = "<group>"; };
E6D2F4930E5F152C8090C4F8 /* BookingsContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BookingsContent.swift; sourceTree = "<group>"; };
AEF7DCAA1AEE67F1291668D1 /* TodosContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TodosContent.swift; sourceTree = "<group>"; };
FFA8802204810ED580DE46F3 /* NotificationService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
84CB5C3B6BD4E1DE1A8BD2B4 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
EE30A6DF943052C6ADE92D63 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
ECC80E1314B31ED2D2347411 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = "<group>"; };
7328217E23761C22814FEC25 /* az */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = az; path = az.lproj/Localizable.strings; sourceTree = "<group>"; };
CC077989456FB9DB5F4FF676 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt-BR; path = pt-BR.lproj/Localizable.strings; sourceTree = "<group>"; };
405B04DFF3A978D1046147C6 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/Localizable.strings; sourceTree = "<group>"; };
D84D92C18CC022646692CB9B /* sv */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = "<group>"; };
DBF6D74B7888714BF264E0E6 /* WidgetSnapshot.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WidgetSnapshot.swift; sourceTree = "<group>"; };
DC0C9AB4D77D77A7BC2BA85B /* ja */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
DD341749E47B7DF89BA9A46D /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
9A3FAA9DBC67D12389EEF0CF /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; };
8D361F7B14AE9F5ACE3BFE78 /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/Localizable.strings; sourceTree = "<group>"; };
60EB00F3670B41B898765DF9 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
B680CBF1268D2EE1982B165C /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = "<group>"; };
12DE24E0B7E2E5F3564D952B /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/Localizable.strings; sourceTree = "<group>"; };
15884CA899CAD3FFD98BA927 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = "<group>"; };
A2F3670F49C3D74C3D98E457 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
1947CC62E5A6FF3483BEB578 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; };
8A0289CB321AE008D8815A19 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
B7F33B7A5C27C126DF29DCA4 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = "<group>"; };
683725459244601B71D92E2D /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
32A62EE30F8AD514D7DB2CAA /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = "<group>"; };
037DA05706AE051CEFCA7D75 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
090D74588DA8436DEC275567 /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = "<group>"; };
9F756414EFBB3542F65C14CE /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = "<group>"; };
5C0109EA628FA897B7BFDD3E /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Localizable.strings; sourceTree = "<group>"; };
DE6873D246AC6B8E2CB6C8B4 /* TripContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TripContent.swift; sourceTree = "<group>"; };
DE8B592FE52EC6FFB589751E /* fr */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
E2CFBDBCC474A7A72E053A8E /* uk */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = "<group>"; };
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 = "<group>"; };
A06BBDA4CF70465F2E3F21D3 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = "<group>"; };
3CB565FC61AB5AEA87A3F615 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = "<group>"; };
608F00CBD96957EEA888E50A /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh-Hans; path = zh-Hans.lproj/Localizable.strings; sourceTree = "<group>"; };
1ED347C91E03E51FDF8ABC1A /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh-Hant; path = zh-Hant.lproj/Localizable.strings; sourceTree = "<group>"; };
E6831E849332626BE8D54CEF /* ShareView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ShareView.swift; sourceTree = "<group>"; };
E6D2F4930E5F152C8090C4F8 /* BookingsContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BookingsContent.swift; sourceTree = "<group>"; };
EAFBDD97043B34E4B64FDA86 /* pt-BR */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = "<group>"; };
ECC80E1314B31ED2D2347411 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = "<group>"; };
ED561D7B7219D23A99FAEB11 /* ShareViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
EE30A6DF943052C6ADE92D63 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
FFA8802204810ED580DE46F3 /* NotificationService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
/* 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 = "<group>";
};
@@ -206,6 +268,7 @@
504EC3041FED79650016851F /* App.app */,
A03A384FE54FAD1ED68E12DC /* NotificationService.appex */,
702D0DF4A7E323CEA94CA03F /* TREKWidgets.appex */,
694631A1018DCE5678F29930 /* ShareExtension.appex */,
);
name = Products;
sourceTree = "<group>";
@@ -238,6 +301,7 @@
BC64330AC19625949E8DC05A /* WidgetKit.framework */,
6E901288FC198E4D33BE754F /* SwiftUI.framework */,
192E69C7E66646BC39566925 /* ActivityKit.framework */,
E45050B942C96E1ACCAB7CE9 /* UniformTypeIdentifiers.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -253,6 +317,22 @@
path = NotificationService;
sourceTree = "<group>";
};
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 = "<group>";
};
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 = "<group>";
};
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 = "<group>";
};
/* 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 = (
+25 -1
View File
@@ -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<UIOpenURLContext>) {
@@ -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
+5 -1
View File
@@ -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)
+53
View File
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>TREK</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<string>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</string>
<key>NSExtensionJavaScriptPreprocessingFile</key>
<string>SharePage</string>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
<key>NSExtensionPrincipalClass</key>
<string>ShareViewController</string>
</dict>
</dict>
</plist>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.liketrek.trek.shared</string>
</array>
</dict>
</plist>
@@ -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
}
}
@@ -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: "<title", options: .caseInsensitive),
let start = html.range(of: ">", range: open.upperBound..<html.endIndex),
let end = html.range(of: "</title>", options: .caseInsensitive, range: start.upperBound..<html.endIndex)
else { return nil }
let title = html[start.upperBound..<end.lowerBound].trimmingCharacters(in: .whitespacesAndNewlines)
return title.isEmpty ? nil : String(title.prefix(120))
}
private static func slug(_ title: String) -> 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) }
}
}
}
}
@@ -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();
@@ -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)
}
}
@@ -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/<lang>.lproj/Localizable.strings).
func L(_ key: String) -> String {
NSLocalizedString(key, comment: "")
}
@@ -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" = "ملف";
@@ -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";
@@ -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";
@@ -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";
@@ -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";
@@ -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" = "Αρχείο";
@@ -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";
@@ -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";
@@ -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";
@@ -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";
@@ -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" = "קובץ";
@@ -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";
@@ -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";
@@ -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";
@@ -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" = "ファイル";
@@ -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" = "파일";
@@ -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";
@@ -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";
@@ -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";
@@ -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" = "Файл";
@@ -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";
@@ -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";
@@ -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" = "ไฟล์";
@@ -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";
@@ -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" = "Файл";
@@ -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";
@@ -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" = "文件";
@@ -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" = "檔案";
+20 -4
View File
@@ -2,15 +2,15 @@
#
# ruby ios/scripts/add-extension.rb <name> <bundle-id> <type> [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/<name>/ with an Info.plist and <name>.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 <name> <bundle-id> <notification|widget> [frameworks...]' unless name && bundle_id && type
abort 'usage: add-extension.rb <name> <bundle-id> <notification|widget|share> [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>')
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|
@@ -38,6 +38,17 @@ describe('NativeImportSheet', () => {
expect(navigate).toHaveBeenCalledWith('/trips/2');
});
it('goes straight to the trip the share sheet picked, without asking', () => {
render(<NativeImportSheet />);
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(<NativeImportSheet />);
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
@@ -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 (
<MSheet open={open} onClose={() => setFile(null)} variant="bottom" ariaLabel={t('native.share.title')}>
+7
View File
@@ -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();
+5 -1
View File
@@ -44,7 +44,11 @@ export async function startSharedFiles(): Promise<void> {
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 {
+2
View File
@@ -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). */