Skip to content

Commit 0c787a2

Browse files
author
Conrad Kramer
committed
Added XML App Link Resolver
1 parent 21eaa1d commit 0c787a2

13 files changed

+407
-194
lines changed

Bolts.podspec

+2
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ Pod::Spec.new do |s|
4242

4343
ss.ios.source_files = 'Bolts/iOS/*.[hm]'
4444
ss.ios.public_header_files = 'Bolts/iOS/*.h'
45+
ss.ios.libraries = 'xml2'
46+
ss.ios.xcconfig = { 'HEADER_SEARCH_PATHS' => '$(SDKROOT)/usr/include/libxml2' }
4547
ss.osx.source_files = ''
4648
ss.watchos.source_files = ''
4749
ss.tvos.source_files = ''

Bolts.xcodeproj/project.pbxproj

+24
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,14 @@
166166
8E8C8F2917F241FF00E3F1C7 /* TaskTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8E9C3D1C17DE9F6500427E62 /* TaskTests.m */; };
167167
8EA6BF691805CF5600337041 /* BoltsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8EA6BF681805CF5600337041 /* BoltsTests.m */; };
168168
8EDDA63017E17DDC00655F8A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8E9C3CEC17DE9DE000427E62 /* Foundation.framework */; };
169+
D0A9104F1A86BF8500BF399F /* BFXMLAppLinkResolver.h in Headers */ = {isa = PBXBuildFile; fileRef = D0A9104D1A86BF8500BF399F /* BFXMLAppLinkResolver.h */; settings = {ATTRIBUTES = (Public, ); }; };
170+
D0A910541A86C83E00BF399F /* BFAppLinkResolvingPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = D0A910531A86C83E00BF399F /* BFAppLinkResolvingPrivate.h */; settings = {ATTRIBUTES = (Private, ); }; };
171+
D0B41D791C3EF97A00510849 /* BFAppLinkResolving.m in Sources */ = {isa = PBXBuildFile; fileRef = D0A910511A86C4AF00BF399F /* BFAppLinkResolving.m */; };
172+
D0B41D7A1C3EF97B00510849 /* BFAppLinkResolving.m in Sources */ = {isa = PBXBuildFile; fileRef = D0A910511A86C4AF00BF399F /* BFAppLinkResolving.m */; };
173+
D0B41D7B1C3EF98200510849 /* BFXMLAppLinkResolver.m in Sources */ = {isa = PBXBuildFile; fileRef = D0A9104E1A86BF8500BF399F /* BFXMLAppLinkResolver.m */; };
174+
D0B41D7C1C3EF98300510849 /* BFXMLAppLinkResolver.m in Sources */ = {isa = PBXBuildFile; fileRef = D0A9104E1A86BF8500BF399F /* BFXMLAppLinkResolver.m */; };
175+
D0B41D7D1C3EFB6300510849 /* BFAppLinkResolvingPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = D0A910531A86C83E00BF399F /* BFAppLinkResolvingPrivate.h */; settings = {ATTRIBUTES = (Private, ); }; };
176+
D0B41D7E1C3EFB6F00510849 /* BFXMLAppLinkResolver.h in Headers */ = {isa = PBXBuildFile; fileRef = D0A9104D1A86BF8500BF399F /* BFXMLAppLinkResolver.h */; settings = {ATTRIBUTES = (Public, ); }; };
169177
F5AFC9EC1BA752750076E927 /* BFTaskCompletionSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 8103FA5319900A84000BAE3F /* BFTaskCompletionSource.m */; };
170178
F5AFC9ED1BA752750076E927 /* BFTask.m in Sources */ = {isa = PBXBuildFile; fileRef = 8103FA5119900A84000BAE3F /* BFTask.m */; };
171179
F5AFC9EE1BA752750076E927 /* Bolts.m in Sources */ = {isa = PBXBuildFile; fileRef = 8103FA5519900A84000BAE3F /* Bolts.m */; };
@@ -309,6 +317,10 @@
309317
B242FAB919A567660097ECAE /* BFMeasurementEvent.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BFMeasurementEvent.m; sourceTree = "<group>"; };
310318
B242FABA19A567660097ECAE /* BFURL_Internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BFURL_Internal.h; sourceTree = "<group>"; };
311319
B242FAC019A599CD0097ECAE /* BFMeasurementEvent_Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BFMeasurementEvent_Internal.h; sourceTree = "<group>"; };
320+
D0A9104D1A86BF8500BF399F /* BFXMLAppLinkResolver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BFXMLAppLinkResolver.h; sourceTree = "<group>"; };
321+
D0A9104E1A86BF8500BF399F /* BFXMLAppLinkResolver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BFXMLAppLinkResolver.m; sourceTree = "<group>"; };
322+
D0A910511A86C4AF00BF399F /* BFAppLinkResolving.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BFAppLinkResolving.m; sourceTree = "<group>"; };
323+
D0A910531A86C83E00BF399F /* BFAppLinkResolvingPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BFAppLinkResolvingPrivate.h; sourceTree = "<group>"; };
312324
F5AFCA021BA752750076E927 /* Bolts.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Bolts.framework; sourceTree = BUILT_PRODUCTS_DIR; };
313325
F5AFCA131BA752770076E927 /* BoltsTests-tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "BoltsTests-tvOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
314326
F5AFCA151BA752AF0076E927 /* Bolts-tvOS.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Bolts-tvOS.xcconfig"; sourceTree = "<group>"; };
@@ -464,8 +476,12 @@
464476
8103FA5A19900A84000BAE3F /* BFAppLinkNavigation.h */,
465477
8103FA5B19900A84000BAE3F /* BFAppLinkNavigation.m */,
466478
8103FA5C19900A84000BAE3F /* BFAppLinkResolving.h */,
479+
D0A910531A86C83E00BF399F /* BFAppLinkResolvingPrivate.h */,
480+
D0A910511A86C4AF00BF399F /* BFAppLinkResolving.m */,
467481
8103FA6619900A84000BAE3F /* BFWebViewAppLinkResolver.h */,
468482
8103FA6719900A84000BAE3F /* BFWebViewAppLinkResolver.m */,
483+
D0A9104D1A86BF8500BF399F /* BFXMLAppLinkResolver.h */,
484+
D0A9104E1A86BF8500BF399F /* BFXMLAppLinkResolver.m */,
469485
8103FA5D19900A84000BAE3F /* BFAppLinkReturnToRefererController.h */,
470486
8103FA5E19900A84000BAE3F /* BFAppLinkReturnToRefererController.m */,
471487
8103FA5F19900A84000BAE3F /* BFAppLinkReturnToRefererView.h */,
@@ -642,6 +658,7 @@
642658
buildActionMask = 2147483647;
643659
files = (
644660
1D5D7DBA1BE3CE8200FD67C7 /* BFWebViewAppLinkResolver.h in Headers */,
661+
D0A9104F1A86BF8500BF399F /* BFXMLAppLinkResolver.h in Headers */,
645662
1D5D7DBB1BE3CE8200FD67C7 /* BFCancellationTokenRegistration.h in Headers */,
646663
1D5D7DBC1BE3CE8200FD67C7 /* BFTask.h in Headers */,
647664
1D5D7DBD1BE3CE8200FD67C7 /* BFAppLinkNavigation.h in Headers */,
@@ -661,6 +678,7 @@
661678
1D5D7DCC1BE3CE8200FD67C7 /* Bolts.h in Headers */,
662679
1D5D7DCD1BE3CE8200FD67C7 /* BFCancellationToken.h in Headers */,
663680
1D5D7DCE1BE3CE8200FD67C7 /* BFAppLink.h in Headers */,
681+
D0A910541A86C83E00BF399F /* BFAppLinkResolvingPrivate.h in Headers */,
664682
1D5D7DCF1BE3CE8200FD67C7 /* BFAppLinkReturnToRefererController.h in Headers */,
665683
);
666684
runOnlyForDeploymentPostprocessing = 0;
@@ -729,6 +747,7 @@
729747
isa = PBXHeadersBuildPhase;
730748
buildActionMask = 2147483647;
731749
files = (
750+
D0B41D7E1C3EFB6F00510849 /* BFXMLAppLinkResolver.h in Headers */,
732751
81ED94311BE1481900795F05 /* BFWebViewAppLinkResolver.h in Headers */,
733752
81ED941D1BE147CF00795F05 /* BFCancellationTokenRegistration.h in Headers */,
734753
81ED941E1BE147CF00795F05 /* BFTask.h in Headers */,
@@ -739,6 +758,7 @@
739758
81ED94381BE1481900795F05 /* BFAppLinkTarget.h in Headers */,
740759
81ED943D1BE1481900795F05 /* BFURL_Internal.h in Headers */,
741760
81ED94301BE1481900795F05 /* BFAppLinkResolving.h in Headers */,
761+
D0B41D7D1C3EFB6300510849 /* BFAppLinkResolvingPrivate.h in Headers */,
742762
81ED94371BE1481900795F05 /* BFAppLinkReturnToRefererView_Internal.h in Headers */,
743763
81ED943E1BE1481900795F05 /* BFURL.h in Headers */,
744764
81ED94221BE147CF00795F05 /* BFTaskCompletionSource.h in Headers */,
@@ -1057,6 +1077,8 @@
10571077
1D5D7DB41BE3CE8200FD67C7 /* BFAppLink.m in Sources */,
10581078
1D5D7DB51BE3CE8200FD67C7 /* BFExecutor.m in Sources */,
10591079
1D5D7DB61BE3CE8200FD67C7 /* BFCancellationToken.m in Sources */,
1080+
D0B41D7B1C3EF98200510849 /* BFXMLAppLinkResolver.m in Sources */,
1081+
D0B41D791C3EF97A00510849 /* BFAppLinkResolving.m in Sources */,
10601082
);
10611083
runOnlyForDeploymentPostprocessing = 0;
10621084
};
@@ -1130,6 +1152,8 @@
11301152
81ED942D1BE1481900795F05 /* BFAppLink.m in Sources */,
11311153
81ED94181BE147CF00795F05 /* BFExecutor.m in Sources */,
11321154
81ED94191BE147CF00795F05 /* BFCancellationToken.m in Sources */,
1155+
D0B41D7C1C3EF98300510849 /* BFXMLAppLinkResolver.m in Sources */,
1156+
D0B41D7A1C3EF97B00510849 /* BFAppLinkResolving.m in Sources */,
11331157
);
11341158
runOnlyForDeploymentPostprocessing = 0;
11351159
};

Bolts/Common/Bolts.h

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#import <Bolts/BFMeasurementEvent.h>
2727
#import <Bolts/BFURL.h>
2828
#import <Bolts/BFWebViewAppLinkResolver.h>
29+
#import <Bolts/BFXMLAppLinkResolver.h>
2930
#endif
3031

3132
NS_ASSUME_NONNULL_BEGIN

Bolts/iOS/BFAppLinkNavigation.h

+1-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@
1010

1111
#import <Foundation/Foundation.h>
1212

13-
#import <Bolts/BFAppLink.h>
14-
1513
/*!
1614
The result of calling navigate on a BFAppLinkNavigation
1715
*/
@@ -25,6 +23,7 @@ typedef NS_ENUM(NSInteger, BFAppLinkNavigationType) {
2523
};
2624

2725
@protocol BFAppLinkResolving;
26+
@class BFAppLink;
2827
@class BFTask;
2928

3029
/*!

Bolts/iOS/BFAppLinkResolving.m

+181
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
/*
2+
* Copyright (c) 2014, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
*/
10+
11+
#import <UIKit/UIKit.h>
12+
13+
#import "BFAppLinkResolving.h"
14+
#import "BFAppLinkResolvingPrivate.h"
15+
#import "BFAppLink.h"
16+
#import "BFAppLinkTarget.h"
17+
#import "BFTask.h"
18+
#import "BFTaskCompletionSource.h"
19+
20+
NSString *const BFAppLinkResolverPreferHeader = @"Prefer-Html-Meta-Tags";
21+
NSString *const BFAppLinkResolverMetaTagPrefix = @"al";
22+
23+
NSString *const BFAppLinkResolverRedirectDataKey = @"data";
24+
NSString *const BFAppLinkResolverRedirectResponseKey = @"response";
25+
26+
static NSString *const BFAppLinkResolverIOSURLKey = @"url";
27+
static NSString *const BFAppLinkResolverIOSAppStoreIdKey = @"app_store_id";
28+
static NSString *const BFAppLinkResolverIOSAppNameKey = @"app_name";
29+
static NSString *const BFAppLinkResolverDictionaryValueKey = @"_value";
30+
static NSString *const BFAppLinkResolverWebKey = @"web";
31+
static NSString *const BFAppLinkResolverIOSKey = @"ios";
32+
static NSString *const BFAppLinkResolverIPhoneKey = @"iphone";
33+
static NSString *const BFAppLinkResolverIPadKey = @"ipad";
34+
static NSString *const BFAppLinkResolverWebURLKey = @"url";
35+
static NSString *const BFAppLinkResolverShouldFallbackKey = @"should_fallback";
36+
37+
NSDictionary *BFAppLinkResolverParseALData(NSArray *dataArray) {
38+
NSMutableDictionary *al = [NSMutableDictionary dictionary];
39+
for (NSDictionary *tag in dataArray) {
40+
NSString *name = tag[@"property"];
41+
if (![name isKindOfClass:[NSString class]]) {
42+
continue;
43+
}
44+
NSArray *nameComponents = [name componentsSeparatedByString:@":"];
45+
if (![nameComponents[0] isEqualToString:BFAppLinkResolverMetaTagPrefix]) {
46+
continue;
47+
}
48+
NSMutableDictionary *root = al;
49+
for (int i = 1; i < nameComponents.count; i++) {
50+
NSMutableArray *children = root[nameComponents[i]];
51+
if (!children) {
52+
children = [NSMutableArray array];
53+
root[nameComponents[i]] = children;
54+
}
55+
NSMutableDictionary *child = children.lastObject;
56+
if (!child || i == nameComponents.count - 1) {
57+
child = [NSMutableDictionary dictionary];
58+
[children addObject:child];
59+
}
60+
root = child;
61+
}
62+
if (tag[@"content"]) {
63+
root[BFAppLinkResolverDictionaryValueKey] = tag[@"content"];
64+
}
65+
}
66+
return al;
67+
}
68+
69+
BFAppLink *BFAppLinkResolverAppLinkFromALData(NSDictionary *appLinkDict, NSURL *destination) {
70+
NSMutableArray *linkTargets = [NSMutableArray array];
71+
72+
NSArray *platformData = nil;
73+
switch (UI_USER_INTERFACE_IDIOM()) {
74+
case UIUserInterfaceIdiomPad:
75+
platformData = @[ appLinkDict[BFAppLinkResolverIPadKey] ?: @{},
76+
appLinkDict[BFAppLinkResolverIOSKey] ?: @{} ];
77+
break;
78+
case UIUserInterfaceIdiomPhone:
79+
platformData = @[ appLinkDict[BFAppLinkResolverIPhoneKey] ?: @{},
80+
appLinkDict[BFAppLinkResolverIOSKey] ?: @{} ];
81+
break;
82+
#ifdef __TVOS_9_0
83+
case UIUserInterfaceIdiomTV:
84+
#endif
85+
case UIUserInterfaceIdiomUnspecified:
86+
default:
87+
// Future-proofing. Other User Interface idioms should only hit ios.
88+
platformData = @[ appLinkDict[BFAppLinkResolverIOSKey] ?: @{} ];
89+
break;
90+
}
91+
92+
for (NSArray *platformObjects in platformData) {
93+
for (NSDictionary *platformDict in platformObjects) {
94+
// The schema requires a single url/app store id/app name,
95+
// but we could find multiple of them. We'll make a best effort
96+
// to interpret this data.
97+
NSArray *urls = platformDict[BFAppLinkResolverIOSURLKey];
98+
NSArray *appStoreIds = platformDict[BFAppLinkResolverIOSAppStoreIdKey];
99+
NSArray *appNames = platformDict[BFAppLinkResolverIOSAppNameKey];
100+
101+
NSUInteger maxCount = MAX(urls.count, MAX(appStoreIds.count, appNames.count));
102+
103+
for (NSUInteger i = 0; i < maxCount; i++) {
104+
NSString *urlString = urls[i][BFAppLinkResolverDictionaryValueKey];
105+
NSURL *url = urlString ? [NSURL URLWithString:urlString] : nil;
106+
NSString *appStoreId = appStoreIds[i][BFAppLinkResolverDictionaryValueKey];
107+
NSString *appName = appNames[i][BFAppLinkResolverDictionaryValueKey];
108+
BFAppLinkTarget *target = [BFAppLinkTarget appLinkTargetWithURL:url
109+
appStoreId:appStoreId
110+
appName:appName];
111+
[linkTargets addObject:target];
112+
}
113+
}
114+
}
115+
116+
NSDictionary *webDict = appLinkDict[BFAppLinkResolverWebKey][0];
117+
NSString *webUrlString = webDict[BFAppLinkResolverWebURLKey][0][BFAppLinkResolverDictionaryValueKey];
118+
NSString *shouldFallbackString = webDict[BFAppLinkResolverShouldFallbackKey][0][BFAppLinkResolverDictionaryValueKey];
119+
120+
NSURL *webUrl = destination;
121+
122+
if (shouldFallbackString &&
123+
[@[ @"no", @"false", @"0" ] containsObject:[shouldFallbackString lowercaseString]]) {
124+
webUrl = nil;
125+
}
126+
if (webUrl && webUrlString) {
127+
webUrl = [NSURL URLWithString:webUrlString];
128+
}
129+
130+
return [BFAppLink appLinkWithSourceURL:destination
131+
targets:linkTargets
132+
webURL:webUrl];
133+
}
134+
135+
BFTask *BFFollowRedirects(NSURL *url) {
136+
// This task will be resolved with either the redirect NSURL
137+
// or a dictionary with the response data to be returned.
138+
BFTaskCompletionSource *tcs = [BFTaskCompletionSource taskCompletionSource];
139+
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
140+
[request setValue:BFAppLinkResolverMetaTagPrefix forHTTPHeaderField:BFAppLinkResolverPreferHeader];
141+
142+
void (^completion)(NSURLResponse *response, NSData *data, NSError *error) = ^(NSURLResponse *response, NSData *data, NSError *error) {
143+
if (error) {
144+
[tcs setError:error];
145+
return;
146+
}
147+
148+
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
149+
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
150+
151+
// NSURLConnection usually follows redirects automatically, but the
152+
// documentation is unclear what the default is. This helps it along.
153+
if (httpResponse.statusCode >= 300 && httpResponse.statusCode < 400) {
154+
NSString *redirectString = httpResponse.allHeaderFields[@"Location"];
155+
NSURL *redirectURL = [NSURL URLWithString:redirectString];
156+
[tcs setResult:redirectURL];
157+
return;
158+
}
159+
}
160+
161+
[tcs setResult:@{ BFAppLinkResolverRedirectResponseKey : response, BFAppLinkResolverRedirectDataKey : data }];
162+
};
163+
164+
#if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_7_0 || __MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_9
165+
NSURLSession *session = [NSURLSession sharedSession];
166+
[[session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
167+
completion(response, data, error);
168+
}] resume];
169+
#else
170+
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:completion];
171+
#endif
172+
173+
return [tcs.task continueWithSuccessBlock:^id(BFTask *task) {
174+
// If we redirected, just keep recursing.
175+
if ([task.result isKindOfClass:[NSURL class]]) {
176+
return BFFollowRedirects(task.result);
177+
}
178+
return task;
179+
}];
180+
}
181+

Bolts/iOS/BFAppLinkResolvingPrivate.h

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright (c) 2014, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
*/
10+
11+
@class BFAppLink;
12+
13+
/*
14+
Builds up a data structure filled with the app link data from the meta tags on a page.
15+
The structure of this object is a dictionary where each key holds an array of app link
16+
data dictionaries. Values are stored in a key called "_value".
17+
*/
18+
extern NSDictionary *BFAppLinkResolverParseALData(NSArray *dataArray);
19+
20+
/*
21+
Converts app link data into a BFAppLink containing the targets relevant for this platform.
22+
*/
23+
extern BFAppLink *BFAppLinkResolverAppLinkFromALData(NSDictionary *appLinkDict, NSURL *destination);
24+
25+
/*
26+
The returned task will be resolved with a dictionary containing the response data
27+
*/
28+
extern BFTask *BFFollowRedirects(NSURL *url);
29+
30+
extern NSString *const BFAppLinkResolverPreferHeader;
31+
extern NSString *const BFAppLinkResolverMetaTagPrefix;
32+
33+
extern NSString *const BFAppLinkResolverRedirectDataKey;
34+
extern NSString *const BFAppLinkResolverRedirectResponseKey;

0 commit comments

Comments
 (0)