Merged PR 13825: Update iOS graph notifications sample for 1.0
This commit is contained in:
Родитель
eca68e949b
Коммит
cde7439268
|
@ -7,41 +7,34 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
0017A4902135B52800EB86D8 /* AADAccountProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 0017A4892135B52700EB86D8 /* AADAccountProvider.m */; };
|
||||
0017A4912135B52800EB86D8 /* MSATokenRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 0017A48A2135B52700EB86D8 /* MSATokenRequest.m */; };
|
||||
0017A4922135B52800EB86D8 /* AADMSAAccountProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 0017A48C2135B52700EB86D8 /* AADMSAAccountProvider.m */; };
|
||||
0017A4932135B52800EB86D8 /* MSATokenCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 0017A48D2135B52700EB86D8 /* MSATokenCache.m */; };
|
||||
0017A4942135B52800EB86D8 /* MSAAccountProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 0017A48F2135B52800EB86D8 /* MSAAccountProvider.m */; };
|
||||
0017A49D2135D10E00EB86D8 /* NotificationProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 0017A49C2135D10E00EB86D8 /* NotificationProvider.m */; };
|
||||
0017D23921E55C6300FFE2A5 /* MSAAccount.m in Sources */ = {isa = PBXBuildFile; fileRef = 0017D22E21E55C6200FFE2A5 /* MSAAccount.m */; };
|
||||
0017D23A21E55C6300FFE2A5 /* AADAccount.m in Sources */ = {isa = PBXBuildFile; fileRef = 0017D23221E55C6200FFE2A5 /* AADAccount.m */; };
|
||||
0017D23B21E55C6300FFE2A5 /* MSATokenCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 0017D23321E55C6200FFE2A5 /* MSATokenCache.m */; };
|
||||
0017D23C21E55C6300FFE2A5 /* MSATokenRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 0017D23521E55C6300FFE2A5 /* MSATokenRequest.m */; };
|
||||
003421A921347622007FC970 /* NotificationsManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 003421A821347622007FC970 /* NotificationsManager.m */; };
|
||||
00823361212F114B0055F6E4 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 00823360212F114B0055F6E4 /* AppDelegate.m */; };
|
||||
00823364212F114B0055F6E4 /* RootViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 00823363212F114B0055F6E4 /* RootViewController.m */; };
|
||||
00823367212F114B0055F6E4 /* LoginViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 00823366212F114B0055F6E4 /* LoginViewController.m */; };
|
||||
0082336A212F114B0055F6E4 /* ModelController.m in Sources */ = {isa = PBXBuildFile; fileRef = 00823369212F114B0055F6E4 /* ModelController.m */; };
|
||||
0082336D212F114B0055F6E4 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0082336B212F114B0055F6E4 /* Main.storyboard */; };
|
||||
0082336F212F114B0055F6E4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0082336E212F114B0055F6E4 /* Assets.xcassets */; };
|
||||
00823372212F114B0055F6E4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 00823370212F114B0055F6E4 /* LaunchScreen.storyboard */; };
|
||||
00823375212F114B0055F6E4 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 00823374212F114B0055F6E4 /* main.m */; };
|
||||
00D6D060213720E5008E5E33 /* NotificationsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 00D6D05F213720E5008E5E33 /* NotificationsViewController.m */; };
|
||||
00D6D06321384D00008E5E33 /* HistoryViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 00D6D06221384D00008E5E33 /* HistoryViewController.m */; };
|
||||
86E3681E27114147F58BE403 /* libPods-GraphNotificationsSample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 06928972061C754C239718DC /* libPods-GraphNotificationsSample.a */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
0017A4892135B52700EB86D8 /* AADAccountProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AADAccountProvider.m; sourceTree = "<group>"; };
|
||||
0017A48A2135B52700EB86D8 /* MSATokenRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSATokenRequest.m; sourceTree = "<group>"; };
|
||||
0017A48B2135B52700EB86D8 /* MSATokenCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSATokenCache.h; sourceTree = "<group>"; };
|
||||
0017A48C2135B52700EB86D8 /* AADMSAAccountProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AADMSAAccountProvider.m; sourceTree = "<group>"; };
|
||||
0017A48D2135B52700EB86D8 /* MSATokenCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSATokenCache.m; sourceTree = "<group>"; };
|
||||
0017A48E2135B52700EB86D8 /* MSATokenRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSATokenRequest.h; sourceTree = "<group>"; };
|
||||
0017A48F2135B52800EB86D8 /* MSAAccountProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSAAccountProvider.m; sourceTree = "<group>"; };
|
||||
0017A4952135B53C00EB86D8 /* SingleUserAccountProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SingleUserAccountProvider.h; sourceTree = "<group>"; };
|
||||
0017A4962135B53C00EB86D8 /* MSAAccountProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSAAccountProvider.h; sourceTree = "<group>"; };
|
||||
0017A4972135B53C00EB86D8 /* SampleAccountActionFailureReason.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SampleAccountActionFailureReason.h; sourceTree = "<group>"; };
|
||||
0017A4982135B53C00EB86D8 /* AADAccountProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AADAccountProvider.h; sourceTree = "<group>"; };
|
||||
0017A4992135B53C00EB86D8 /* AADMSAAccountProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AADMSAAccountProvider.h; sourceTree = "<group>"; };
|
||||
0017A49B2135D10A00EB86D8 /* NotificationProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NotificationProvider.h; sourceTree = "<group>"; };
|
||||
0017A49C2135D10E00EB86D8 /* NotificationProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NotificationProvider.m; sourceTree = "<group>"; };
|
||||
0017D22E21E55C6200FFE2A5 /* MSAAccount.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSAAccount.m; sourceTree = "<group>"; };
|
||||
0017D22F21E55C6200FFE2A5 /* MSATokenCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSATokenCache.h; sourceTree = "<group>"; };
|
||||
0017D23021E55C6200FFE2A5 /* SampleAccountActionFailureReason.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SampleAccountActionFailureReason.h; sourceTree = "<group>"; };
|
||||
0017D23121E55C6200FFE2A5 /* SingleUserAccountProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SingleUserAccountProvider.h; sourceTree = "<group>"; };
|
||||
0017D23221E55C6200FFE2A5 /* AADAccount.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AADAccount.m; sourceTree = "<group>"; };
|
||||
0017D23321E55C6200FFE2A5 /* MSATokenCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSATokenCache.m; sourceTree = "<group>"; };
|
||||
0017D23421E55C6300FFE2A5 /* MSAAccount.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSAAccount.h; sourceTree = "<group>"; };
|
||||
0017D23521E55C6300FFE2A5 /* MSATokenRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSATokenRequest.m; sourceTree = "<group>"; };
|
||||
0017D23621E55C6300FFE2A5 /* MSATokenRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSATokenRequest.h; sourceTree = "<group>"; };
|
||||
0017D23721E55C6300FFE2A5 /* AADAccount.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AADAccount.h; sourceTree = "<group>"; };
|
||||
0017D23821E55C6300FFE2A5 /* SignInAccount.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SignInAccount.h; sourceTree = "<group>"; };
|
||||
003421A72130A887007FC970 /* NotificationsManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NotificationsManager.h; sourceTree = "<group>"; };
|
||||
003421A821347622007FC970 /* NotificationsManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NotificationsManager.m; sourceTree = "<group>"; };
|
||||
0082335C212F114B0055F6E4 /* GraphNotificationsSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GraphNotificationsSample.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
|
@ -51,8 +44,6 @@
|
|||
00823363212F114B0055F6E4 /* RootViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RootViewController.m; sourceTree = "<group>"; };
|
||||
00823365212F114B0055F6E4 /* LoginViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LoginViewController.h; sourceTree = "<group>"; };
|
||||
00823366212F114B0055F6E4 /* LoginViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LoginViewController.m; sourceTree = "<group>"; };
|
||||
00823368212F114B0055F6E4 /* ModelController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ModelController.h; sourceTree = "<group>"; };
|
||||
00823369212F114B0055F6E4 /* ModelController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ModelController.m; sourceTree = "<group>"; };
|
||||
0082336C212F114B0055F6E4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
0082336E212F114B0055F6E4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
00823371212F114B0055F6E4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
|
@ -60,8 +51,6 @@
|
|||
00823374212F114B0055F6E4 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
|
||||
00D6D05E21372041008E5E33 /* NotificationsViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NotificationsViewController.h; sourceTree = "<group>"; };
|
||||
00D6D05F213720E5008E5E33 /* NotificationsViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NotificationsViewController.m; sourceTree = "<group>"; };
|
||||
00D6D06121384BA3008E5E33 /* HistoryViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HistoryViewController.h; sourceTree = "<group>"; };
|
||||
00D6D06221384D00008E5E33 /* HistoryViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HistoryViewController.m; sourceTree = "<group>"; };
|
||||
00D6D0642138519C008E5E33 /* GraphNotificationsSample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GraphNotificationsSample.entitlements; sourceTree = "<group>"; };
|
||||
06928972061C754C239718DC /* libPods-GraphNotificationsSample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-GraphNotificationsSample.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
63E3FBAFA254E6B80A8DA76E /* Pods-GraphNotifications.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GraphNotifications.release.xcconfig"; path = "Pods/Target Support Files/Pods-GraphNotifications/Pods-GraphNotifications.release.xcconfig"; sourceTree = "<group>"; };
|
||||
|
@ -87,18 +76,17 @@
|
|||
0017A49A2135B55400EB86D8 /* SampleAccountProviders */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0017A4982135B53C00EB86D8 /* AADAccountProvider.h */,
|
||||
0017A4992135B53C00EB86D8 /* AADMSAAccountProvider.h */,
|
||||
0017A4962135B53C00EB86D8 /* MSAAccountProvider.h */,
|
||||
0017A4972135B53C00EB86D8 /* SampleAccountActionFailureReason.h */,
|
||||
0017A4952135B53C00EB86D8 /* SingleUserAccountProvider.h */,
|
||||
0017A4892135B52700EB86D8 /* AADAccountProvider.m */,
|
||||
0017A48C2135B52700EB86D8 /* AADMSAAccountProvider.m */,
|
||||
0017A48F2135B52800EB86D8 /* MSAAccountProvider.m */,
|
||||
0017A48B2135B52700EB86D8 /* MSATokenCache.h */,
|
||||
0017A48D2135B52700EB86D8 /* MSATokenCache.m */,
|
||||
0017A48E2135B52700EB86D8 /* MSATokenRequest.h */,
|
||||
0017A48A2135B52700EB86D8 /* MSATokenRequest.m */,
|
||||
0017D23721E55C6300FFE2A5 /* AADAccount.h */,
|
||||
0017D23221E55C6200FFE2A5 /* AADAccount.m */,
|
||||
0017D23421E55C6300FFE2A5 /* MSAAccount.h */,
|
||||
0017D22E21E55C6200FFE2A5 /* MSAAccount.m */,
|
||||
0017D22F21E55C6200FFE2A5 /* MSATokenCache.h */,
|
||||
0017D23321E55C6200FFE2A5 /* MSATokenCache.m */,
|
||||
0017D23621E55C6300FFE2A5 /* MSATokenRequest.h */,
|
||||
0017D23521E55C6300FFE2A5 /* MSATokenRequest.m */,
|
||||
0017D23021E55C6200FFE2A5 /* SampleAccountActionFailureReason.h */,
|
||||
0017D23821E55C6300FFE2A5 /* SignInAccount.h */,
|
||||
0017D23121E55C6200FFE2A5 /* SingleUserAccountProvider.h */,
|
||||
);
|
||||
path = SampleAccountProviders;
|
||||
sourceTree = "<group>";
|
||||
|
@ -126,16 +114,12 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
00D6D0642138519C008E5E33 /* GraphNotificationsSample.entitlements */,
|
||||
0017A49B2135D10A00EB86D8 /* NotificationProvider.h */,
|
||||
0017A49C2135D10E00EB86D8 /* NotificationProvider.m */,
|
||||
0082335F212F114B0055F6E4 /* AppDelegate.h */,
|
||||
00823360212F114B0055F6E4 /* AppDelegate.m */,
|
||||
00823362212F114B0055F6E4 /* RootViewController.h */,
|
||||
00823363212F114B0055F6E4 /* RootViewController.m */,
|
||||
00823365212F114B0055F6E4 /* LoginViewController.h */,
|
||||
00823366212F114B0055F6E4 /* LoginViewController.m */,
|
||||
00823368212F114B0055F6E4 /* ModelController.h */,
|
||||
00823369212F114B0055F6E4 /* ModelController.m */,
|
||||
0082336B212F114B0055F6E4 /* Main.storyboard */,
|
||||
0082336E212F114B0055F6E4 /* Assets.xcassets */,
|
||||
00823370212F114B0055F6E4 /* LaunchScreen.storyboard */,
|
||||
|
@ -145,8 +129,6 @@
|
|||
003421A821347622007FC970 /* NotificationsManager.m */,
|
||||
00D6D05E21372041008E5E33 /* NotificationsViewController.h */,
|
||||
00D6D05F213720E5008E5E33 /* NotificationsViewController.m */,
|
||||
00D6D06121384BA3008E5E33 /* HistoryViewController.h */,
|
||||
00D6D06221384D00008E5E33 /* HistoryViewController.m */,
|
||||
913EB4AA217E75C700A78C79 /* Secrets.h */,
|
||||
);
|
||||
path = GraphNotificationsSample;
|
||||
|
@ -184,6 +166,7 @@
|
|||
00823359212F114B0055F6E4 /* Frameworks */,
|
||||
0082335A212F114B0055F6E4 /* Resources */,
|
||||
534E2B6519087712F45067D1 /* [CP] Embed Pods Frameworks */,
|
||||
C62E42AF81F89136AC56D4FD /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
|
@ -207,6 +190,9 @@
|
|||
CreatedOnToolsVersion = 9.2;
|
||||
ProvisioningStyle = Manual;
|
||||
SystemCapabilities = {
|
||||
com.apple.BackgroundModes = {
|
||||
enabled = 1;
|
||||
};
|
||||
com.apple.Push = {
|
||||
enabled = 1;
|
||||
};
|
||||
|
@ -281,7 +267,22 @@
|
|||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
C62E42AF81F89136AC56D4FD /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-GraphNotificationsSample/Pods-GraphNotificationsSample-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
@ -291,20 +292,16 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
0017A4922135B52800EB86D8 /* AADMSAAccountProvider.m in Sources */,
|
||||
00823375212F114B0055F6E4 /* main.m in Sources */,
|
||||
00823364212F114B0055F6E4 /* RootViewController.m in Sources */,
|
||||
003421A921347622007FC970 /* NotificationsManager.m in Sources */,
|
||||
0017A4942135B52800EB86D8 /* MSAAccountProvider.m in Sources */,
|
||||
00D6D06321384D00008E5E33 /* HistoryViewController.m in Sources */,
|
||||
0017A4932135B52800EB86D8 /* MSATokenCache.m in Sources */,
|
||||
0082336A212F114B0055F6E4 /* ModelController.m in Sources */,
|
||||
00D6D060213720E5008E5E33 /* NotificationsViewController.m in Sources */,
|
||||
0017A49D2135D10E00EB86D8 /* NotificationProvider.m in Sources */,
|
||||
0017A4902135B52800EB86D8 /* AADAccountProvider.m in Sources */,
|
||||
00823367212F114B0055F6E4 /* LoginViewController.m in Sources */,
|
||||
0017D23B21E55C6300FFE2A5 /* MSATokenCache.m in Sources */,
|
||||
00823361212F114B0055F6E4 /* AppDelegate.m in Sources */,
|
||||
0017A4912135B52800EB86D8 /* MSATokenRequest.m in Sources */,
|
||||
0017D23C21E55C6300FFE2A5 /* MSATokenRequest.m in Sources */,
|
||||
0017D23A21E55C6300FFE2A5 /* AADAccount.m in Sources */,
|
||||
0017D23921E55C6300FFE2A5 /* MSAAccount.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -359,7 +356,7 @@
|
|||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_IDENTITY = "iPhone Distribution";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
|
@ -414,7 +411,7 @@
|
|||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_IDENTITY = "iPhone Distribution";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
|
@ -429,6 +426,7 @@
|
|||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.2;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
|
@ -440,17 +438,18 @@
|
|||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = GraphNotificationsSample/GraphNotificationsSample.entitlements;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_IDENTITY = "iPhone Distribution";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
DEVELOPMENT_TEAM = UBF8T346G9;
|
||||
DEVELOPMENT_TEAM = 9UFF8D37DM;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = GraphNotificationsSample/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
LIBRARY_SEARCH_PATHS = "$(inherited)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.connecteddevices.graphnotifications;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.connecteddevices.graphnotificationsample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE = "ba3afbc5-30e6-4150-9eb9-d8967de4ce9b";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "Graph Notifications Development";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "GN Adhoc";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
|
@ -461,17 +460,18 @@
|
|||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = GraphNotificationsSample/GraphNotificationsSample.entitlements;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_IDENTITY = "iPhone Distribution";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
DEVELOPMENT_TEAM = UBF8T346G9;
|
||||
DEVELOPMENT_TEAM = 9UFF8D37DM;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = GraphNotificationsSample/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
LIBRARY_SEARCH_PATHS = "$(inherited)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.connecteddevices.graphnotifications;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.connecteddevices.graphnotificationsample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE = "ba3afbc5-30e6-4150-9eb9-d8967de4ce9b";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "Graph Notifications Development";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "GN Adhoc";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
Secrets.h
|
|
@ -7,10 +7,16 @@
|
|||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <ConnectedDevices/ConnectedDevices.h>
|
||||
#import <UserNotifications/UserNotifications.h>
|
||||
|
||||
@interface AppDelegate : UIResponder <UIApplicationDelegate>
|
||||
@interface AppDelegate : UIResponder <UIApplicationDelegate, UNUserNotificationCenterDelegate>
|
||||
|
||||
@property (strong, nonatomic) UIWindow *window;
|
||||
@property (strong, nonatomic) UIWindow* window;
|
||||
@property (strong, nonatomic) MCDConnectedDevicesPlatform* platform;
|
||||
- (void)initializePlatform;
|
||||
- (void)startPlatform;
|
||||
- (void)registerNotificationsForAccount:(MCDConnectedDevicesAccount*)account callback:(void(^)(BOOL,NSError*))callback;
|
||||
|
||||
|
||||
@end
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
|
||||
#import "AppDelegate.h"
|
||||
#import "NotificationProvider.h"
|
||||
|
||||
#import "NotificationsManager.h"
|
||||
|
||||
void uncaughtExceptionHandler(NSException* uncaughtException)
|
||||
{
|
||||
|
@ -9,62 +8,181 @@ void uncaughtExceptionHandler(NSException* uncaughtException)
|
|||
}
|
||||
|
||||
@interface AppDelegate ()
|
||||
|
||||
@property (nonatomic) MCDConnectedDevicesNotificationRegistration* notificationRegistration;
|
||||
@property (nonatomic) MCDConnectedDevicesAccount* pendingAccount;
|
||||
@property (nonatomic) void (^pendingCallback)(BOOL,NSError*);
|
||||
- (void)createNotificationRegistrationWithToken:(NSString* _Nonnull)deviceToken;
|
||||
- (BOOL)processNotification:(NSDictionary* _Nonnull)userInfo;
|
||||
@end
|
||||
|
||||
@implementation AppDelegate
|
||||
|
||||
- (void)initializePlatform
|
||||
{
|
||||
if (!self.platform)
|
||||
{
|
||||
self.platform = [MCDConnectedDevicesPlatform new];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
|
||||
- (void)startPlatform
|
||||
{
|
||||
[self.platform start];
|
||||
}
|
||||
|
||||
- (void)registerNotificationsForAccount:(MCDConnectedDevicesAccount*)account callback:(void(^)(BOOL,NSError*))callback
|
||||
{
|
||||
@try
|
||||
{
|
||||
if (self.notificationRegistration)
|
||||
{
|
||||
NSLog(@"GraphNotifications Registering notifications with registration with token %@ and appId %@", self.notificationRegistration.token, self.notificationRegistration.appId);
|
||||
[self.platform.notificationRegistrationManager registerForAccountAsync:account registration:self.notificationRegistration callback:callback];
|
||||
self.pendingAccount = nil;
|
||||
self.pendingCallback = nil;
|
||||
NSLog(@"GraphNotifications Successfully registered notification");
|
||||
}
|
||||
else
|
||||
{
|
||||
NSLog(@"GraphNotifications Do not have notification registration yet, pending registration for account %@", account.accountId);
|
||||
self.pendingAccount = account;
|
||||
self.pendingCallback = callback;
|
||||
}
|
||||
}
|
||||
@catch(NSException* e)
|
||||
{
|
||||
NSLog(@"GraphNotifications Failed to register with %@", e.description);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)createNotificationRegistrationWithToken:(NSString* _Nonnull)deviceToken
|
||||
{
|
||||
_notificationRegistration = [[MCDConnectedDevicesNotificationRegistration alloc] init];
|
||||
_notificationRegistration.type = MCDNotificationTypeAPN;
|
||||
_notificationRegistration.appId = [[NSBundle mainBundle] bundleIdentifier];
|
||||
_notificationRegistration.appDisplayName = (NSString*)[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"];
|
||||
_notificationRegistration.token = deviceToken;
|
||||
NSLog(@"GraphNotifications Successfully created notification registration!");
|
||||
if (self.pendingAccount)
|
||||
{
|
||||
[self registerNotificationsForAccount:self.pendingAccount callback:self.pendingCallback];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)processNotification:(NSDictionary* _Nonnull)userInfo
|
||||
{
|
||||
@try
|
||||
{
|
||||
if ([NSJSONSerialization isValidJSONObject:userInfo])
|
||||
{
|
||||
id romeData = userInfo[@"rome-data"];
|
||||
if ([romeData isKindOfClass:NSDictionary.class])
|
||||
{
|
||||
userInfo = romeData;
|
||||
}
|
||||
|
||||
// Forward the notification to CDP.
|
||||
NSError* error;
|
||||
NSData* data = [NSJSONSerialization dataWithJSONObject:userInfo options:0 error:&error];
|
||||
if (data != nil && error == nil)
|
||||
{
|
||||
NSString* byteString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
||||
MCDConnectedDevicesProcessNotificationOperation* result = [self.platform processNotification:byteString];
|
||||
return result.connectedDevicesNotification;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
NSLog(@"Notification was not valid json! %@", userInfo);
|
||||
}
|
||||
}
|
||||
@catch (NSException* e)
|
||||
{
|
||||
NSLog(@"GraphNotifications Error. Processing notification failed.");
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
||||
{
|
||||
NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);
|
||||
|
||||
[self initializePlatform];
|
||||
[NotificationsManager startWithPlatform:self.platform];
|
||||
[self.platform.notificationRegistrationManager.notificationRegistrationStateChanged subscribe:^(__unused MCDConnectedDevicesNotificationRegistrationManager * _Nonnull manager, MCDConnectedDevicesNotificationRegistrationStateChangedEventArgs * _Nonnull args)
|
||||
{
|
||||
NSLog(@"GraphNotifications NotificationRegistrationState changed to %ld", args.state);
|
||||
if ((args.state == MCDConnectedDevicesNotificationRegistrationStateExpired) || (args.state == MCDConnectedDevicesNotificationRegistrationStateExpiring))
|
||||
{
|
||||
[[NotificationsManager sharedInstance] refresh];
|
||||
}
|
||||
}];
|
||||
|
||||
// Set up Notifications
|
||||
NSDictionary* userInfo = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey];
|
||||
|
||||
|
||||
if (!userInfo)
|
||||
{
|
||||
// User launch app by tapping the App icon normal launch
|
||||
[application
|
||||
registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert categories:nil]];
|
||||
NSLog(@"GraphNotifications launching without user info");
|
||||
UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
[center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge) completionHandler:^(BOOL granted, NSError * _Nullable error)
|
||||
{
|
||||
NSLog(@"GraphNotifications granted notification: %d error: %@", granted, error);
|
||||
}];
|
||||
|
||||
// User launch app by tapping the App icon normal launch
|
||||
[application registerForRemoteNotifications];
|
||||
center.delegate = self;
|
||||
}
|
||||
else
|
||||
{
|
||||
@try
|
||||
{
|
||||
// app run in background and received the push notification, app is launched by user tapping the alert view
|
||||
[MCDNotificationReceiver receiveNotification:userInfo];
|
||||
[self processNotification:userInfo];
|
||||
}
|
||||
@catch(NSException* exception) {
|
||||
NSLog(@"Failed start up notification with exception %@", exception);
|
||||
@catch(NSException* exception)
|
||||
{
|
||||
NSLog(@"GraphNotifications Failed start up notification with exception %@", exception);
|
||||
}
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)application:(__unused UIApplication*)application didFailToRegisterForRemoteNotificationsWithError:(nonnull NSError *)error
|
||||
{
|
||||
NSLog(@"GraphNotifications Failed to register with %@", error);
|
||||
}
|
||||
|
||||
- (void)applicationWillResignActive:(UIApplication *)application {
|
||||
- (void)applicationWillResignActive:(UIApplication *)application
|
||||
{
|
||||
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
|
||||
}
|
||||
|
||||
|
||||
- (void)applicationDidEnterBackground:(UIApplication *)application {
|
||||
- (void)applicationDidEnterBackground:(UIApplication *)application
|
||||
{
|
||||
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
|
||||
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
|
||||
}
|
||||
|
||||
|
||||
- (void)applicationWillEnterForeground:(UIApplication *)application {
|
||||
- (void)applicationWillEnterForeground:(UIApplication *)application
|
||||
{
|
||||
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
|
||||
}
|
||||
|
||||
|
||||
- (void)applicationDidBecomeActive:(UIApplication *)application {
|
||||
- (void)applicationDidBecomeActive:(UIApplication *)application
|
||||
{
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||
}
|
||||
|
||||
|
||||
- (void)applicationWillTerminate:(UIApplication *)application {
|
||||
- (void)applicationWillTerminate:(UIApplication *)application
|
||||
{
|
||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||
}
|
||||
|
||||
|
@ -73,6 +191,7 @@ didRegisterUserNotificationSettings:(__unused UIUserNotificationSettings*)notifi
|
|||
{
|
||||
// actually registerForRemoteNotifications after registerUserNotificationSettings is finished
|
||||
[application registerForRemoteNotifications];
|
||||
[UNUserNotificationCenter currentNotificationCenter].delegate = self;
|
||||
}
|
||||
|
||||
- (void)application:(__unused UIApplication*)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken
|
||||
|
@ -84,41 +203,45 @@ didRegisterUserNotificationSettings:(__unused UIUserNotificationSettings*)notifi
|
|||
{
|
||||
[deviceTokenStr appendFormat:@"%02X", (unsigned int)byteBuffer[i]];
|
||||
}
|
||||
NSLog(@"APNs token: %@", deviceTokenStr);
|
||||
|
||||
NSLog(@"GraphNotifications APNs token: %@", deviceTokenStr);
|
||||
|
||||
@try
|
||||
{
|
||||
// invoke notificationProvider with new notification registration
|
||||
[NotificationProvider
|
||||
updateNotificationRegistration:[[MCDNotificationRegistration alloc]
|
||||
initWithNotificationType:MCDNotificationTypeAPN
|
||||
token:deviceTokenStr
|
||||
appId:[[NSBundle mainBundle] bundleIdentifier]
|
||||
appDisplayName:(NSString*)[[NSBundle mainBundle]
|
||||
objectForInfoDictionaryKey:@"CFBundleDisplayName"]]];
|
||||
[self createNotificationRegistrationWithToken:deviceTokenStr];
|
||||
}
|
||||
@catch (NSException* exception) {
|
||||
NSLog(@"Failed to update notification registration with exception %@", exception);
|
||||
@catch (NSException* exception)
|
||||
{
|
||||
NSLog(@"GraphNotifications Failed to update notification registration with exception %@", exception);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)application:(__unused UIApplication*)application didReceiveRemoteNotification:(nonnull NSDictionary*)userInfo
|
||||
{
|
||||
// app run in foreground and received the push notification, pump notification into CDPPlatform
|
||||
NSLog(@"Received remote notification...");
|
||||
NSLog(@"GraphNotifications Received remote notification...");
|
||||
[userInfo enumerateKeysAndObjectsUsingBlock:^(
|
||||
id _Nonnull key, id _Nonnull obj, __unused BOOL* _Nonnull stop) { NSLog(@"%@: %@", key, obj); }];
|
||||
@try
|
||||
{
|
||||
if (![MCDNotificationReceiver receiveNotification:userInfo])
|
||||
if (![self processNotification:userInfo])
|
||||
{
|
||||
NSLog(@"Received notification was not for Rome");
|
||||
NSLog(@"GraphNotifications Received notification was not for Rome");
|
||||
}
|
||||
}
|
||||
@catch(NSException* exception) {
|
||||
NSLog(@"Failed to receive notification with exception %@", exception);
|
||||
NSLog(@"GraphNotifications Failed to receive notification with exception %@", exception);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)userNotificationCenter:(__unused UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler
|
||||
{
|
||||
completionHandler(UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound);
|
||||
}
|
||||
|
||||
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler
|
||||
{
|
||||
[[NotificationsManager sharedInstance] dismissNotificationWithId:response.notification.request.identifier];
|
||||
completionHandler();
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -33,31 +33,31 @@
|
|||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="s1b-V9-EN7">
|
||||
<rect key="frame" x="20" y="20" width="339" height="627"/>
|
||||
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="nW6-tV-2PU">
|
||||
<rect key="frame" x="0.0" y="280" width="375" height="30"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="fz1-y2-7rY">
|
||||
<rect key="frame" x="0.0" y="112" width="335" height="30"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<state key="normal" title="Login Work/School Account"/>
|
||||
<connections>
|
||||
<action selector="loginAAD" destination="S4R-Ja-viH" eventType="touchUpInside" id="jIC-Q3-hja"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="nW6-tV-2PU">
|
||||
<rect key="frame" x="0.0" y="289" width="335" height="30"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<state key="normal" title="Login Personal Account"/>
|
||||
<connections>
|
||||
<action selector="loginMSA" destination="S4R-Ja-viH" eventType="touchUpInside" id="JLQ-Q2-VCr"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</view>
|
||||
<state key="normal" title="Login with MSA"/>
|
||||
<connections>
|
||||
<action selector="loginMSA" destination="S4R-Ja-viH" eventType="touchUpInside" id="JLQ-Q2-VCr"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="fz1-y2-7rY">
|
||||
<rect key="frame" x="0.0" y="318" width="375" height="30"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<state key="normal" title="Login with AAD"/>
|
||||
<connections>
|
||||
<action selector="loginAAD" destination="S4R-Ja-viH" eventType="touchUpInside" id="jIC-Q3-hja"/>
|
||||
</connections>
|
||||
</button>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Graph Notifications" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="MiM-ed-uOi">
|
||||
<rect key="frame" x="0.0" y="76" width="375" height="21"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" heightSizable="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.97826086960000003" green="0.91848131079999995" blue="0.73914263440000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<viewLayoutGuide key="safeArea" id="K9I-jD-KjA"/>
|
||||
</view>
|
||||
<connections>
|
||||
|
@ -77,13 +77,63 @@
|
|||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<view key="tableHeaderView" contentMode="scaleToFill" id="fPV-Vy-tGM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Notifications" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Vts-Ah-6q5">
|
||||
<rect key="frame" x="8" y="12" width="180" height="21"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="right" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="RnW-fA-Aoo">
|
||||
<rect key="frame" x="205" y="6" width="162" height="30"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<state key="normal" title="Refresh"/>
|
||||
<connections>
|
||||
<action selector="refresh" destination="7uX-h6-yAZ" eventType="touchUpInside" id="NRw-E6-hF8"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</view>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="5s3-lz-tU0">
|
||||
<rect key="frame" x="0.0" y="28" width="375" height="44"/>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="NotificationCell" id="5s3-lz-tU0">
|
||||
<rect key="frame" x="0.0" y="72" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="5s3-lz-tU0" id="UNp-yx-8lI">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" tag="1" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="6GG-xT-kg7">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="14"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" tag="2" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="MoY-k6-SpS">
|
||||
<rect key="frame" x="0.0" y="11" width="375" height="17"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" tag="3" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="XA4-dL-enO">
|
||||
<rect key="frame" x="0.0" y="26" width="181" height="18"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" tag="4" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="oNv-VT-M1b">
|
||||
<rect key="frame" x="189" y="20" width="186" height="30"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<state key="normal" title="Read"/>
|
||||
</button>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
|
@ -95,35 +145,7 @@
|
|||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="gyR-70-0hf" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1310" y="161"/>
|
||||
</scene>
|
||||
<!--Table View Controller-->
|
||||
<scene sceneID="POB-cl-eRc">
|
||||
<objects>
|
||||
<tableViewController id="TH6-n8-5Lo" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="rcJ-uf-0Gl">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="FQB-xj-D8j">
|
||||
<rect key="frame" x="0.0" y="28" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="FQB-xj-D8j" id="IIp-5u-wBC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="TH6-n8-5Lo" id="97g-p9-rkt"/>
|
||||
<outlet property="delegate" destination="TH6-n8-5Lo" id="3lj-z6-Hv2"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="1pT-t6-eTV" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="494" y="483"/>
|
||||
<point key="canvasLocation" x="1309.5999999999999" y="160.56971514242881"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "NotificationsManager.h"
|
||||
|
||||
@interface HistoryViewController : UITableViewController
|
||||
@end
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "HistoryViewController.h"
|
||||
#import "AdaptiveCards/ACFramework.h"
|
||||
|
||||
|
||||
@interface HistoryViewController() {
|
||||
}
|
||||
@property (nonatomic) NSMutableArray<MCDUserNotification*>* notifications;
|
||||
- (void)updateData;
|
||||
- (void)handleTap:(UITapGestureRecognizer*)recognizer;
|
||||
@end
|
||||
|
||||
@implementation HistoryViewController
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
// Do any additional setup after loading the view, typically from a nib.
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[[NotificationsManager sharedInstance] addNotificationsChangedListener:^{
|
||||
[self updateData];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[weakSelf.tableView reloadData];
|
||||
});
|
||||
}];
|
||||
|
||||
[self updateData];
|
||||
}
|
||||
|
||||
- (NSInteger)numberOfSectionsInTableView:(__unused UITableView*)tableView
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
- (nonnull UITableViewCell*)tableView:(nonnull UITableView*)tableView cellForRowAtIndexPath:(nonnull __unused NSIndexPath*)indexPath
|
||||
{
|
||||
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:@"NotificationCell"];
|
||||
MCDUserNotification* notification = self.notifications[indexPath.row];
|
||||
|
||||
ACOAdaptiveCardParseResult* parseResult = [ACOAdaptiveCard fromJson:notification.content];
|
||||
ACRRenderResult* renderResult = [ACRRenderer render:parseResult.card config:[ACOHostConfig new] widthConstraint:0.0f];
|
||||
|
||||
[cell.contentView addSubview:renderResult.view];
|
||||
[cell addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)]];
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(nonnull __unused UITableView*)tableView numberOfRowsInSection:(__unused NSInteger)section
|
||||
{
|
||||
return self.notifications.count;
|
||||
}
|
||||
|
||||
- (void)handleTap:(UITapGestureRecognizer *)recognizer {
|
||||
[[NotificationsManager sharedInstance] readNotificationAtIndex:[self.tableView indexPathForCell:(UITableViewCell*)recognizer.view].row];
|
||||
}
|
||||
|
||||
- (void)updateData {
|
||||
@synchronized (self) {
|
||||
[self.notifications removeAllObjects];
|
||||
for (MCDUserNotification* notification in [NotificationsManager sharedInstance].notifications) {
|
||||
if (notification.status == MCDUserNotificationStatusActive && notification.readState == MCDUserNotificationReadStateUnread && notification.userActionState != MCDUserNotificationUserActionStateNoInteraction) {
|
||||
[self.notifications addObject:notification];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
@ -4,6 +4,8 @@
|
|||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Graph Notifications</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
|
@ -20,6 +22,11 @@
|
|||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>fetch</string>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
|
|
|
@ -8,40 +8,142 @@
|
|||
|
||||
#import "LoginViewController.h"
|
||||
#import "NotificationsManager.h"
|
||||
#import "Secrets.h"
|
||||
#import "AppDelegate.h"
|
||||
|
||||
|
||||
@interface LoginViewController ()
|
||||
@property (nonatomic) AADMSAAccountProvider* accountProvider;
|
||||
@property (nonatomic) AADAccount* aadAccount;
|
||||
@property (nonatomic) MSAAccount* msaAccount;
|
||||
@end
|
||||
|
||||
@implementation LoginViewController
|
||||
|
||||
- (void)viewDidLoad {
|
||||
typedef NS_ENUM(NSInteger, LoginState)
|
||||
{
|
||||
AAD,
|
||||
MSA,
|
||||
SIGNED_OUT
|
||||
};
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
// Do any additional setup after loading the view, typically from a nib.
|
||||
|
||||
self.accountProvider = [NotificationsManager sharedInstance].accountProvider;
|
||||
[self setButtonTextForState:self.accountProvider.signInState];
|
||||
|
||||
self.aadAccount = [[AADAccount alloc] initWithClientId:AAD_CLIENT_ID redirectUri:[NSURL URLWithString:AAD_REDIRECT_URI]];
|
||||
self.msaAccount = [[MSAAccount alloc] initWithClientId:MSA_CLIENT_ID scopeOverrides:@{@"https://activity.windows.com/UserActivity.ReadWrite.CreatedByApp" : @[@"https://activity.windows.com/UserActivity.ReadWrite.CreatedByApp", @"https://activity.windows.com/Notifications.ReadWrite.CreatedByApp"]}];
|
||||
|
||||
AppDelegate* appDelegate = (AppDelegate*)([UIApplication sharedApplication].delegate);
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[appDelegate.platform.accountManager.accessTokenRequested subscribe:^(MCDConnectedDevicesAccountManager * _Nonnull manager __unused, MCDConnectedDevicesAccessTokenRequestedEventArgs * _Nonnull args)
|
||||
{
|
||||
switch ([weakSelf getState])
|
||||
{
|
||||
case AAD:
|
||||
{
|
||||
[weakSelf.aadAccount getAccessTokenForUserAccountIdAsync:args.request.account.accountId scopes:args.request.scopes completion:^(NSString * _Nonnull token, NSError * _Nullable error)
|
||||
{
|
||||
if (error)
|
||||
{
|
||||
[args.request completeWithErrorMessage:error.description];
|
||||
}
|
||||
else
|
||||
{
|
||||
[args.request completeWithAccessToken:token];
|
||||
}
|
||||
}];
|
||||
break;
|
||||
}
|
||||
case MSA:
|
||||
{
|
||||
[weakSelf.msaAccount getAccessTokenForUserAccountIdAsync:args.request.account.accountId scopes:args.request.scopes completion:^(NSString * _Nonnull token, NSError * _Nullable error)
|
||||
{
|
||||
if (error)
|
||||
{
|
||||
[args.request completeWithErrorMessage:error.description];
|
||||
}
|
||||
else
|
||||
{
|
||||
[args.request completeWithAccessToken:token];
|
||||
}
|
||||
}];
|
||||
break;
|
||||
}
|
||||
case SIGNED_OUT:
|
||||
{
|
||||
[args.request completeWithErrorMessage:@"Not currently signed in!"];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
[appDelegate.platform.accountManager.accessTokenInvalidated subscribe:^(MCDConnectedDevicesAccountManager * _Nonnull manager __unused, MCDConnectedDevicesAccessTokenInvalidatedEventArgs * _Nonnull args)
|
||||
{
|
||||
NSLog(@"Access token invalidated for %@ account for %@ scopes", args.account.accountId, args.scopes);
|
||||
}];
|
||||
|
||||
[self setButtonTextForState];
|
||||
|
||||
switch([self getState])
|
||||
{
|
||||
case AAD:
|
||||
[NotificationsManager sharedInstance].account = self.aadAccount.mcdAccount;
|
||||
break;
|
||||
case MSA:
|
||||
[NotificationsManager sharedInstance].account = self.msaAccount.mcdAccount;
|
||||
break;
|
||||
case SIGNED_OUT:
|
||||
// No need to do anything
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (IBAction)loginAAD {
|
||||
- (IBAction)loginAAD
|
||||
{
|
||||
__weak typeof(self) weakSelf = self;
|
||||
switch (self.accountProvider.signInState) {
|
||||
case AADMSAAccountProviderSignInStateSignedInAAD: {
|
||||
[self.accountProvider signOutWithCompletionCallback:^(BOOL successful, SampleAccountActionFailureReason reason) {
|
||||
if (successful) {
|
||||
[weakSelf setButtonTextForState:weakSelf.accountProvider.signInState];
|
||||
} else {
|
||||
NSLog(@"Failed to sign out AAD with reason %ld", reason);
|
||||
switch ([self getState])
|
||||
{
|
||||
case AAD:
|
||||
{
|
||||
[self.aadAccount signOutWithCompletionCallback:^(MCDConnectedDevicesAccount* account, NSError* error)
|
||||
{
|
||||
if (!error)
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(),
|
||||
^{
|
||||
[weakSelf setButtonTextForState];
|
||||
[weakSelf.view setNeedsDisplay];
|
||||
});
|
||||
|
||||
[NotificationsManager sharedInstance].account = nil;
|
||||
}
|
||||
else
|
||||
{
|
||||
NSLog(@"Failed to sign out AAD with reason %@", [error description]);
|
||||
}
|
||||
}];
|
||||
break;
|
||||
}
|
||||
case AADMSAAccountProviderSignInStateSignedOut: {
|
||||
[self.accountProvider signInAADWithCompletionCallback:^(BOOL successful, SampleAccountActionFailureReason reason) {
|
||||
if (successful) {
|
||||
[weakSelf setButtonTextForState:weakSelf.accountProvider.signInState];
|
||||
} else {
|
||||
NSLog(@"Failed to sign in with AAD with reason %ld", reason);
|
||||
case SIGNED_OUT:
|
||||
{
|
||||
[self.aadAccount signInWithCompletionCallback:^(MCDConnectedDevicesAccount* account, NSError* error)
|
||||
{
|
||||
if (!error)
|
||||
{
|
||||
NSLog(@"Signed in to AAD with no error!");
|
||||
dispatch_async(dispatch_get_main_queue(),
|
||||
^{
|
||||
[weakSelf setButtonTextForState];
|
||||
[weakSelf.view setNeedsDisplay];
|
||||
});
|
||||
|
||||
[NotificationsManager sharedInstance].account = account;
|
||||
}
|
||||
else
|
||||
{
|
||||
NSLog(@"Failed to sign in AAD with reason %@", [error description]);
|
||||
}
|
||||
}];
|
||||
break;
|
||||
|
@ -52,25 +154,52 @@
|
|||
}
|
||||
}
|
||||
|
||||
- (IBAction)loginMSA {
|
||||
- (IBAction)loginMSA
|
||||
{
|
||||
__weak typeof(self) weakSelf = self;
|
||||
switch (self.accountProvider.signInState) {
|
||||
case AADMSAAccountProviderSignInStateSignedInMSA: {
|
||||
[self.accountProvider signOutWithCompletionCallback:^(BOOL successful, SampleAccountActionFailureReason reason) {
|
||||
if (successful) {
|
||||
[weakSelf setButtonTextForState:weakSelf.accountProvider.signInState];
|
||||
} else {
|
||||
NSLog(@"Failed to sign out MSA with reason %ld", reason);
|
||||
switch ([self getState])
|
||||
{
|
||||
case MSA:
|
||||
{
|
||||
[self.msaAccount signOutWithCompletionCallback:^(MCDConnectedDevicesAccount* account, NSError* error)
|
||||
{
|
||||
if (!error)
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(),
|
||||
^{
|
||||
[weakSelf setButtonTextForState];
|
||||
[weakSelf.view setNeedsDisplay];
|
||||
});
|
||||
|
||||
[NotificationsManager sharedInstance].account = nil;
|
||||
}
|
||||
else
|
||||
{
|
||||
NSLog(@"Failed to sign out MSA with reason %@", [error description]);
|
||||
}
|
||||
}];
|
||||
break;
|
||||
}
|
||||
case AADMSAAccountProviderSignInStateSignedOut: {
|
||||
[self.accountProvider signInMSAWithCompletionCallback:^(BOOL successful, SampleAccountActionFailureReason reason) {
|
||||
if (successful) {
|
||||
[weakSelf setButtonTextForState:weakSelf.accountProvider.signInState];
|
||||
} else {
|
||||
NSLog(@"Failed to sign in with MSA with reason %ld", reason);
|
||||
case SIGNED_OUT:
|
||||
{
|
||||
NSLog(@"Going to sign in with MSA");
|
||||
[self.msaAccount signInWithCompletionCallback:^(MCDConnectedDevicesAccount* account, NSError* error)
|
||||
{
|
||||
if (!error)
|
||||
{
|
||||
NSLog(@"Signed in to MSA with no error!");
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(),
|
||||
^{
|
||||
[weakSelf setButtonTextForState];
|
||||
[weakSelf.view setNeedsDisplay];
|
||||
});
|
||||
|
||||
[NotificationsManager sharedInstance].account = account;
|
||||
}
|
||||
else
|
||||
{
|
||||
NSLog(@"Failed to sign in MSA with reason %@", [error description]);
|
||||
}
|
||||
}];
|
||||
break;
|
||||
|
@ -81,22 +210,45 @@
|
|||
}
|
||||
}
|
||||
|
||||
- (void)setButtonTextForState:(AADMSAAccountProviderSignInState)state {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
switch (state) {
|
||||
case AADMSAAccountProviderSignInStateSignedOut:
|
||||
- (void)setButtonTextForState
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(),
|
||||
^{
|
||||
switch ([self getState])
|
||||
{
|
||||
case SIGNED_OUT:
|
||||
[self.aadButton setTitle:@"Login Work/School Account" forState:UIControlStateNormal];
|
||||
[self.msaButton setTitle:@"Login Personal Account" forState:UIControlStateNormal];
|
||||
break;
|
||||
case AADMSAAccountProviderSignInStateSignedInAAD:
|
||||
case AAD:
|
||||
[self.aadButton setTitle:@"Logout" forState:UIControlStateNormal];
|
||||
[self.msaButton setTitle:@"" forState:UIControlStateNormal];
|
||||
break;
|
||||
case AADMSAAccountProviderSignInStateSignedInMSA:
|
||||
case MSA:
|
||||
[self.aadButton setTitle:@"" forState:UIControlStateNormal];
|
||||
[self.msaButton setTitle:@"Logout" forState:UIControlStateNormal];
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (LoginState)getState
|
||||
{
|
||||
@synchronized (self)
|
||||
{
|
||||
if (self.aadAccount.isSignedIn)
|
||||
{
|
||||
return AAD;
|
||||
}
|
||||
else if (self.msaAccount.isSignedIn)
|
||||
{
|
||||
return MSA;
|
||||
}
|
||||
else
|
||||
{
|
||||
return SIGNED_OUT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="703-4V-yAM">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Root View Controller-->
|
||||
<scene sceneID="clB-vc-fyl">
|
||||
<objects>
|
||||
<viewController id="703-4V-yAM" customClass="RootViewController" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="502-Ir-ELC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="0.60000002379999995" green="0.40000000600000002" blue="0.20000000300000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<viewLayoutGuide key="safeArea" id="iAZ-4m-2z3"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Q3h-pU-vEd" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-101" y="-243"/>
|
||||
</scene>
|
||||
<!--Login-->
|
||||
<scene sceneID="snT-py-3hH">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="LoginViewController" title="Login" id="S4R-Ja-viH" customClass="LoginViewController" sceneMemberID="viewController">
|
||||
<view key="view" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="ZwX-cT-FIQ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="nW6-tV-2PU">
|
||||
<rect key="frame" x="0.0" y="280" width="375" height="30"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<state key="normal" title="Login with MSA"/>
|
||||
<connections>
|
||||
<action selector="loginMSA" destination="S4R-Ja-viH" eventType="touchUpInside" id="JLQ-Q2-VCr"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="fz1-y2-7rY">
|
||||
<rect key="frame" x="0.0" y="318" width="375" height="30"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<state key="normal" title="Login with AAD"/>
|
||||
<connections>
|
||||
<action selector="loginAAD" destination="S4R-Ja-viH" eventType="touchUpInside" id="jIC-Q3-hja"/>
|
||||
</connections>
|
||||
</button>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Graph Notifications" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="MiM-ed-uOi">
|
||||
<rect key="frame" x="0.0" y="76" width="375" height="21"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" heightSizable="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<viewLayoutGuide key="safeArea" id="K9I-jD-KjA"/>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="aadButton" destination="fz1-y2-7rY" id="W6O-WH-oAW"/>
|
||||
<outlet property="msaButton" destination="nW6-tV-2PU" id="RsH-8N-pIc"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="GmD-Rr-ZGN" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="596" y="-243.32833583208398"/>
|
||||
</scene>
|
||||
<!--Notifications View Controller-->
|
||||
<scene sceneID="BEH-RM-JJY">
|
||||
<objects>
|
||||
<tableViewController storyboardIdentifier="NotificationsViewController" id="7uX-h6-yAZ" customClass="NotificationsViewController" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="bd2-mg-Jex">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<view key="tableHeaderView" contentMode="scaleToFill" id="fPV-Vy-tGM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Notifications" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Vts-Ah-6q5">
|
||||
<rect key="frame" x="8" y="12" width="180" height="21"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="right" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="RnW-fA-Aoo">
|
||||
<rect key="frame" x="205" y="6" width="162" height="30"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<state key="normal" title="Refresh"/>
|
||||
<connections>
|
||||
<action selector="refresh" destination="7uX-h6-yAZ" eventType="touchUpInside" id="NRw-E6-hF8"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</view>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="NotificationCell" id="5s3-lz-tU0">
|
||||
<rect key="frame" x="0.0" y="72" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="5s3-lz-tU0" id="UNp-yx-8lI">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" tag="1" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="6GG-xT-kg7">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="14"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" tag="2" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="MoY-k6-SpS">
|
||||
<rect key="frame" x="0.0" y="11" width="375" height="17"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" tag="3" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="XA4-dL-enO">
|
||||
<rect key="frame" x="0.0" y="26" width="181" height="18"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" tag="4" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="oNv-VT-M1b">
|
||||
<rect key="frame" x="189" y="20" width="186" height="30"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<state key="normal" title="Read"/>
|
||||
</button>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="7uX-h6-yAZ" id="hEu-dZ-9bH"/>
|
||||
<outlet property="delegate" destination="7uX-h6-yAZ" id="Jfe-fL-s75"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="gyR-70-0hf" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1309.5999999999999" y="160.56971514242881"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
|
@ -1,19 +0,0 @@
|
|||
//
|
||||
// ModelController.h
|
||||
// GraphNotifications
|
||||
//
|
||||
// Created by Allen Ballway on 8/23/18.
|
||||
// Copyright © 2018 Microsoft. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class LoginViewController;
|
||||
|
||||
@interface ModelController : NSObject <UIPageViewControllerDataSource>
|
||||
|
||||
- (UIViewController *)viewControllerAtIndex:(NSUInteger)index storyboard:(UIStoryboard *)storyboard;
|
||||
- (NSUInteger)indexOfViewController:(UIViewController *)viewController;
|
||||
|
||||
@end
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
#import "ModelController.h"
|
||||
#import "LoginViewController.h"
|
||||
|
||||
|
||||
|
||||
@interface ModelController ()
|
||||
|
||||
@end
|
||||
|
||||
@implementation ModelController
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
return self;
|
||||
}
|
||||
|
||||
- (UIViewController *)viewControllerAtIndex:(NSUInteger)index storyboard:(UIStoryboard *)storyboard {
|
||||
|
||||
switch(index) {
|
||||
case 0: {
|
||||
LoginViewController *dataViewController = [storyboard instantiateViewControllerWithIdentifier:@"LoginViewController"];
|
||||
return dataViewController;
|
||||
}
|
||||
default:
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (NSUInteger)indexOfViewController:(UIViewController *)viewController {
|
||||
if ([viewController isKindOfClass:[LoginViewController class]]) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return NSNotFound;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Page View Controller Data Source
|
||||
|
||||
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
|
||||
{
|
||||
NSUInteger index = [self indexOfViewController:viewController];
|
||||
if ((index == 0) || (index == NSNotFound)) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [self viewControllerAtIndex:--index storyboard:viewController.storyboard];
|
||||
}
|
||||
|
||||
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
|
||||
{
|
||||
NSUInteger index = [self indexOfViewController:viewController];
|
||||
if (index == NSNotFound || index >= 2) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [self viewControllerAtIndex:++index storyboard:viewController.storyboard];
|
||||
}
|
||||
|
||||
@end
|
|
@ -1,19 +0,0 @@
|
|||
//
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <ConnectedDevices/Core/Core.h>
|
||||
|
||||
// @brief provides an sample implementation of MCDNotificationProvider
|
||||
@interface NotificationProvider : NSObject <MCDNotificationProvider>
|
||||
|
||||
// @brief get the shared instance of MCDNotificationProvider
|
||||
+ (nullable instancetype)sharedInstance;
|
||||
|
||||
// @brief class method update the notification registration provider with new notification registration
|
||||
+ (void)updateNotificationRegistration:(nonnull MCDNotificationRegistration*)notificationRegistration;
|
||||
|
||||
@end
|
|
@ -1,67 +0,0 @@
|
|||
//
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NotificationProvider.h"
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@implementation NotificationProvider
|
||||
{
|
||||
MCDRegistrationUpdatedEvent* _registrationUpdated;
|
||||
MCDNotificationRegistration* _notificationRegistration;
|
||||
}
|
||||
|
||||
+ (instancetype)sharedInstance
|
||||
{
|
||||
static NotificationProvider* sharedInstance = nil;
|
||||
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{ sharedInstance = [[NotificationProvider alloc] init]; });
|
||||
|
||||
return sharedInstance;
|
||||
}
|
||||
|
||||
+ (void)updateNotificationRegistration:(MCDNotificationRegistration*)notificationRegistration
|
||||
{
|
||||
NSLog(@"Raise notification registration changed event\nType: %ld\nApplication: %@", (long)notificationRegistration.type,
|
||||
notificationRegistration.identifier);
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
|
||||
^{ [[NotificationProvider sharedInstance] _updateNotificationRegistration:notificationRegistration]; });
|
||||
}
|
||||
|
||||
// MCDNotificationProvider
|
||||
@synthesize registrationUpdated = _registrationUpdated;
|
||||
|
||||
- (void)getNotificationRegistrationAsync:(nonnull void (^)(MCDNotificationRegistration* _Nullable, NSError* _Nullable))completionBlock
|
||||
{
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ completionBlock(_notificationRegistration, nil); });
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
if (self = [super init])
|
||||
{
|
||||
@try
|
||||
{
|
||||
_registrationUpdated = [MCDRegistrationUpdatedEvent new];
|
||||
}
|
||||
@catch(NSException* e) {
|
||||
NSLog(@"Failed to create new MCDRegistrationUpdatedEvent with exception %@", e.description);
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)_updateNotificationRegistration:(MCDNotificationRegistration*)notificationRegistration
|
||||
{
|
||||
_notificationRegistration = notificationRegistration;
|
||||
@try
|
||||
{
|
||||
[_registrationUpdated raiseWithNotificationRegistration:notificationRegistration];
|
||||
}
|
||||
@catch(NSException* e) {
|
||||
NSLog(@"Failed to update notification registration with exception %@", e.description);
|
||||
}
|
||||
}
|
||||
@end
|
|
@ -2,21 +2,25 @@
|
|||
#pragma once
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <ConnectedDevices/Core/Core.h>
|
||||
#import <ConnectedDevices/UserData/UserData.h>
|
||||
#import <ConnectedDevices/UserData.UserNotifications/UserData.UserNotifications.h>
|
||||
#import "AADMSAAccountProvider.h"
|
||||
#import <ConnectedDevices/ConnectedDevices.h>
|
||||
#import <ConnectedDevices/ConnectedDevicesUserData/ConnectedDevicesUserData.h>
|
||||
#import <ConnectedDevices/ConnectedDevicesUserDataUserNotifications/ConnectedDevicesUserDataUserNotifications.h>
|
||||
#import "AADAccount.h"
|
||||
#import "MSAAccount.h"
|
||||
|
||||
@interface NotificationsManager : NSObject
|
||||
+ (instancetype)startWithAccountProvider:(AADMSAAccountProvider*)accountProvider platform:(MCDPlatform*)platform;
|
||||
+ (instancetype)startWithPlatform:(MCDConnectedDevicesPlatform*)platform;
|
||||
|
||||
+ (instancetype)sharedInstance;
|
||||
|
||||
@property (nonatomic, readonly) AADMSAAccountProvider* accountProvider;
|
||||
@property (nonatomic, readonly) NSArray<MCDUserNotification*>* notifications;
|
||||
@property (nonatomic) MCDConnectedDevicesAccount* account;
|
||||
|
||||
- (NSInteger)addNotificationsChangedListener:(void(^)(void))listener;
|
||||
- (void)removeListener:(NSInteger)token;
|
||||
- (void)forceRead;
|
||||
- (void)dismissNotificationAtIndex:(NSUInteger)index;
|
||||
- (void)readNotificationAtIndex:(NSUInteger)index;
|
||||
- (void)refresh;
|
||||
- (void)dismissNotification:(MCDUserNotification*)notification;
|
||||
- (void)dismissNotificationWithId:(NSString*)notificationId;
|
||||
- (void)readNotification:(MCDUserNotification*)notification;
|
||||
@end
|
||||
|
|
|
@ -1,138 +1,312 @@
|
|||
#import "NotificationsManager.h"
|
||||
#import "Secrets.h"
|
||||
#import "AppDelegate.h"
|
||||
#import <UserNotifications/UserNotifications.h>
|
||||
|
||||
|
||||
static NotificationsManager* s_manager;
|
||||
|
||||
@interface NotificationsManager () {
|
||||
@interface NotificationsManager ()
|
||||
{
|
||||
NSInteger _listenerValue;
|
||||
MCDEventSubscription* _readerSubscription;
|
||||
NSMutableArray<MCDUserNotification*>* _notifications;
|
||||
}
|
||||
|
||||
|
||||
- (instancetype)initWithPlatform:(MCDConnectedDevicesPlatform*)platform;
|
||||
|
||||
@property (nonatomic) NSMutableDictionary<NSNumber*, void(^)(void)>* listenerMap;
|
||||
@property (nonatomic) MCDPlatform* platform;
|
||||
@property (nonatomic) MCDConnectedDevicesPlatform* platform;
|
||||
@property (nonatomic) MCDUserNotificationChannel* channel;
|
||||
@property (nonatomic) MCDUserNotificationReader* reader;
|
||||
- (instancetype)initWithAccountProvider:(id<MCDUserAccountProvider>)accountProvider platform:(MCDPlatform*)platform;
|
||||
@property (nonatomic) BOOL platformStarted;
|
||||
@end
|
||||
|
||||
@implementation NotificationsManager
|
||||
- (instancetype)initWithAccountProvider:(AADMSAAccountProvider*)accountProvider platform:(MCDPlatform*)platform {
|
||||
if (self = [super init]) {
|
||||
- (instancetype)initWithPlatform:(MCDConnectedDevicesPlatform*)platform
|
||||
{
|
||||
if (self = [super init])
|
||||
{
|
||||
_notifications = [NSMutableArray array];
|
||||
_listenerValue = 0;
|
||||
_listenerMap = [NSMutableDictionary dictionary];
|
||||
_accountProvider = accountProvider;
|
||||
_platform = platform;
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[_accountProvider.userAccountChanged subscribe:^ {
|
||||
if ([weakSelf.accountProvider getUserAccounts].count > 0) {
|
||||
[weakSelf setupWithAccount:[weakSelf.accountProvider getUserAccounts][0]];
|
||||
} else {
|
||||
for (void (^listener)(void) in weakSelf.listenerMap.allValues) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
}];
|
||||
_platformStarted = NO;
|
||||
}
|
||||
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setupWithAccount:(MCDUserAccount*)account {
|
||||
@synchronized (self) {
|
||||
MCDUserDataFeed* dataFeed = [MCDUserDataFeed getForAccount:account platform:_platform activitySourceHost:APP_HOST_NAME];
|
||||
self.channel = [MCDUserNotificationChannel channelWithUserDataFeed:dataFeed];
|
||||
self.reader = [self.channel createReader];
|
||||
|
||||
__weak typeof(self) weakSelf;
|
||||
_readerSubscription = [self.reader.dataChanged subscribe:^(__unused MCDUserNotificationReader* source, __unused MCDUserNotificationReaderDataChangedEventArgs* args) {
|
||||
[weakSelf forceRead];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)setAccount:(MCDConnectedDevicesAccount*)account
|
||||
{
|
||||
@synchronized (self)
|
||||
{
|
||||
if (account)
|
||||
{
|
||||
NSLog(@"GraphNotifications Updating NotificationsManager with account %@", account.accountId);
|
||||
AppDelegate* delegate = (AppDelegate*)([UIApplication sharedApplication].delegate);
|
||||
|
||||
if (!self.platformStarted)
|
||||
{
|
||||
NSLog(@"GraphNotifications Starting Platform!");
|
||||
[delegate startPlatform];
|
||||
self.platformStarted = YES;
|
||||
}
|
||||
|
||||
NSLog(@"GraphNotifications Adding account to platform");
|
||||
[self.platform.accountManager addAccountAsync:account callback:^(__unused MCDConnectedDevicesAddAccountResult* result, __unused NSError* error)
|
||||
{
|
||||
// Don't use `setAccount:` here or we'll end up in an infinite loop :(
|
||||
self->_account = account;
|
||||
NSLog(@"GraphNotifications Registering notifications for account %@", account.accountId);
|
||||
[delegate registerNotificationsForAccount:account callback:^(__unused BOOL result, __unused NSError* error)
|
||||
{
|
||||
NSLog(@"GraphNotifications Initializing UserDataFeed!");
|
||||
MCDUserDataFeed* dataFeed;
|
||||
@try
|
||||
{
|
||||
dataFeed = [MCDUserDataFeed getForAccount:account platform:self.platform activitySourceHost:APP_HOST_NAME];
|
||||
}
|
||||
@catch (NSException* e)
|
||||
{
|
||||
NSLog(@"GraphNotifications Failed to initialize UserDataFeed with %@", e.description);
|
||||
return;
|
||||
}
|
||||
|
||||
NSLog(@"GraphNotifications Susbcribing for sync scopes async!");
|
||||
[dataFeed subscribeToSyncScopesAsync:@[[MCDUserNotificationChannel syncScope]] callback:^(__unused BOOL result, __unused NSError * error)
|
||||
{
|
||||
@synchronized (self)
|
||||
{
|
||||
NSLog(@"GraphNotifications Initializing channel and reader");
|
||||
[dataFeed startSync];
|
||||
self.channel = [MCDUserNotificationChannel channelWithUserDataFeed:dataFeed];
|
||||
self.reader = [self.channel createReader];
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
_readerSubscription = [self.reader.dataChanged subscribe:^(__unused MCDUserNotificationReader* source, __unused MCDUserNotificationReaderDataChangedEventArgs* args)
|
||||
{
|
||||
NSLog(@"GraphNotifications Got an update!");
|
||||
[weakSelf forceRead];
|
||||
}];
|
||||
|
||||
[self forceRead];
|
||||
}
|
||||
}];
|
||||
}];
|
||||
}];
|
||||
} else if (self.platformStarted)
|
||||
{
|
||||
[self.platform.accountManager removeAccountAsync:account callback:^(__unused MCDConnectedDevicesRemoveAccountResult* result, NSError* error)
|
||||
{
|
||||
@synchronized (self)
|
||||
{
|
||||
if (error)
|
||||
{
|
||||
NSLog(@"GraphNotifications failed to remove account from platform with error %@", error.description);
|
||||
}
|
||||
|
||||
// Don't use `setAccount:` here or we'll end up in an infinite loop :(
|
||||
self->_account = nil;
|
||||
self.channel = nil;
|
||||
self.reader = nil;
|
||||
}
|
||||
}];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
- (void)forceRead {
|
||||
[self.reader readBatchAsyncWithMaxSize:0 completion:^(NSArray<MCDUserNotification *> * _Nullable notifications, NSError * _Nullable error) {
|
||||
if (error) {
|
||||
NSLog(@"Failed to read batch with error %@", error);
|
||||
} else {
|
||||
- (void)forceRead
|
||||
{
|
||||
[self.reader readBatchAsyncWithMaxSize:NSUIntegerMax completion:^(NSArray<MCDUserNotification *> * _Nullable notifications, NSError * _Nullable error)
|
||||
{
|
||||
if (error)
|
||||
{
|
||||
NSLog(@"GraphNotifications Failed to read batch with error %@", error);
|
||||
}
|
||||
else
|
||||
{
|
||||
NSLog(@"GraphNotifications NotificationsManager got %ld notifications", notifications.count);
|
||||
[self _handleNotifications:notifications];
|
||||
for (void (^listener)(void) in self.listenerMap.allValues) {
|
||||
for (void (^listener)(void) in self.listenerMap.allValues)
|
||||
{
|
||||
listener();
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)readNotificationAtIndex:(NSUInteger)index {
|
||||
@synchronized (self) {
|
||||
self.notifications[index].readState = MCDUserNotificationReadStateRead;
|
||||
[self.notifications[index] saveAsync:^(__unused MCDUserNotificationUpdateResult * _Nullable result, __unused NSError * _Nullable err) {
|
||||
// Do Nothing
|
||||
- (void)readNotification:(MCDUserNotification*)notification
|
||||
{
|
||||
@synchronized (self)
|
||||
{
|
||||
notification.readState = MCDUserNotificationReadStateRead;
|
||||
[notification saveAsync:^(__unused MCDUserNotificationUpdateResult * _Nullable result, __unused NSError * _Nullable err)
|
||||
{
|
||||
NSLog(@"GraphNotifications Read notification with result %d error %@", result.succeeded, err);
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dismissNotificationAtIndex:(NSUInteger)index {
|
||||
@synchronized (self) {
|
||||
self.notifications[index].userActionState = MCDUserNotificationUserActionStateDismissed;
|
||||
[self.notifications[index] saveAsync:^(__unused MCDUserNotificationUpdateResult * _Nullable result, __unused NSError * _Nullable err) {
|
||||
// Do Nothing
|
||||
}];
|
||||
}
|
||||
- (void)dismissNotificationFromTrayWithId:(NSString *)notificationId
|
||||
{
|
||||
[[UNUserNotificationCenter currentNotificationCenter] removePendingNotificationRequestsWithIdentifiers:@[notificationId]];
|
||||
[[UNUserNotificationCenter currentNotificationCenter] removeDeliveredNotificationsWithIdentifiers:@[notificationId]];
|
||||
}
|
||||
|
||||
+ (instancetype)startWithAccountProvider:(AADMSAAccountProvider*)accountProvider platform:(MCDPlatform*)platform {
|
||||
@synchronized (self) {
|
||||
if (s_manager == nil) {
|
||||
s_manager = [[self alloc] initWithAccountProvider:accountProvider platform:platform];
|
||||
- (void)dismissNotificationWithId:(NSString *)notificationId
|
||||
{
|
||||
@synchronized (self)
|
||||
{
|
||||
for (MCDUserNotification* notification in self.notifications)
|
||||
{
|
||||
if ([notification.notificationId isEqualToString:notificationId])
|
||||
{
|
||||
[self dismissNotification:notification];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dismissNotification:(MCDUserNotification*)notification
|
||||
{
|
||||
@synchronized (self)
|
||||
{
|
||||
[self dismissNotificationFromTrayWithId:notification.notificationId];
|
||||
notification.userActionState = MCDUserNotificationUserActionStateActivated;
|
||||
[notification saveAsync:^(__unused MCDUserNotificationUpdateResult * _Nullable result, __unused NSError * _Nullable err)
|
||||
{
|
||||
NSLog(@"GraphNotifications Dismiss notification with result %d error %@", result.succeeded, err);
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
+ (instancetype)startWithPlatform:(MCDConnectedDevicesPlatform*)platform
|
||||
{
|
||||
@synchronized (self)
|
||||
{
|
||||
if (s_manager == nil)
|
||||
{
|
||||
s_manager = [[self alloc] initWithPlatform:platform];
|
||||
}
|
||||
|
||||
return s_manager;
|
||||
}
|
||||
}
|
||||
|
||||
+ (instancetype)sharedInstance {
|
||||
@synchronized (self) {
|
||||
+ (instancetype)sharedInstance
|
||||
{
|
||||
@synchronized (self)
|
||||
{
|
||||
return s_manager;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSInteger)addNotificationsChangedListener:(void(^)(void))listener {
|
||||
@synchronized (self) {
|
||||
- (NSInteger)addNotificationsChangedListener:(void(^)(void))listener
|
||||
{
|
||||
@synchronized (self)
|
||||
{
|
||||
_listenerMap[[NSNumber numberWithInteger:(++_listenerValue)]] = listener;
|
||||
return _listenerValue;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)removeListener:(NSInteger)token {
|
||||
@synchronized (self) {
|
||||
- (void)removeListener:(NSInteger)token
|
||||
{
|
||||
@synchronized (self)
|
||||
{
|
||||
[_listenerMap removeObjectForKey:[NSNumber numberWithInteger:token]];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray<MCDUserNotification*>*)notifications {
|
||||
- (NSArray<MCDUserNotification*>*)notifications
|
||||
{
|
||||
return _notifications;
|
||||
}
|
||||
|
||||
- (void)_handleNotifications:(NSArray<MCDUserNotification*>*)notifications {
|
||||
@synchronized (self) {
|
||||
for (MCDUserNotification* notification in notifications) {
|
||||
NSUInteger index = [_notifications indexOfObjectPassingTest:^BOOL(MCDUserNotification * _Nonnull obj, __unused NSUInteger idx, __unused BOOL * _Nonnull stop) {
|
||||
return [obj.notificationId isEqualToString:notification.notificationId];
|
||||
}];
|
||||
|
||||
if (index != NSNotFound) {
|
||||
[_notifications removeObjectAtIndex:index];
|
||||
- (void)_handleNotifications:(NSArray<MCDUserNotification*>*)notifications
|
||||
{
|
||||
@synchronized (self)
|
||||
{
|
||||
NSLog(@"GraphNotifications Got %ld notifications!", notifications.count);
|
||||
for (MCDUserNotification* notification in notifications)
|
||||
{
|
||||
for (NSUInteger i = 0; i < _notifications.count; ++i)
|
||||
{
|
||||
if ([_notifications[i].notificationId isEqualToString:notification.notificationId])
|
||||
{
|
||||
NSLog(@"GraphNotifications Found a match for %@", notification.notificationId);
|
||||
[_notifications removeObjectAtIndex:i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (notification.status == MCDUserNotificationStatusActive) {
|
||||
|
||||
if (notification.status == MCDUserNotificationStatusActive)
|
||||
{
|
||||
NSLog(@"GraphNotifications Notification is active %@", notification.notificationId);
|
||||
[_notifications insertObject:notification atIndex:0];
|
||||
|
||||
if ((notification.userActionState == MCDUserNotificationUserActionStateNoInteraction) && (notification.readState == MCDUserNotificationReadStateUnread))
|
||||
{
|
||||
UNMutableNotificationContent* content = [UNMutableNotificationContent new];
|
||||
content.title = @"New MSGraph Notification";
|
||||
content.body = notification.content;
|
||||
UNTimeIntervalNotificationTrigger* trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:1 repeats:NO];
|
||||
UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:notification.notificationId content:content trigger:trigger];
|
||||
|
||||
[[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error)
|
||||
{
|
||||
if (error)
|
||||
{
|
||||
NSLog(@"GraphNotifications Failed to post local notification with error %@", error);
|
||||
}
|
||||
else
|
||||
{
|
||||
NSLog(@"GraphNotifications Successfully posted local notification request");
|
||||
}
|
||||
}];
|
||||
}
|
||||
else
|
||||
{
|
||||
[self dismissNotificationFromTrayWithId:notification.notificationId];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
NSLog(@"GraphNotifications Notification is deleted %@", notification.notificationId);
|
||||
[self dismissNotificationFromTrayWithId:notification.notificationId];
|
||||
}
|
||||
}
|
||||
|
||||
NSLog(@"GraphNotifications NotificationsManager now has %ld notifications", _notifications.count);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_clearNotifications
|
||||
{
|
||||
@synchronized (self)
|
||||
{
|
||||
[_notifications removeAllObjects];
|
||||
[[UNUserNotificationCenter currentNotificationCenter] removeAllPendingNotificationRequests];
|
||||
[[UNUserNotificationCenter currentNotificationCenter] removeAllDeliveredNotifications];
|
||||
for (void (^listener)(void) in self.listenerMap.allValues)
|
||||
{
|
||||
listener();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)refresh
|
||||
{
|
||||
NSLog(@"GraphNotifications Refreshing NotificationsManager");
|
||||
@synchronized (self)
|
||||
{
|
||||
[self _clearNotifications];
|
||||
|
||||
// Starts setup based on new account, which since this is the current account will just refresh all of the actual setup
|
||||
[self setAccount:self.account];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -4,4 +4,5 @@
|
|||
#import "NotificationsManager.h"
|
||||
|
||||
@interface NotificationsViewController : UITableViewController
|
||||
- (IBAction)refresh;
|
||||
@end
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "NotificationsViewController.h"
|
||||
#import "AdaptiveCards/ACFramework.h"
|
||||
|
||||
|
||||
@interface NotificationsViewController() {
|
||||
@interface NotificationsViewController()
|
||||
{
|
||||
}
|
||||
|
||||
@property (nonatomic) NSMutableArray<MCDUserNotification*>* notifications;
|
||||
- (void)updateData;
|
||||
- (void)handleTap:(UITapGestureRecognizer*)recognizer;
|
||||
|
@ -13,19 +14,28 @@
|
|||
|
||||
@implementation NotificationsViewController
|
||||
|
||||
- (void)viewDidLoad {
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
_notifications = [NSMutableArray array];
|
||||
// Do any additional setup after loading the view, typically from a nib.
|
||||
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[[NotificationsManager sharedInstance] addNotificationsChangedListener:^{
|
||||
[self updateData];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[[NotificationsManager sharedInstance] addNotificationsChangedListener:
|
||||
^{
|
||||
NSLog(@"GraphNotifications NotificationsViewController notified of changes!");
|
||||
dispatch_async(dispatch_get_main_queue(),
|
||||
^{
|
||||
[weakSelf updateData];
|
||||
[weakSelf.tableView reloadData];
|
||||
});
|
||||
}];
|
||||
|
||||
[self updateData];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(),
|
||||
^{
|
||||
[self updateData];
|
||||
[self.tableView reloadData];
|
||||
});
|
||||
}
|
||||
|
||||
- (NSInteger)numberOfSectionsInTableView:(__unused UITableView*)tableView
|
||||
|
@ -33,16 +43,36 @@
|
|||
return 1;
|
||||
}
|
||||
|
||||
- (nonnull UITableViewCell*)tableView:(nonnull UITableView*)tableView cellForRowAtIndexPath:(nonnull __unused NSIndexPath*)indexPath
|
||||
- (nonnull UITableViewCell*)tableView:(nonnull UITableView*)tableView cellForRowAtIndexPath:(nonnull NSIndexPath*)indexPath
|
||||
{
|
||||
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:@"NotificationCell"];
|
||||
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:@"NotificationCell" forIndexPath:indexPath];
|
||||
|
||||
MCDUserNotification* notification = self.notifications[indexPath.row];
|
||||
|
||||
ACOAdaptiveCardParseResult* parseResult = [ACOAdaptiveCard fromJson:notification.content];
|
||||
ACRRenderResult* renderResult = [ACRRenderer render:parseResult.card config:[ACOHostConfig new] widthConstraint:0.0f];
|
||||
|
||||
[cell.contentView addSubview:renderResult.view];
|
||||
[cell addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)]];
|
||||
|
||||
UILabel* idLabel = (UILabel*)[cell viewWithTag:1];
|
||||
[idLabel setText:notification.notificationId];
|
||||
|
||||
UILabel* contentLabel = (UILabel*)[cell viewWithTag:2];
|
||||
[contentLabel setText:notification.content];
|
||||
|
||||
UILabel* userActionStateLabel = (UILabel*)[cell viewWithTag:3];
|
||||
[userActionStateLabel setText:((notification.userActionState == MCDUserNotificationUserActionStateNoInteraction) ? @"No Interaction" : @"Activated")];
|
||||
|
||||
UIButton* readButton = (UIButton*)[cell viewWithTag:4];
|
||||
if (notification.readState == MCDUserNotificationReadStateUnread)
|
||||
{
|
||||
readButton.enabled = YES;
|
||||
[readButton addTarget:self action:@selector(handleRead:) forControlEvents:UIControlEventTouchDown];
|
||||
}
|
||||
else
|
||||
{
|
||||
readButton.enabled = NO;
|
||||
}
|
||||
|
||||
if (notification.userActionState == MCDUserNotificationUserActionStateNoInteraction)
|
||||
{
|
||||
[cell addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)]];
|
||||
}
|
||||
return cell;
|
||||
}
|
||||
|
||||
|
@ -51,19 +81,28 @@
|
|||
return self.notifications.count;
|
||||
}
|
||||
|
||||
- (void)handleTap:(UITapGestureRecognizer *)recognizer {
|
||||
[[NotificationsManager sharedInstance] dismissNotificationAtIndex:[self.tableView indexPathForCell:(UITableViewCell*)recognizer.view].row];
|
||||
- (void)handleTap:(UITapGestureRecognizer *)recognizer
|
||||
{
|
||||
[[NotificationsManager sharedInstance] dismissNotification:(self.notifications[[self.tableView indexPathForCell:(UITableViewCell*)recognizer.view].row])];
|
||||
}
|
||||
|
||||
- (void)updateData {
|
||||
@synchronized (self) {
|
||||
- (void)updateData
|
||||
{
|
||||
@synchronized (self)
|
||||
{
|
||||
[self.notifications removeAllObjects];
|
||||
for (MCDUserNotification* notification in [NotificationsManager sharedInstance].notifications) {
|
||||
if (notification.status == MCDUserNotificationStatusActive && notification.readState == MCDUserNotificationReadStateUnread && notification.userActionState == MCDUserNotificationUserActionStateNoInteraction) {
|
||||
[self.notifications addObject:notification];
|
||||
}
|
||||
}
|
||||
[self.notifications addObjectsFromArray:[NotificationsManager sharedInstance].notifications];
|
||||
NSLog(@"GraphNotifications Got %ld valid objects for NotificationsViewController out of %ld", self.notifications.count, [NotificationsManager sharedInstance].notifications.count);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleRead:(id)button
|
||||
{
|
||||
[[NotificationsManager sharedInstance] readNotification:(self.notifications[[self.tableView indexPathForCell:(UITableViewCell*)((UIView*)button).superview].row])];
|
||||
}
|
||||
|
||||
- (IBAction)refresh
|
||||
{
|
||||
[[NotificationsManager sharedInstance] refresh];
|
||||
}
|
||||
@end
|
||||
|
|
|
@ -1,16 +1,8 @@
|
|||
//
|
||||
// RootViewController.h
|
||||
// GraphNotifications
|
||||
//
|
||||
// Created by Allen Ballway on 8/23/18.
|
||||
// Copyright © 2018 Microsoft. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface RootViewController : UIViewController <UIPageViewControllerDelegate>
|
||||
@interface RootViewController : UIViewController
|
||||
|
||||
@property (strong, nonatomic) UIPageViewController *pageViewController;
|
||||
@property (strong, nonatomic) UITabBarController *tabBarController;
|
||||
|
||||
@end
|
||||
|
||||
|
|
|
@ -7,104 +7,46 @@
|
|||
//
|
||||
|
||||
#import "RootViewController.h"
|
||||
#import "ModelController.h"
|
||||
#import "LoginViewController.h"
|
||||
#import "NotificationsManager.h"
|
||||
#import "NotificationProvider.h"
|
||||
#import "Secrets.h"
|
||||
#import "NotificationsViewController.h"
|
||||
|
||||
|
||||
@interface RootViewController ()
|
||||
|
||||
@property (readonly, strong, nonatomic) ModelController *modelController;
|
||||
@end
|
||||
|
||||
@implementation RootViewController
|
||||
|
||||
@synthesize modelController = _modelController;
|
||||
|
||||
- (void)viewDidLoad {
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
AADMSAAccountProvider* accountProvider = [[AADMSAAccountProvider alloc] initWithMsaClientId:MSA_CLIENT_ID msaScopeOverrides:@{@"https://activity.windows.com/UserActivity.ReadWrite.CreatedByApp" : @[@"https://activity.windows.com/UserActivity.ReadWrite.CreatedByApp", @"https://activity.windows.com/UserNotification.ReadWrite.CreatedByApp"]} aadApplicationId:AAD_CLIENT_ID aadRedirectUri:AAD_REDIRECT_URI];
|
||||
|
||||
NotificationProvider* notificationProvider = [NotificationProvider sharedInstance];
|
||||
|
||||
MCDPlatform* platform = [MCDPlatform platformWithAccountProvider:accountProvider notificationProvider:notificationProvider];
|
||||
|
||||
[NotificationsManager startWithAccountProvider:accountProvider platform:platform];
|
||||
|
||||
// Do any additional setup after loading the view, typically from a nib.
|
||||
// Configure the page view controller and add it as a child view controller.
|
||||
self.pageViewController = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStylePageCurl navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal options:nil];
|
||||
self.pageViewController.delegate = self;
|
||||
|
||||
UIViewController *startingViewController = [self.modelController viewControllerAtIndex:0 storyboard:self.storyboard];
|
||||
NSArray *viewControllers = @[startingViewController];
|
||||
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];
|
||||
LoginViewController* loginViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"LoginViewController"];
|
||||
NotificationsViewController* notificationsViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"NotificationsViewController"];
|
||||
|
||||
self.pageViewController.dataSource = self.modelController;
|
||||
UITabBarItem* loginItem = [[UITabBarItem alloc] initWithTitle:@"Login" image:nil tag:1];
|
||||
UITabBarItem* notificationsItem = [[UITabBarItem alloc] initWithTitle:@"Notifications" image:nil tag:2];
|
||||
|
||||
[self addChildViewController:self.pageViewController];
|
||||
[self.view addSubview:self.pageViewController.view];
|
||||
loginViewController.tabBarItem = loginItem;
|
||||
notificationsViewController.tabBarItem = notificationsItem;
|
||||
|
||||
// Set the page view controller's bounds using an inset rect so that self's view is visible around the edges of the pages.
|
||||
CGRect pageViewRect = self.view.bounds;
|
||||
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
|
||||
pageViewRect = CGRectInset(pageViewRect, 40.0, 40.0);
|
||||
}
|
||||
self.pageViewController.view.frame = pageViewRect;
|
||||
|
||||
[self.pageViewController didMoveToParentViewController:self];
|
||||
self.tabBarController = [UITabBarController new];
|
||||
self.tabBarController.viewControllers = @[loginViewController, notificationsViewController];
|
||||
self.tabBarController.selectedIndex = 0;
|
||||
|
||||
[self addChildViewController:self.tabBarController];
|
||||
[self.view addSubview:self.tabBarController.view];
|
||||
|
||||
}
|
||||
|
||||
|
||||
- (void)didReceiveMemoryWarning {
|
||||
- (void)didReceiveMemoryWarning
|
||||
{
|
||||
[super didReceiveMemoryWarning];
|
||||
// Dispose of any resources that can be recreated.
|
||||
}
|
||||
|
||||
|
||||
- (ModelController *)modelController {
|
||||
// Return the model controller object, creating it if necessary.
|
||||
// In more complex implementations, the model controller may be passed to the view controller.
|
||||
if (!_modelController) {
|
||||
_modelController = [[ModelController alloc] init];
|
||||
}
|
||||
return _modelController;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - UIPageViewController delegate methods
|
||||
|
||||
- (UIPageViewControllerSpineLocation)pageViewController:(UIPageViewController *)pageViewController spineLocationForInterfaceOrientation:(UIInterfaceOrientation)orientation {
|
||||
if (UIInterfaceOrientationIsPortrait(orientation) || ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone)) {
|
||||
// In portrait orientation or on iPhone: Set the spine position to "min" and the page view controller's view controllers array to contain just one view controller. Setting the spine position to 'UIPageViewControllerSpineLocationMid' in landscape orientation sets the doubleSided property to YES, so set it to NO here.
|
||||
|
||||
UIViewController *currentViewController = self.pageViewController.viewControllers[0];
|
||||
NSArray *viewControllers = @[currentViewController];
|
||||
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:nil];
|
||||
|
||||
self.pageViewController.doubleSided = NO;
|
||||
return UIPageViewControllerSpineLocationMin;
|
||||
}
|
||||
|
||||
// In landscape orientation: Set set the spine location to "mid" and the page view controller's view controllers array to contain two view controllers. If the current page is even, set it to contain the current and next view controllers; if it is odd, set the array to contain the previous and current view controllers.
|
||||
LoginViewController *currentViewController = self.pageViewController.viewControllers[0];
|
||||
NSArray *viewControllers = nil;
|
||||
|
||||
NSUInteger indexOfCurrentViewController = [self.modelController indexOfViewController:currentViewController];
|
||||
if (indexOfCurrentViewController == 0 || indexOfCurrentViewController % 2 == 0) {
|
||||
UIViewController *nextViewController = [self.modelController pageViewController:self.pageViewController viewControllerAfterViewController:currentViewController];
|
||||
viewControllers = @[currentViewController, nextViewController];
|
||||
} else {
|
||||
UIViewController *previousViewController = [self.modelController pageViewController:self.pageViewController viewControllerBeforeViewController:currentViewController];
|
||||
viewControllers = @[previousViewController, currentViewController];
|
||||
}
|
||||
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:nil];
|
||||
|
||||
|
||||
return UIPageViewControllerSpineLocationMid;
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
// These come from the converged app registration portal at apps.dev.microsoft.com
|
||||
// MSA_CLIENT_ID: Id of this app's registration in the MSA portal
|
||||
// AAD_CLIENT_ID: Id of this app's registration in the Azure portal
|
||||
|
@ -11,7 +13,7 @@
|
|||
// AAD is supposed to use this Uri to call the app back after login (currently not true, external requirement)
|
||||
// And this app is supposed to be able to handle this Uri (currently not true)
|
||||
// APP_HOST_NAME Cross-device domain of this app's registration
|
||||
static NSString* const MSA_CLIENT_ID = "<<MSA client ID goes here>>";
|
||||
static NSString* const AAD_CLIENT_ID = "<<AAD client ID goes here>>";
|
||||
static NSString* const AAD_REDIRECT_URI = "<<AAD redirect URI goes here>>";
|
||||
static NSString* const APP_HOST_NAME = "<<App cross-device domain goes here>>";
|
||||
static NSString* const MSA_CLIENT_ID = @"<<MSA client ID goes here>>";
|
||||
static NSString* const AAD_CLIENT_ID = @"<<AAD client ID goes here>>";
|
||||
static NSString* const AAD_REDIRECT_URI = @"<<AAD redirect URI goes here>>";
|
||||
static NSString* const APP_HOST_NAME = @"<<App cross-device domain goes here>>";
|
|
@ -0,0 +1,23 @@
|
|||
//
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "SignInAccount.h"
|
||||
#import <ADAL/ADAL.h>
|
||||
#import <ConnectedDevices/ConnectedDevices/ConnectedDevices.h>
|
||||
#import "SignInAccount.h"
|
||||
|
||||
@interface AADAccount : NSObject <SignInAccount>
|
||||
|
||||
- (nullable instancetype)initWithClientId:(nonnull NSString*)clientId redirectUri:(nonnull NSURL*)redirectUri;
|
||||
- (void)signInWithCompletionCallback:(nonnull void (^)(MCDConnectedDevicesAccount* _Nonnull, NSError* _Nullable))callback;
|
||||
- (void)signOutWithCompletionCallback:(nonnull void (^)(MCDConnectedDevicesAccount* _Nonnull, NSError* _Nullable))callback;
|
||||
|
||||
@property(readonly, nonatomic, copy, nonnull) NSString* clientId;
|
||||
@property(readonly, nonatomic, copy, nonnull) NSURL* redirectUri;
|
||||
@property(readonly) BOOL isSignedIn;
|
||||
@property(readonly, nonatomic, nonnull) MCDConnectedDevicesAccount* mcdAccount;
|
||||
@end
|
|
@ -0,0 +1,306 @@
|
|||
//
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
//
|
||||
|
||||
#import "AADAccount.h"
|
||||
|
||||
#import <ADAL/ADAL.h>
|
||||
|
||||
static NSString* const AADAccountProviderErrorDomain = @"AADAccount";
|
||||
|
||||
/**
|
||||
* Notes about AAD/ADAL:
|
||||
* - Resource An Azure web service/app, such as https://graph.windows.net, or a CDP service.
|
||||
* - Scope Individual permissions within a resource
|
||||
* - Access Token A standard JSON web token for a given scope.
|
||||
* This is the actual token/user ticket used to authenticate with CDP services.
|
||||
* https://oauth.net/2/
|
||||
* https://www.oauth.com/oauth2-servers/access-tokens/
|
||||
* - Refresh token: A standard OAuth refresh token.
|
||||
* Lasts longer than access tokens, and is used to request new access tokens/refresh access tokens when they expire.
|
||||
* ADAL manages this automatically.
|
||||
* https://oauth.net/2/grant-types/refresh-token/
|
||||
* - MRRT Multiresource refresh token. A refresh token that can be used to fetch access tokens for more than one resource.
|
||||
* Getting one requires the user consent to all the covered resources. ADAL manages this automatically.
|
||||
*/
|
||||
@interface AADAccount ()
|
||||
{
|
||||
ADAuthenticationContext* _authContext;
|
||||
ADTokenCacheItem* _tokenCacheItem;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation AADAccount
|
||||
|
||||
@synthesize isSignedIn;
|
||||
@synthesize mcdAccount;
|
||||
|
||||
- (nullable instancetype)initWithClientId:(nonnull NSString*)clientId redirectUri:(nonnull NSURL*)redirectUri
|
||||
{
|
||||
if (self = [super init])
|
||||
{
|
||||
_clientId = [clientId copy];
|
||||
_redirectUri = [redirectUri copy];
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
// Don't share token cache between applications, only need them to be cached for this application
|
||||
// Without this, the MRRT is not cached, and the acquireTokenSilentWithResource: in getAccessToken
|
||||
// always fails with AD_ERROR_SERVER_USER_INPUT_NEEDED
|
||||
[[ADAuthenticationSettings sharedInstance] setDefaultKeychainGroup:nil];
|
||||
#endif
|
||||
|
||||
ADAuthenticationError* error = nil;
|
||||
_authContext =
|
||||
[ADAuthenticationContext authenticationContextWithAuthority:@"https://login.microsoftonline.com/common" error:&error];
|
||||
if (error)
|
||||
{
|
||||
NSLog(@"Error creating ADAuthenticationContext for AADAccountProvider: %@.", error);
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSLog(@"Checking if previous AADAccountProvider session can be loaded...");
|
||||
#if TARGET_OS_IPHONE
|
||||
NSArray<ADTokenCacheItem*>* tokenCacheItems = [[ADKeychainTokenCache defaultKeychainCache] allItems:nil];
|
||||
#else
|
||||
NSArray<ADTokenCacheItem*>* tokenCacheItems = [[ADTokenCache defaultCache] allItems:nil];
|
||||
#endif
|
||||
if (tokenCacheItems.count > 0)
|
||||
{
|
||||
for (ADTokenCacheItem* item in tokenCacheItems)
|
||||
{
|
||||
if (item.isMultiResourceRefreshToken && [_clientId isEqualToString:item.clientId])
|
||||
{
|
||||
_tokenCacheItem = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (_tokenCacheItem)
|
||||
{
|
||||
NSLog(@"Loaded previous AADAccountProvider session, starting as signed in.");
|
||||
}
|
||||
else
|
||||
{
|
||||
NSLog(@"No previous AADAccountProvider session could be loaded, starting as signed out.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)isSignedIn
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
isSignedIn = _tokenCacheItem != nil;
|
||||
return isSignedIn;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)signInWithCompletionCallback:(void (^)(MCDConnectedDevicesAccount*, NSError*))callback
|
||||
{
|
||||
if (self.isSignedIn)
|
||||
{
|
||||
callback(
|
||||
NO, [NSError errorWithDomain:AADAccountProviderErrorDomain code:SampleAccountActionFailureReasonAlreadySignedIn userInfo:nil]);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user has not previously consented for this default resource for this app,
|
||||
// the interactive flow will ask for user consent for all resources used by the app.
|
||||
// If the user previously consented to this resource on this app, and more resources are added to the app later on,
|
||||
// a consent prompt for all app resources will be raised when an access token for a new resource is requested -
|
||||
// see getAccessTokenForUserAccountIdAsync:
|
||||
NSString* defaultResource = @"https://graph.windows.net";
|
||||
|
||||
[_authContext
|
||||
acquireTokenWithResource:defaultResource
|
||||
clientId:_clientId
|
||||
redirectUri:_redirectUri
|
||||
completionBlock:^(ADAuthenticationResult* result) {
|
||||
switch (result.status)
|
||||
{
|
||||
case AD_SUCCEEDED:
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
_tokenCacheItem = result.tokenCacheItem;
|
||||
mcdAccount = [[MCDConnectedDevicesAccount alloc] initWithAccountId:_tokenCacheItem.userInformation.uniqueId
|
||||
type:MCDConnectedDevicesAccountTypeAAD];
|
||||
callback(mcdAccount, nil);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case AD_USER_CANCELLED:
|
||||
{
|
||||
callback(nil, [NSError errorWithDomain:AADAccountProviderErrorDomain
|
||||
code:SampleAccountActionFailureReasonUserCancelled
|
||||
userInfo:nil]);
|
||||
break;
|
||||
}
|
||||
case AD_FAILED:
|
||||
default:
|
||||
{
|
||||
NSString* errorString =
|
||||
[NSString stringWithFormat:@"Error occurred in ADAL when signing in to an AAD account. Status: %u, Error: %@",
|
||||
result.status, result.error];
|
||||
NSLog(@"%@", errorString);
|
||||
callback(nil, [NSError errorWithDomain:AADAccountProviderErrorDomain
|
||||
code:SampleAccountActionFailureReasonADAL
|
||||
userInfo:@{ NSLocalizedDescriptionKey : errorString }]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)signOutWithCompletionCallback:(void (^)(MCDConnectedDevicesAccount*, NSError*))callback
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
if (!self.isSignedIn)
|
||||
{
|
||||
callback(nil,
|
||||
[NSError errorWithDomain:AADAccountProviderErrorDomain code:SampleAccountActionFailureReasonAlreadySignedOut userInfo:nil]);
|
||||
return;
|
||||
}
|
||||
|
||||
ADAuthenticationError* error;
|
||||
#if TARGET_OS_IPHONE
|
||||
BOOL removed = [[ADKeychainTokenCache defaultKeychainCache] removeAllForClientId:_clientId error:&error];
|
||||
#else
|
||||
// The above convenience method does not exist on OSX
|
||||
BOOL removed;
|
||||
NSArray<ADTokenCacheItem*>* tokenCacheItems = [[ADTokenCache defaultCache] allItems:&error];
|
||||
if (!error)
|
||||
{
|
||||
for (ADTokenCacheItem* item in tokenCacheItems)
|
||||
{
|
||||
if ([item.clientId isEqualToString:_clientId])
|
||||
{
|
||||
removed = [[ADTokenCache defaultCache] removeItem:item error:&error];
|
||||
|
||||
if (!removed || error)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!removed || error)
|
||||
{
|
||||
NSString* errorString = [NSString stringWithFormat:@"Failed to remove token from ADAL cache, error %@", error];
|
||||
NSLog(@"%@", errorString);
|
||||
callback(nil, [NSError errorWithDomain:AADAccountProviderErrorDomain
|
||||
code:SampleAccountActionFailureReasonADAL
|
||||
userInfo:@{ NSLocalizedDescriptionKey : errorString }]);
|
||||
return;
|
||||
}
|
||||
|
||||
NSArray<NSString*>* cookieNamesToDelete =
|
||||
@[ @"SignInStateCookie", @"ESTSAUTHPERSISTENT", @"ESTSAUTHLIGHT", @"ESTSAUTH", @"ESTSSC" ];
|
||||
|
||||
NSHTTPCookieStorage* cookieJar = [NSHTTPCookieStorage sharedHTTPCookieStorage];
|
||||
for (NSHTTPCookie* cookie in [cookieJar cookies])
|
||||
{
|
||||
if ([cookieNamesToDelete containsObject:cookie.name])
|
||||
{
|
||||
[cookieJar deleteCookie:cookie];
|
||||
}
|
||||
}
|
||||
|
||||
_tokenCacheItem = nil;
|
||||
}
|
||||
|
||||
callback(mcdAccount, nil);
|
||||
mcdAccount = nil;
|
||||
}
|
||||
|
||||
- (NSError*)_errorFromAdalStatus:(ADAuthenticationResult*)result
|
||||
{
|
||||
switch (result.status)
|
||||
{
|
||||
case AD_SUCCEEDED: { return nil;
|
||||
}
|
||||
case AD_USER_CANCELLED: { return [NSError errorWithDomain:@"AADAccountProvider" code:0 userInfo:@{ @"Reason" : @"Cancelled by user." }];
|
||||
}
|
||||
case AD_FAILED:
|
||||
default: { return [NSError errorWithDomain:@"AADAccountProvider" code:0 userInfo:@{ @"Reason" : @"Unknown ADAL error." }];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)getAccessTokenForUserAccountIdAsync:(NSString*)accountId
|
||||
scopes:(NSArray<NSString*>*)scopes
|
||||
completion:(void (^)(NSString*, NSError*))completionBlock
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
if (!self.isSignedIn || ![accountId isEqualToString:_tokenCacheItem.userInformation.uniqueId])
|
||||
{
|
||||
completionBlock(nil, [NSError errorWithDomain:@"AADAccountProvider"
|
||||
code:0
|
||||
userInfo:@{
|
||||
@"Reason" : @"AADAccountProvider does not provide this account."
|
||||
}]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to fetch the token silently in the background, escalating to the ui thread if needed for a unique case (see below)
|
||||
void (^adalCallback)(ADAuthenticationResult*) = ^void(ADAuthenticationResult* adalResult) {
|
||||
|
||||
if ((adalResult.status == AD_FAILED) && (adalResult.error.code == AD_ERROR_SERVER_USER_INPUT_NEEDED))
|
||||
{
|
||||
// This error only returns from acquireTokenSilentWithResource: when an interactive prompt is needed.
|
||||
// ADAL has an MRRT, but the user has not consented for this resource/the MRRT does not cover this resource.
|
||||
// Usually, users consent for all resources the app needs during the interactive flow in signInWith...:
|
||||
// However, if the app adds new resources after the user consented previously, signIn will not prompt.
|
||||
// Escalate to the UI thread and do an interactive flow,
|
||||
// which should raise a new consent prompt for all current app resources.
|
||||
NSLog(@"A resource was requested that the user did not previously consent to. "
|
||||
@"Attempting to raise an interactive consent prompt.");
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[_authContext
|
||||
acquireTokenWithResource:scopes[0]
|
||||
clientId:_clientId
|
||||
redirectUri:_redirectUri
|
||||
completionBlock:^void(ADAuthenticationResult* adalResult) {
|
||||
// Check if still signed in at this point
|
||||
if (!self.isSignedIn || ![accountId isEqualToString:_tokenCacheItem.userInformation.uniqueId])
|
||||
{
|
||||
completionBlock(nil, [NSError errorWithDomain:@"AADAccountProvider"
|
||||
code:0
|
||||
userInfo:@{
|
||||
@"Reason" : @"Tried to escalate to interactive prompt, "
|
||||
@"but user was signed out in the middle."
|
||||
}]);
|
||||
}
|
||||
else
|
||||
{
|
||||
NSString* accessToken = adalResult.accessToken;
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
|
||||
^{ completionBlock(accessToken, nil); });
|
||||
}
|
||||
}];
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
NSError* error = [self _errorFromAdalStatus:adalResult];
|
||||
dispatch_async(
|
||||
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ completionBlock(adalResult.accessToken, error); });
|
||||
};
|
||||
|
||||
[_authContext acquireTokenSilentWithResource:scopes[0]
|
||||
clientId:_clientId
|
||||
redirectUri:_redirectUri
|
||||
userId:_tokenCacheItem.userInformation.userId
|
||||
completionBlock:adalCallback];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
|
@ -1,25 +0,0 @@
|
|||
//
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <ConnectedDevices/Core/Core.h>
|
||||
|
||||
#import "SingleUserAccountProvider.h"
|
||||
|
||||
// @brief MCDUserAccountProvider that performs a log in/out flow using ADAL.
|
||||
// Supports a single AAD user account.
|
||||
// For getAccessTokenForUserAccountIdAsync: and onAccessTokenError:, because of ADAL limitations, only the first scope in scopes[] is used
|
||||
@interface AADAccountProvider : NSObject <SingleUserAccountProvider>
|
||||
|
||||
// @brief clientId is a guid from the app's registration in the azure portal
|
||||
// redirectUri is a Uri specified in the same portal
|
||||
- (nullable instancetype)initWithClientId:(nonnull NSString*)clientId redirectUri:(nonnull NSURL*)redirectUri;
|
||||
|
||||
@property(readonly, nonatomic, copy, nonnull) NSString* clientId;
|
||||
@property(readonly, nonatomic, copy, nonnull) NSURL* redirectUri;
|
||||
|
||||
@end
|
|
@ -1,329 +0,0 @@
|
|||
//
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
//
|
||||
|
||||
#import "AADAccountProvider.h"
|
||||
|
||||
#import <ADAL/ADAL.h>
|
||||
|
||||
static NSString* const AADAccountProviderExceptionName = @"AADAccountProviderException";
|
||||
|
||||
/**
|
||||
* Notes about AAD/ADAL:
|
||||
* - Resource An Azure web service/app, such as https://graph.windows.net, or a CDP service.
|
||||
* - Scope Individual permissions within a resource
|
||||
* - Access Token A standard JSON web token for a given scope.
|
||||
* This is the actual token/user ticket used to authenticate with CDP services.
|
||||
* https://oauth.net/2/
|
||||
* https://www.oauth.com/oauth2-servers/access-tokens/
|
||||
* - Refresh token: A standard OAuth refresh token.
|
||||
* Lasts longer than access tokens, and is used to request new access tokens/refresh access tokens when they expire.
|
||||
* ADAL manages this automatically.
|
||||
* https://oauth.net/2/grant-types/refresh-token/
|
||||
* - MRRT Multiresource refresh token. A refresh token that can be used to fetch access tokens for more than one resource.
|
||||
* Getting one requires the user consent to all the covered resources. ADAL manages this automatically.
|
||||
*/
|
||||
@interface AADAccountProvider ()
|
||||
{
|
||||
ADAuthenticationContext* _authContext;
|
||||
ADTokenCacheItem* _tokenCacheItem;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation AADAccountProvider
|
||||
@synthesize userAccountChanged = _userAccountChanged;
|
||||
|
||||
- (instancetype)initWithClientId:(NSString*)clientId redirectUri:(NSURL*)redirectUri
|
||||
{
|
||||
if (self = [super init])
|
||||
{
|
||||
_clientId = [clientId copy];
|
||||
_redirectUri = [redirectUri copy];
|
||||
_userAccountChanged = [MCDUserAccountChangedEvent new];
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
// Don't share token cache between applications, only need them to be cached for this application
|
||||
// Without this, the MRRT is not cached, and the acquireTokenSilentWithResource: in getAccessToken
|
||||
// always fails with AD_ERROR_SERVER_USER_INPUT_NEEDED
|
||||
[[ADAuthenticationSettings sharedInstance] setDefaultKeychainGroup:nil];
|
||||
#endif
|
||||
|
||||
ADAuthenticationError* error = nil;
|
||||
_authContext =
|
||||
[ADAuthenticationContext authenticationContextWithAuthority:@"https://login.microsoftonline.com/common" error:&error];
|
||||
if (error)
|
||||
{
|
||||
NSLog(@"Error creating ADAuthenticationContext for AADAccountProvider: %@.", error);
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSLog(@"Checking if previous AADAccountProvider session can be loaded...");
|
||||
#if TARGET_OS_IPHONE
|
||||
NSArray<ADTokenCacheItem*>* tokenCacheItems = [[ADKeychainTokenCache defaultKeychainCache] allItems:nil];
|
||||
#else
|
||||
NSArray<ADTokenCacheItem*>* tokenCacheItems = [[ADTokenCache defaultCache] allItems:nil];
|
||||
#endif
|
||||
if (tokenCacheItems.count > 0)
|
||||
{
|
||||
for (ADTokenCacheItem* item in tokenCacheItems)
|
||||
{
|
||||
if (item.isMultiResourceRefreshToken && [_clientId isEqualToString:item.clientId])
|
||||
{
|
||||
_tokenCacheItem = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (_tokenCacheItem)
|
||||
{
|
||||
NSLog(@"Loaded previous AADAccountProvider session, starting as signed in.");
|
||||
}
|
||||
else
|
||||
{
|
||||
NSLog(@"No previous AADAccountProvider session could be loaded, starting as signed out.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)_raiseAccountChangedEvent
|
||||
{
|
||||
NSLog(@"Raise Account changed event");
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
// fire event on a different thread
|
||||
[self.userAccountChanged raise];
|
||||
});
|
||||
}
|
||||
|
||||
- (BOOL)signedIn
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
return _tokenCacheItem != nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)signInWithCompletionCallback:(SampleAccountProviderCompletionBlock)callback
|
||||
{
|
||||
if (self.signedIn)
|
||||
{
|
||||
callback(NO, SampleAccountActionFailureReasonAlreadySignedIn);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user has not previously consented for this default resource for this app,
|
||||
// the interactive flow will ask for user consent for all resources used by the app.
|
||||
// If the user previously consented to this resource on this app, and more resources are added to the app later on,
|
||||
// a consent prompt for all app resources will be raised when an access token for a new resource is requested -
|
||||
// see getAccessTokenForUserAccountIdAsync:
|
||||
NSString* defaultResource = @"https://graph.windows.net";
|
||||
|
||||
[_authContext acquireTokenWithResource:defaultResource
|
||||
clientId:_clientId
|
||||
redirectUri:_redirectUri
|
||||
completionBlock:^(ADAuthenticationResult* result) {
|
||||
switch (result.status)
|
||||
{
|
||||
case AD_SUCCEEDED:
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
_tokenCacheItem = result.tokenCacheItem;
|
||||
}
|
||||
[self _raiseAccountChangedEvent];
|
||||
callback(YES, SampleAccountActionNoFailure);
|
||||
break;
|
||||
}
|
||||
case AD_USER_CANCELLED:
|
||||
{
|
||||
callback(NO, SampleAccountActionFailureReasonUserCancelled);
|
||||
break;
|
||||
}
|
||||
case AD_FAILED:
|
||||
default:
|
||||
{
|
||||
NSLog(@"Error occurred in ADAL when signing in to an AAD account. Status: %u, Error: %@", result.status,
|
||||
result.error);
|
||||
callback(NO, SampleAccountActionFailureReasonADAL);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)signOutWithCompletionCallback:(SampleAccountProviderCompletionBlock)callback
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
if (!self.signedIn)
|
||||
{
|
||||
callback(NO, SampleAccountActionFailureReasonAlreadySignedOut);
|
||||
return;
|
||||
}
|
||||
|
||||
ADAuthenticationError* error;
|
||||
#if TARGET_OS_IPHONE
|
||||
BOOL removed = [[ADKeychainTokenCache defaultKeychainCache] removeAllForClientId:_clientId error:&error];
|
||||
#else
|
||||
// The above convenience method does not exist on OSX
|
||||
BOOL removed;
|
||||
NSArray<ADTokenCacheItem*>* tokenCacheItems = [[ADTokenCache defaultCache] allItems:&error];
|
||||
if (!error)
|
||||
{
|
||||
for (ADTokenCacheItem* item in tokenCacheItems)
|
||||
{
|
||||
if ([item.clientId isEqualToString:_clientId])
|
||||
{
|
||||
removed = [[ADTokenCache defaultCache] removeItem:item error:&error];
|
||||
|
||||
if (!removed || error)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!removed || error)
|
||||
{
|
||||
NSLog(@"Failed to remove token from ADAL cache, error %@", error);
|
||||
callback(NO, SampleAccountActionFailureReasonADAL);
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete cookies
|
||||
NSArray<NSString*>* cookieNamesToDelete =
|
||||
@[ @"SignInStateCookie", @"ESTSAUTHPERSISTENT", @"ESTSAUTHLIGHT", @"ESTSAUTH", @"ESTSSC" ];
|
||||
|
||||
NSHTTPCookieStorage* cookieJar = [NSHTTPCookieStorage sharedHTTPCookieStorage];
|
||||
for (NSHTTPCookie* cookie in [cookieJar cookies])
|
||||
{
|
||||
if ([cookieNamesToDelete containsObject:cookie.name])
|
||||
{
|
||||
[cookieJar deleteCookie:cookie];
|
||||
}
|
||||
}
|
||||
|
||||
_tokenCacheItem = nil;
|
||||
}
|
||||
|
||||
[self _raiseAccountChangedEvent];
|
||||
callback(YES, SampleAccountActionNoFailure);
|
||||
}
|
||||
|
||||
- (void)getAccessTokenForUserAccountIdAsync:(NSString*)accountId
|
||||
scopes:(NSArray<NSString*>*)scopes
|
||||
completion:(void (^)(MCDAccessTokenResult*, NSError*))completionBlock
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
if (!self.signedIn || ![accountId isEqualToString:_tokenCacheItem.userInformation.uniqueId])
|
||||
{
|
||||
completionBlock(nil, [NSError errorWithDomain:@"AADAccountProvider"
|
||||
code:0
|
||||
userInfo:@{
|
||||
@"Reason" : @"AADAccountProvider does not provide this account."
|
||||
}]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to fetch the token silently in the background, escalating to the ui thread if needed for a unique case (see below)
|
||||
__weak __block void (^weakAdalCallback)(ADAuthenticationResult*); // __weak __block is needed for recursive blocks under ARC
|
||||
__block void (^adalCallback)(ADAuthenticationResult*) = ^void(ADAuthenticationResult* adalResult) {
|
||||
MCDAccessTokenResult* result;
|
||||
NSError* error;
|
||||
|
||||
switch (adalResult.status)
|
||||
{
|
||||
case AD_SUCCEEDED:
|
||||
{
|
||||
result =
|
||||
[[MCDAccessTokenResult alloc] initWithAccessToken:adalResult.accessToken status:MCDAccessTokenRequestStatusSuccess];
|
||||
break;
|
||||
}
|
||||
case AD_USER_CANCELLED:
|
||||
{
|
||||
error = [NSError errorWithDomain:@"AADAccountProvider" code:0 userInfo:@{ @"Reason" : @"Cancelled by user." }];
|
||||
break;
|
||||
}
|
||||
case AD_FAILED:
|
||||
default:
|
||||
{
|
||||
if (adalResult.error.code == AD_ERROR_SERVER_USER_INPUT_NEEDED)
|
||||
{
|
||||
// This error only returns from acquireTokenSilentWithResource: when an interactive prompt is needed.
|
||||
// ADAL has an MRRT, but the user has not consented for this resource/the MRRT does not cover this resource.
|
||||
// Usually, users consent for all resources the app needs during the interactive flow in signInWith...:
|
||||
// However, if the app adds new resources after the user consented previously, signIn will not prompt.
|
||||
// Escalate to the UI thread and do an interactive flow,
|
||||
// which should raise a new consent prompt for all current app resources.
|
||||
NSLog(@"A resource was requested that the user did not previously consent to. "
|
||||
@"Attempting to raise an interactive consent prompt.");
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[_authContext acquireTokenWithResource:scopes[0]
|
||||
clientId:_clientId
|
||||
redirectUri:_redirectUri
|
||||
completionBlock:weakAdalCallback];
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
error = [NSError errorWithDomain:@"AADAccountProvider" code:0 userInfo:@{ @"Reason" : @"Unknown ADAL error." }];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ completionBlock(result, error); });
|
||||
};
|
||||
|
||||
weakAdalCallback = adalCallback;
|
||||
[_authContext acquireTokenSilentWithResource:scopes[0]
|
||||
clientId:_clientId
|
||||
redirectUri:_redirectUri
|
||||
userId:_tokenCacheItem.userInformation.userId
|
||||
completionBlock:adalCallback];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray<MCDUserAccount*>*)getUserAccounts
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
return _tokenCacheItem ?
|
||||
@[ [[MCDUserAccount alloc] initWithAccountId:_tokenCacheItem.userInformation.uniqueId type:MCDUserAccountTypeAAD] ] :
|
||||
nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)onAccessTokenError:(NSString*)accountId scopes:(NSArray<NSString*>*)scopes isPermanentError:(BOOL)isPermanentError
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
if ([accountId isEqualToString:_tokenCacheItem.userInformation.uniqueId])
|
||||
{
|
||||
if (isPermanentError)
|
||||
{
|
||||
_tokenCacheItem = nil;
|
||||
[self _raiseAccountChangedEvent];
|
||||
}
|
||||
else
|
||||
{
|
||||
// If not a permanent error, try just refreshing the token by calling ADAL's acquireToken: again
|
||||
[_authContext acquireTokenWithResource:scopes[0]
|
||||
clientId:_clientId
|
||||
redirectUri:_redirectUri
|
||||
completionBlock:^(__unused ADAuthenticationResult* result){}];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
NSLog(@"accountId was not found in AADAccountProvider.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
|
@ -1,44 +0,0 @@
|
|||
//
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <ConnectedDevices/Core/Core.h>
|
||||
|
||||
#import "SampleAccountActionFailureReason.h"
|
||||
|
||||
typedef NS_ENUM(NSInteger, AADMSAAccountProviderSignInState)
|
||||
{
|
||||
AADMSAAccountProviderSignInStateSignedOut,
|
||||
AADMSAAccountProviderSignInStateSignedInMSA,
|
||||
AADMSAAccountProviderSignInStateSignedInAAD,
|
||||
};
|
||||
|
||||
// @brief A sample MCDUserAccountProvider that wraps around an AAD provider and an MSA provider.
|
||||
// Supports only a single user account at a time - trying to log into more than one account at once will throw an exception.
|
||||
// Any accounts logged into will be made available through the MCDUserAccountProvider interface.
|
||||
//
|
||||
// When signed into an AAD account, because of AAD limitations,
|
||||
// only the first scope in scopes[] passed to for getAccessTokenForUserAccountIdAsync: and onAccessTokenError:, is used
|
||||
//
|
||||
// msaClientId is a guid from the app's registration in the msa apps portal
|
||||
// msaScopeOverrides is a map for the app to specify special scopes to replace the default ones
|
||||
// aadApplicationId is a guid from the app's registration in the azure portal
|
||||
// aadRedirectUri is a Uri specified in the azure portal
|
||||
@interface AADMSAAccountProvider : NSObject <MCDUserAccountProvider>
|
||||
@property(readonly, atomic) AADMSAAccountProviderSignInState signInState;
|
||||
@property(readonly, nonatomic, copy, nonnull) NSString* msaClientId;
|
||||
@property(readonly, nonatomic, copy, nonnull) NSString* aadApplicationId;
|
||||
|
||||
- (nullable instancetype)initWithMsaClientId:(nonnull NSString*)msaClientId
|
||||
msaScopeOverrides:(nullable NSDictionary<NSString*, NSArray<NSString*>*>*) scopes
|
||||
aadApplicationId:(nonnull NSString*)aadApplicationId
|
||||
aadRedirectUri:(nonnull NSURL*)aadRedirectUri;
|
||||
- (void)signInMSAWithCompletionCallback:(nonnull SampleAccountProviderCompletionBlock)callback;
|
||||
- (void)signInAADWithCompletionCallback:(nonnull SampleAccountProviderCompletionBlock)callback;
|
||||
- (void)signOutWithCompletionCallback:(nonnull SampleAccountProviderCompletionBlock)callback;
|
||||
|
||||
@end
|
|
@ -1,135 +0,0 @@
|
|||
//
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
//
|
||||
|
||||
#import "AADMSAAccountProvider.h"
|
||||
|
||||
#import "AADAccountProvider.h"
|
||||
#import "MSAAccountProvider.h"
|
||||
|
||||
static NSString* const AADMSAAccountProviderExceptionName = @"AADMSAAccountProviderException";
|
||||
|
||||
@interface AADMSAAccountProvider ()
|
||||
@property(readonly, nonatomic, strong) MSAAccountProvider* msaProvider;
|
||||
@property(readonly, nonatomic, strong) AADAccountProvider* aadProvider;
|
||||
@end
|
||||
|
||||
@implementation AADMSAAccountProvider
|
||||
|
||||
@synthesize userAccountChanged = _userAccountChanged;
|
||||
|
||||
- (instancetype)initWithMsaClientId:(NSString*)msaClientId
|
||||
msaScopeOverrides:(NSDictionary<NSString*, NSArray<NSString*>*>*)scopes
|
||||
aadApplicationId:(NSString*)aadApplicationId
|
||||
aadRedirectUri:(NSURL*)aadRedirectUri
|
||||
{
|
||||
if (self = [super init])
|
||||
{
|
||||
_userAccountChanged = [MCDUserAccountChangedEvent new];
|
||||
_msaProvider = [[MSAAccountProvider alloc] initWithClientId:msaClientId scopeOverrides:scopes];
|
||||
_aadProvider = [[AADAccountProvider alloc] initWithClientId:aadApplicationId redirectUri:aadRedirectUri];
|
||||
|
||||
if (_msaProvider.signedIn && _aadProvider.signedIn)
|
||||
{
|
||||
// Shouldn't ever happen, but if it does, sign out of AAD
|
||||
[_aadProvider signOutWithCompletionCallback:^(__unused BOOL success, __unused SampleAccountActionFailureReason reason){}];
|
||||
}
|
||||
|
||||
[_msaProvider.userAccountChanged subscribe:^void() { [self.userAccountChanged raise]; }];
|
||||
[_aadProvider.userAccountChanged subscribe:^void() { [self.userAccountChanged raise]; }];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (AADMSAAccountProviderSignInState)signInState
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
if (_msaProvider.signedIn)
|
||||
{
|
||||
return AADMSAAccountProviderSignInStateSignedInMSA;
|
||||
}
|
||||
else if (_aadProvider.signedIn)
|
||||
{
|
||||
return AADMSAAccountProviderSignInStateSignedInAAD;
|
||||
}
|
||||
return AADMSAAccountProviderSignInStateSignedOut;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString*)msaClientId
|
||||
{
|
||||
return _msaProvider.clientId;
|
||||
}
|
||||
|
||||
- (NSString*)aadApplicationId
|
||||
{
|
||||
return _aadProvider.clientId;
|
||||
}
|
||||
|
||||
- (id<SingleUserAccountProvider>)_signedInProvider
|
||||
{
|
||||
switch (self.signInState)
|
||||
{
|
||||
case AADMSAAccountProviderSignInStateSignedInMSA: return _msaProvider;
|
||||
case AADMSAAccountProviderSignInStateSignedInAAD: return _aadProvider;
|
||||
default: return nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)signInMSAWithCompletionCallback:(SampleAccountProviderCompletionBlock)callback
|
||||
{
|
||||
if (self.signInState != AADMSAAccountProviderSignInStateSignedOut)
|
||||
{
|
||||
[NSException raise:AADMSAAccountProviderExceptionName format:@"Already signed into an account!"];
|
||||
}
|
||||
[_msaProvider signInWithCompletionCallback:callback];
|
||||
}
|
||||
|
||||
- (void)signInAADWithCompletionCallback:(SampleAccountProviderCompletionBlock)callback
|
||||
{
|
||||
if (self.signInState != AADMSAAccountProviderSignInStateSignedOut)
|
||||
{
|
||||
[NSException raise:AADMSAAccountProviderExceptionName format:@"Already signed into an account!"];
|
||||
}
|
||||
[_aadProvider signInWithCompletionCallback:callback];
|
||||
}
|
||||
|
||||
- (void)signOutWithCompletionCallback:(__unused SampleAccountProviderCompletionBlock)callback
|
||||
{
|
||||
id<SingleUserAccountProvider> signedInProvider = [self _signedInProvider];
|
||||
if (!signedInProvider)
|
||||
{
|
||||
[NSException raise:AADMSAAccountProviderExceptionName format:@"Not signed into an account!"];
|
||||
}
|
||||
[signedInProvider signOutWithCompletionCallback:callback];
|
||||
}
|
||||
|
||||
- (void)getAccessTokenForUserAccountIdAsync:(NSString*)accountId
|
||||
scopes:(NSArray<NSString*>*)scopes
|
||||
completion:(void (^)(MCDAccessTokenResult*, NSError*))completionBlock
|
||||
{
|
||||
id<SingleUserAccountProvider> signedInProvider = [self _signedInProvider];
|
||||
if (!signedInProvider)
|
||||
{
|
||||
[NSException raise:AADMSAAccountProviderExceptionName format:@"Not signed into an account!"];
|
||||
}
|
||||
[signedInProvider getAccessTokenForUserAccountIdAsync:accountId scopes:scopes completion:completionBlock];
|
||||
}
|
||||
|
||||
- (NSArray<MCDUserAccount*>*)getUserAccounts
|
||||
{
|
||||
return [[self _signedInProvider] getUserAccounts];
|
||||
}
|
||||
|
||||
- (void)onAccessTokenError:(NSString*)accountId scopes:(NSArray<NSString*>*)scopes isPermanentError:(BOOL)isPermanentError
|
||||
{
|
||||
id<SingleUserAccountProvider> signedInProvider = [self _signedInProvider];
|
||||
if (!signedInProvider)
|
||||
{
|
||||
[NSException raise:AADMSAAccountProviderExceptionName format:@"Not signed into an account!"];
|
||||
}
|
||||
[signedInProvider onAccessTokenError:accountId scopes:scopes isPermanentError:isPermanentError];
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,23 @@
|
|||
//
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <ConnectedDevices/ConnectedDevices/ConnectedDevices.h>
|
||||
#import "SignInAccount.h"
|
||||
|
||||
@interface MSAAccount : NSObject <SignInAccount>
|
||||
- (nullable instancetype)initWithClientId:(nonnull NSString*)clientId
|
||||
scopeOverrides:(nullable NSDictionary<NSString*, NSArray<NSString*>*>*)scopes;
|
||||
- (void)signInWithCompletionCallback:(nonnull void (^)(MCDConnectedDevicesAccount* _Nonnull, NSError* _Nullable))callback;
|
||||
- (void)signOutWithCompletionCallback:(nonnull void (^)(MCDConnectedDevicesAccount* _Nonnull, NSError* _Nullable))callback;
|
||||
|
||||
@property(readonly, nonatomic, copy, nonnull) NSString* clientId;
|
||||
@property(readonly) BOOL isSignedIn;
|
||||
@property(readonly, nonatomic, nonnull) MCDConnectedDevicesAccount* mcdAccount;
|
||||
@property(readonly, nonatomic, copy, nonnull) NSDictionary<NSString*, NSArray<NSString*>*>* scopeOverrides;
|
||||
- (void)removeAccount;
|
||||
@end
|
|
@ -2,7 +2,7 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MSAAccountProvider.h"
|
||||
#import "MSAAccount.h"
|
||||
#import "MSATokenCache.h"
|
||||
#import "MSATokenRequest.h"
|
||||
|
||||
|
@ -51,6 +51,17 @@
|
|||
* 4. Now treated as signed out. Account is no longer exposed to CDP. userAccountChanged event is fired.
|
||||
*/
|
||||
|
||||
/*
|
||||
- (nullable instancetype)initWithClientId:(nonnull NSString*)clientId
|
||||
accountType:(MCDConnectedDevicesAccountType)type
|
||||
scopeOverrides:(nullable NSDictionary<NSString*, NSArray<NSString*>*>*)scopes
|
||||
redirectUri:(nonnull NSURL*)redirectUri;
|
||||
|
||||
- (nullable instancetype)initMSAWithClientId:(nonnull NSString*)clientId
|
||||
scopeOverrides:(nullable NSDictionary<NSString*, NSArray<NSString*>*>*)scopes;
|
||||
|
||||
- (nullable instancetype)initAADWithClientId:(nonnull NSString*)clientId redirectUri:(nonnull NSURL*)redirectUri;*/
|
||||
|
||||
#pragma mark - Constants
|
||||
// CDP's SDK currently requires authorization for all features, otherwise platform initialization will fail.
|
||||
// As such, the user must sign in/consent for the following scopes. This may change to become more modular in the future.
|
||||
|
@ -61,7 +72,7 @@ static NSString* const MsaRequiredScopes = //
|
|||
@"dds.register+" // device discovery scope (allow discovering this device)
|
||||
@"wns.connect+" // push notification scope
|
||||
@"asimovrome.telemetry+" // asimov token scope
|
||||
@"https://activity.windows.com/UserActivity.ReadWrite.CreatedByApp"; // default userdata.useractivities scope
|
||||
@"https://activity.windows.com/UserActivity.ReadWrite.CreatedByApp"; // default useractivities scope
|
||||
|
||||
// OAuth URLs
|
||||
static NSString* const MsaRedirectUrl = @"https://login.live.com/oauth20_desktop.srf";
|
||||
|
@ -70,9 +81,6 @@ static NSString* const MsaLogoutUrl = @"https://login.live.com/oauth20_logout.sr
|
|||
|
||||
// NSError constants
|
||||
static NSString* const MsaAccountProviderErrorDomain = @"MSAAccountProvider";
|
||||
static const NSInteger MsaAccountProviderErrorInvalidAccountId = 100;
|
||||
static const NSInteger MsaAccountProviderErrorAccessTokenTemporaryError = 101;
|
||||
static const NSInteger MsaAccountProviderErrorAccessTokenPermanentError = 102;
|
||||
|
||||
#pragma mark - Static Helpers
|
||||
// Helper function - gets the NSURLQueryItem matching name
|
||||
|
@ -85,24 +93,21 @@ static NSURLQueryItem* GetQueryItemForName(NSArray<NSURLQueryItem*>* queryItems,
|
|||
}
|
||||
|
||||
#pragma mark - Private Members
|
||||
@interface MSAAccountProvider () <MSATokenCacheDelegate, UIWebViewDelegate>
|
||||
@interface MSAAccount () <MSATokenCacheDelegate, UIWebViewDelegate>
|
||||
{
|
||||
NSString* _clientId;
|
||||
NSDictionary<NSString*, NSArray<NSString*>*>* _scopeOverrides;
|
||||
MCDUserAccount* _account;
|
||||
MSATokenCache* _tokenCache;
|
||||
BOOL _signInSignOutInProgress;
|
||||
SampleAccountProviderCompletionBlock _signInSignOutCallback;
|
||||
void (^_signInSignOutCallback)(MCDConnectedDevicesAccount*, NSError*);
|
||||
UIWebView* _webView;
|
||||
}
|
||||
@end
|
||||
|
||||
#pragma mark - Implementation
|
||||
@implementation MSAAccountProvider
|
||||
@synthesize userAccountChanged = _userAccountChanged;
|
||||
@implementation MSAAccount
|
||||
|
||||
- (instancetype)initWithClientId:(NSString*)clientId
|
||||
scopeOverrides:(NSDictionary<NSString*, NSArray<NSString*>*>*)scopes
|
||||
- (instancetype)initWithClientId:(NSString*)clientId scopeOverrides:(NSDictionary<NSString*, NSArray<NSString*>*>*)scopes
|
||||
{
|
||||
NSLog(@"MSAAccountProvider initWithClientId");
|
||||
|
||||
|
@ -113,14 +118,14 @@ static NSURLQueryItem* GetQueryItemForName(NSArray<NSURLQueryItem*>* queryItems,
|
|||
|
||||
_tokenCache = [MSATokenCache cacheWithClientId:_clientId delegate:self];
|
||||
|
||||
_userAccountChanged = [MCDUserAccountChangedEvent new];
|
||||
_signInSignOutInProgress = NO;
|
||||
_signInSignOutCallback = nil;
|
||||
|
||||
if ([_tokenCache loadSavedRefreshToken])
|
||||
{
|
||||
NSLog(@"Loaded previous session for MSAAccountProvider. Starting as signed in.");
|
||||
_account = [[MCDUserAccount alloc] initWithAccountId:[[NSUUID UUID] UUIDString] type:MCDUserAccountTypeMSA];
|
||||
|
||||
_mcdAccount = [[MCDConnectedDevicesAccount alloc] initWithAccountId:[[_tokenCache getAccountId] UUIDString]
|
||||
type:MCDConnectedDevicesAccountTypeMSA];
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -132,7 +137,7 @@ static NSURLQueryItem* GetQueryItemForName(NSArray<NSURLQueryItem*>* queryItems,
|
|||
}
|
||||
|
||||
#pragma mark - Private Helpers
|
||||
- (NSString*)_getAuthScopes: (NSArray<NSString*>*) incoming
|
||||
- (NSString*)_getAuthScopes:(NSArray<NSString*>*)incoming
|
||||
{
|
||||
NSMutableArray<NSString*>* scopes = [NSMutableArray new];
|
||||
for (NSString* scope in incoming)
|
||||
|
@ -150,40 +155,6 @@ static NSURLQueryItem* GetQueryItemForName(NSArray<NSURLQueryItem*>* queryItems,
|
|||
return [scopes componentsJoinedByString:@"+"];
|
||||
}
|
||||
|
||||
- (void)_raiseAccountChangedEvent
|
||||
{
|
||||
NSLog(@"Raise Account changed event");
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
// fire event on a different thread
|
||||
[self.userAccountChanged raise];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)_addAccount
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
NSLog(@"Adding an account.");
|
||||
_account = [[MCDUserAccount alloc] initWithAccountId:[[NSUUID UUID] UUIDString] type:MCDUserAccountTypeMSA];
|
||||
[self _raiseAccountChangedEvent];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_removeAccount
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
// clean account states
|
||||
if (self.signedIn)
|
||||
{
|
||||
NSLog(@"Removing account.");
|
||||
_account = nil;
|
||||
[_tokenCache clearTokens];
|
||||
[self _raiseAccountChangedEvent];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_loadWebRequest:(NSString*)requestUri
|
||||
{
|
||||
@synchronized(self)
|
||||
|
@ -204,72 +175,22 @@ static NSURLQueryItem* GetQueryItemForName(NSArray<NSURLQueryItem*>* queryItems,
|
|||
}
|
||||
}
|
||||
|
||||
- (void)_signInSignOutSucceededAsync:(BOOL)successful reason:(SampleAccountActionFailureReason)reason
|
||||
- (void)_signInSignOutSucceededAsync:(BOOL)__unused successful error:(NSError*)error
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{ [_webView removeFromSuperview]; });
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
_signInSignOutCallback(successful, reason);
|
||||
_signInSignOutCallback(_mcdAccount, error);
|
||||
_signInSignOutCallback = nil;
|
||||
_signInSignOutInProgress = NO;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously requests a new access token for the provided scope(s) and caches it.
|
||||
* This assumes that the sign in helper is currently signed in.
|
||||
*/
|
||||
- (void)_requestNewAccessTokenAsync:(NSString*)scope callback:(void (^)(MCDAccessTokenResult*, NSError*))completionBlock
|
||||
{
|
||||
// Need the refresh token first, then can use it to request an access token
|
||||
[_tokenCache getRefreshTokenAsync:^void(NSString* refreshToken) {
|
||||
NSLog(@"Fetching access token for scope:%@", scope);
|
||||
[MSATokenRequest
|
||||
doAsyncRequestWithClientId:_clientId
|
||||
grantType:MsaTokenRequestGrantTypeRefresh
|
||||
scope:scope
|
||||
redirectUri:nil
|
||||
token:refreshToken
|
||||
callback:^void(MSATokenRequestResult* result) {
|
||||
switch (result.status)
|
||||
{
|
||||
case MSATokenRequestStatusSuccess:
|
||||
{
|
||||
NSLog(@"Successfully fetched access token.");
|
||||
[_tokenCache setAccessToken:result.accessToken forScope:scope expiresIn:result.expiresIn];
|
||||
|
||||
completionBlock([[MCDAccessTokenResult alloc] initWithAccessToken:result.accessToken
|
||||
status:MCDAccessTokenRequestStatusSuccess],
|
||||
nil);
|
||||
break;
|
||||
}
|
||||
case MSATokenRequestStatusTransientFailure:
|
||||
{
|
||||
NSLog(@"Requesting new access token failed temporarily, please try again.");
|
||||
completionBlock(nil, [NSError errorWithDomain:MsaAccountProviderErrorDomain
|
||||
code:MsaAccountProviderErrorAccessTokenTemporaryError
|
||||
userInfo:nil]);
|
||||
break;
|
||||
}
|
||||
default: // PermanentFailure
|
||||
{
|
||||
NSLog(@"Permanent error occurred while fetching access token.");
|
||||
[self onAccessTokenError:_account.accountId scopes:@[ scope ] isPermanentError:YES];
|
||||
completionBlock(nil, [NSError errorWithDomain:MsaAccountProviderErrorDomain
|
||||
code:MsaAccountProviderErrorAccessTokenPermanentError
|
||||
userInfo:nil]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}];
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Interactive Sign In/Out
|
||||
- (BOOL)signedIn
|
||||
- (BOOL)isSignedIn
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
return _account != nil;
|
||||
return _mcdAccount != nil;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -277,18 +198,20 @@ static NSURLQueryItem* GetQueryItemForName(NSArray<NSURLQueryItem*>* queryItems,
|
|||
* Pops up a webview for the user to sign in with their MSA, then uses the auth code returned to cache a refresh token for the user.
|
||||
* If a refresh token was already cached from a previous session, it will be used instead, and no webview will be displayed.
|
||||
*/
|
||||
- (void)signInWithCompletionCallback:(SampleAccountProviderCompletionBlock)signInCallback
|
||||
- (void)signInWithCompletionCallback:(void (^)(MCDConnectedDevicesAccount*, NSError*))signInCallback
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
_signInSignOutCallback = signInCallback;
|
||||
|
||||
if (self.signedIn || _signInSignOutInProgress)
|
||||
if (self.isSignedIn || _signInSignOutInProgress)
|
||||
{
|
||||
// if already signed in or in the process, callback immediately with failure and reason
|
||||
[self _signInSignOutSucceededAsync:NO
|
||||
reason:(self.signedIn ? SampleAccountActionFailureReasonAlreadySignedIn :
|
||||
SampleAccountActionFailureReasonSigninSignOutInProgress)];
|
||||
NSError* error = [NSError errorWithDomain:MsaAccountProviderErrorDomain
|
||||
code:(self.isSignedIn ? SampleAccountActionFailureReasonAlreadySignedIn :
|
||||
SampleAccountActionFailureReasonSigninSignOutInProgress)
|
||||
userInfo:nil];
|
||||
[self _signInSignOutSucceededAsync:NO error:error];
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -304,18 +227,20 @@ static NSURLQueryItem* GetQueryItemForName(NSArray<NSURLQueryItem*>* queryItems,
|
|||
/**
|
||||
* Signs the user out by going through the webview, then clears the cache and current state.
|
||||
*/
|
||||
- (void)signOutWithCompletionCallback:(SampleAccountProviderCompletionBlock)signOutCallback
|
||||
- (void)signOutWithCompletionCallback:(void (^)(MCDConnectedDevicesAccount*, NSError*))signOutCallback
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
_signInSignOutCallback = signOutCallback;
|
||||
|
||||
if (!self.signedIn || _signInSignOutInProgress)
|
||||
if (!self.isSignedIn || _signInSignOutInProgress)
|
||||
{
|
||||
// if already signed out or in the process, callback immediately with failure and reason
|
||||
[self _signInSignOutSucceededAsync:NO
|
||||
reason:(self.signedIn ? SampleAccountActionFailureReasonSigninSignOutInProgress :
|
||||
SampleAccountActionFailureReasonAlreadySignedOut)];
|
||||
// if already signed out or in the process, callback immediately with failure
|
||||
NSError* error = [NSError errorWithDomain:MsaAccountProviderErrorDomain
|
||||
code:(self.isSignedIn ? SampleAccountActionFailureReasonSigninSignOutInProgress :
|
||||
SampleAccountActionFailureReasonAlreadySignedOut)
|
||||
userInfo:nil];
|
||||
[self _signInSignOutSucceededAsync:NO error:error];
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -326,6 +251,22 @@ static NSURLQueryItem* GetQueryItemForName(NSArray<NSURLQueryItem*>* queryItems,
|
|||
}
|
||||
}
|
||||
|
||||
- (void)removeAccount
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
NSLog(@"Remove the account");
|
||||
|
||||
// clean account states
|
||||
if (self.isSignedIn)
|
||||
{
|
||||
NSLog(@"Remove the account - clean cached account state");
|
||||
_mcdAccount = nil;
|
||||
[_tokenCache clearTokens];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Continuation for signIn/signOut after the webview completes.
|
||||
*/
|
||||
|
@ -346,10 +287,14 @@ static NSURLQueryItem* GetQueryItemForName(NSArray<NSURLQueryItem*>* queryItems,
|
|||
|
||||
NSArray<NSURLQueryItem*>* tokenURLQueryItems = tokenURLComponents.queryItems;
|
||||
|
||||
if (GetQueryItemForName(tokenURLQueryItems, @"error"))
|
||||
NSURLQueryItem* errorResponse = GetQueryItemForName(tokenURLQueryItems, @"error");
|
||||
if (errorResponse)
|
||||
{
|
||||
// sign in or sign out ending in failure
|
||||
[self _signInSignOutSucceededAsync:NO reason:SampleAccountActionFailureReasonUnknown];
|
||||
NSError* error = [NSError errorWithDomain:MsaAccountProviderErrorDomain
|
||||
code:SampleAccountActionFailureReasonGeneric
|
||||
userInfo:@{ NSLocalizedDescriptionKey : errorResponse.description }];
|
||||
[self _signInSignOutSucceededAsync:NO error:error];
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -357,8 +302,22 @@ static NSURLQueryItem* GetQueryItemForName(NSArray<NSURLQueryItem*>* queryItems,
|
|||
if (!authCode)
|
||||
{
|
||||
// sign out ended in success
|
||||
[self _removeAccount];
|
||||
[self _signInSignOutSucceededAsync:YES reason:SampleAccountActionNoFailure];
|
||||
// Delete cookies
|
||||
NSHTTPCookieStorage* cookieJar = [NSHTTPCookieStorage sharedHTTPCookieStorage];
|
||||
NSArray<NSHTTPCookie*>* cookies = [[cookieJar cookies] copy];
|
||||
for (NSHTTPCookie* cookie in cookies)
|
||||
{
|
||||
if ([cookie.domain containsString:@"live.com"])
|
||||
{
|
||||
[cookieJar deleteCookie:cookie];
|
||||
}
|
||||
}
|
||||
|
||||
[self removeAccount];
|
||||
[self _signInSignOutSucceededAsync:YES
|
||||
error:[NSError errorWithDomain:MsaAccountProviderErrorDomain
|
||||
code:SampleAccountActionFailureReasonFailToRetrieveAuthCode
|
||||
userInfo:nil]];
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -366,7 +325,10 @@ static NSURLQueryItem* GetQueryItemForName(NSArray<NSURLQueryItem*>* queryItems,
|
|||
if (authCode.length <= 0)
|
||||
{
|
||||
// very unusual
|
||||
[self _signInSignOutSucceededAsync:NO reason:SampleAccountActionFailureReasonFailToRetrieveAuthCode];
|
||||
[self _signInSignOutSucceededAsync:NO
|
||||
error:[NSError errorWithDomain:MsaAccountProviderErrorDomain
|
||||
code:SampleAccountActionFailureReasonFailToRetrieveAuthCode
|
||||
userInfo:nil]];
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -378,14 +340,23 @@ static NSURLQueryItem* GetQueryItemForName(NSArray<NSURLQueryItem*>* queryItems,
|
|||
NSAssert(newRefreshToken != nil, @"refresh token can not be null when refreshing refresh token succeeded");
|
||||
|
||||
NSLog(@"Successfully fetch the root refresh token.");
|
||||
[_tokenCache setRefreshToken:newRefreshToken];
|
||||
[self _addAccount];
|
||||
[self _signInSignOutSucceededAsync:YES reason:SampleAccountActionNoFailure];
|
||||
NSUUID* accountId = [NSUUID UUID];
|
||||
[_tokenCache setRefreshToken:newRefreshToken withAccountId:accountId];
|
||||
@synchronized(self)
|
||||
{
|
||||
NSLog(@"Adding an account.");
|
||||
_mcdAccount = [[MCDConnectedDevicesAccount alloc] initWithAccountId:[accountId UUIDString]
|
||||
type:MCDConnectedDevicesAccountTypeMSA];
|
||||
}
|
||||
[self _signInSignOutSucceededAsync:YES error:nil];
|
||||
}
|
||||
else
|
||||
{
|
||||
NSLog(@"Failed to fetch root refresh token using authcode.");
|
||||
[self _signInSignOutSucceededAsync:NO reason:SampleAccountActionFailureReasonFailToRetrieveRefreshToken];
|
||||
[self _signInSignOutSucceededAsync:NO
|
||||
error:[NSError errorWithDomain:MsaAccountProviderErrorDomain
|
||||
code:SampleAccountActionFailureReasonFailToRetrieveRefreshToken
|
||||
userInfo:nil]];
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -414,21 +385,23 @@ static NSURLQueryItem* GetQueryItemForName(NSArray<NSURLQueryItem*>* queryItems,
|
|||
if (error.code != WebKitErrorFrameLoadInterruptedByPolicyChange /*interrupted*/
|
||||
&& error.code != NSURLErrorCancelled)
|
||||
{
|
||||
[self _signInSignOutSucceededAsync:NO reason:SampleAccountActionFailureReasonUserCancelled];
|
||||
[self _signInSignOutSucceededAsync:NO
|
||||
error:[NSError errorWithDomain:MsaAccountProviderErrorDomain
|
||||
code:SampleAccountActionFailureReasonUserCancelled
|
||||
userInfo:nil]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - MCDUserAccountProvider Overrides
|
||||
- (void)getAccessTokenForUserAccountIdAsync:(NSString*)accountId
|
||||
scopes:(NSArray<NSString*>*)scopes
|
||||
completion:(void (^)(MCDAccessTokenResult*, NSError*))completionBlock
|
||||
completion:(void (^)(NSString*, NSError*))completionBlock
|
||||
{
|
||||
if (![accountId isEqualToString:_account.accountId])
|
||||
if (![accountId isEqualToString:_mcdAccount.accountId])
|
||||
{
|
||||
NSLog(@"accountId did not match logged in account - is the user signed in?");
|
||||
completionBlock(
|
||||
nil, [NSError errorWithDomain:MsaAccountProviderErrorDomain code:MsaAccountProviderErrorInvalidAccountId userInfo:nil]);
|
||||
completionBlock([NSString stringWithFormat:@"Account ID %@ does it match account ID %@", accountId, _mcdAccount.accountId],
|
||||
[NSError errorWithDomain:MsaAccountProviderErrorDomain code:SampleAccountActionFailureReasonInvalidAccountId userInfo:nil]);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -444,8 +417,7 @@ static NSURLQueryItem* GetQueryItemForName(NSArray<NSURLQueryItem*>* queryItems,
|
|||
if (accessToken.length > 0)
|
||||
{
|
||||
NSLog(@"Found valid access token for scope %@ in cache, return early", accessTokenScope);
|
||||
completionBlock(
|
||||
[[MCDAccessTokenResult alloc] initWithAccessToken:accessToken status:MCDAccessTokenRequestStatusSuccess], nil);
|
||||
completionBlock(accessToken, nil);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -457,21 +429,13 @@ static NSURLQueryItem* GetQueryItemForName(NSArray<NSURLQueryItem*>* queryItems,
|
|||
});
|
||||
}
|
||||
|
||||
- (NSArray<MCDUserAccount*>*)getUserAccounts
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
return _account ? @[ _account ] : nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)onAccessTokenError:(NSString*)__unused accountId scopes:(NSArray<NSString*>*)__unused scopes isPermanentError:(BOOL)isPermanentError
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
if (isPermanentError)
|
||||
{
|
||||
[self _removeAccount];
|
||||
[self removeAccount];
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -483,10 +447,63 @@ static NSURLQueryItem* GetQueryItemForName(NSArray<NSURLQueryItem*>* queryItems,
|
|||
#pragma mark - MSATokenCache Delegate
|
||||
- (void)onTokenCachePermanentFailure
|
||||
{
|
||||
if (_account)
|
||||
if (_mcdAccount)
|
||||
{
|
||||
[self onAccessTokenError:_account.accountId scopes:[_tokenCache allScopes] isPermanentError:YES];
|
||||
[self onAccessTokenError:_mcdAccount.accountId scopes:[_tokenCache allScopes] isPermanentError:YES];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously requests a new access token for the provided scope(s) and caches it.
|
||||
* This assumes that the sign in helper is currently signed in.
|
||||
*/
|
||||
- (void)_requestNewAccessTokenAsync:(NSString*)scope callback:(void (^)(NSString*, NSError*))completionBlock
|
||||
{
|
||||
// Need the refresh token first, then can use it to request an access token
|
||||
[_tokenCache getRefreshTokenAsync:^void(NSString* refreshToken) {
|
||||
NSLog(@"Fetching access token for scope:%@", scope);
|
||||
[MSATokenRequest
|
||||
doAsyncRequestWithClientId:_clientId
|
||||
grantType:MsaTokenRequestGrantTypeRefresh
|
||||
scope:scope
|
||||
redirectUri:nil
|
||||
token:refreshToken
|
||||
callback:^void(MSATokenRequestResult* result) {
|
||||
switch (result.status)
|
||||
{
|
||||
case MSATokenRequestStatusSuccess:
|
||||
{
|
||||
NSLog(@"Successfully fetched access token.");
|
||||
[_tokenCache setAccessToken:result.accessToken
|
||||
withAccountId:[NSUUID UUID]
|
||||
forScope:scope
|
||||
expiresIn:result.expiresIn];
|
||||
|
||||
completionBlock(result.accessToken, nil);
|
||||
break;
|
||||
}
|
||||
case MSATokenRequestStatusTransientFailure:
|
||||
{
|
||||
NSLog(@"Requesting new access token failed temporarily, please try again.");
|
||||
completionBlock(
|
||||
@"fail2", [NSError errorWithDomain:MsaAccountProviderErrorDomain
|
||||
code:SampleAccountActionFailureReasonAccessTokenTemporaryError
|
||||
userInfo:nil]);
|
||||
break;
|
||||
}
|
||||
default: // PermanentFailure
|
||||
{
|
||||
NSLog(@"Permanent error occurred while fetching access token.");
|
||||
[self onAccessTokenError:_mcdAccount.accountId scopes:@[ scope ] isPermanentError:YES];
|
||||
completionBlock(
|
||||
@"fail3", [NSError errorWithDomain:MsaAccountProviderErrorDomain
|
||||
code:SampleAccountActionFailureReasonAccessTokenPermanentError
|
||||
userInfo:nil]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}];
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
|
@ -1,28 +0,0 @@
|
|||
//
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <ConnectedDevices/Core/Core.h>
|
||||
|
||||
#import "SingleUserAccountProvider.h"
|
||||
|
||||
/**
|
||||
* @brief
|
||||
* Sample implementation of MCDUserAccountProvider.
|
||||
* Exposes a single MSA account, that the user logs into via UIWebView, to CDP.
|
||||
* Follows OAuth2.0 protocol, but automatically refreshes tokens when they are close to expiring.
|
||||
*/
|
||||
@interface MSAAccountProvider : NSObject <SingleUserAccountProvider>
|
||||
|
||||
// @brief clientId is a guid from the app's registration in the msa portal
|
||||
// scopeOverrides is a map for the app to specify special scopes to replace the default ones
|
||||
- (nullable instancetype)initWithClientId:(nonnull NSString*)clientId
|
||||
scopeOverrides:(nullable NSDictionary<NSString*, NSArray<NSString*>*>*)scopes;
|
||||
|
||||
@property(readonly, nonatomic, copy, nonnull) NSString* clientId;
|
||||
|
||||
@end
|
|
@ -20,10 +20,14 @@
|
|||
- (nullable instancetype)initWithClientId:(nonnull NSString*)clientId delegate:(nullable id<MSATokenCacheDelegate>)delegate;
|
||||
|
||||
// @brief Adds/gets tokens to/from the cache, automatically refreshing them once expired.
|
||||
- (void)setRefreshToken:(nonnull NSString*)refreshToken;
|
||||
- (void)setAccessToken:(nonnull NSString*)accessToken forScope:(nonnull NSString*)scope expiresIn:(NSTimeInterval)expiry;
|
||||
- (void)setRefreshToken:(nonnull NSString*)refreshToken withAccountId:(nonnull NSUUID*)accountId;
|
||||
- (void)setAccessToken:(nonnull NSString*)accessToken
|
||||
withAccountId:(nonnull NSUUID*)accountId
|
||||
forScope:(nonnull NSString*)scope
|
||||
expiresIn:(NSTimeInterval)expiry;
|
||||
- (void)getRefreshTokenAsync:(nonnull void (^)(NSString* _Nullable accessToken))callback;
|
||||
- (void)getAccessTokenForScopeAsync:(nonnull NSString*)scope callback:(nonnull void (^)(NSString* _Nullable accessToken))callback;
|
||||
- (nullable NSUUID*)getAccountId;
|
||||
|
||||
// @brief Returns the scopes for which there are currently access tokens cached.
|
||||
- (nonnull NSArray<NSString*>*)allScopes;
|
||||
|
@ -39,4 +43,4 @@
|
|||
- (void)markAllTokensExpired;
|
||||
|
||||
@property(nonatomic, readwrite, nullable, strong) id<MSATokenCacheDelegate> delegate;
|
||||
@end
|
||||
@end
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
static NSString* const MsaOfflineAccessScope = @"wl.offline_access";
|
||||
|
||||
static NSString* const JsonTokenKey = @"refresh_token";
|
||||
static NSString* const JsonAccountIdKey = @"account_id";
|
||||
static NSString* const JsonExpirationKey = @"expires";
|
||||
|
||||
// Max number of times to try to refresh a token through transient failures
|
||||
|
@ -32,10 +33,12 @@ static const NSTimeInterval MsaAccessTokenCloseToExpiryInterval = 5 * 60;
|
|||
// @brief Private helper class, encapsulates a single MSA token to be cached, and how to refresh it.
|
||||
@interface MSATokenCacheItem : NSObject
|
||||
+ (nullable instancetype)cacheItemWithToken:(nonnull NSString*)token
|
||||
accountId:(nonnull NSUUID*)accountId
|
||||
expiresIn:(NSTimeInterval)expiry
|
||||
refreshWith:(nonnull MSATokenRequest*)refreshRequest
|
||||
parent:(nonnull MSATokenCache*)parent;
|
||||
- (nullable instancetype)initWithToken:(nonnull NSString*)token
|
||||
accountId:(nonnull NSUUID*)accountId
|
||||
expiresIn:(NSTimeInterval)expiry
|
||||
refreshWith:(nonnull MSATokenRequest*)refreshRequest
|
||||
parent:(nonnull MSATokenCache*)parent;
|
||||
|
@ -44,6 +47,7 @@ static const NSTimeInterval MsaAccessTokenCloseToExpiryInterval = 5 * 60;
|
|||
- (void)getTokenAsync:(nonnull void (^)(NSString* _Nullable token))callback;
|
||||
|
||||
@property(readwrite, nonnull, nonatomic, copy) NSString* token;
|
||||
@property(readwrite, nonnull, nonatomic, copy) NSUUID* accountId;
|
||||
@property(readwrite, nonnull, nonatomic, strong) NSDate* expirationDate;
|
||||
@property(readwrite, nonnull, nonatomic, strong) MSATokenRequest* refreshRequest;
|
||||
@property(readwrite, nonnull, nonatomic, strong) MSATokenCache* parent;
|
||||
|
@ -80,14 +84,16 @@ static const NSTimeInterval MsaAccessTokenCloseToExpiryInterval = 5 * 60;
|
|||
@implementation MSATokenCacheItem
|
||||
|
||||
+ (instancetype)cacheItemWithToken:(NSString*)token
|
||||
accountId:(NSUUID*)accountId
|
||||
expiresIn:(NSTimeInterval)expiry
|
||||
refreshWith:(MSATokenRequest*)refreshRequest
|
||||
parent:(MSATokenCache*)parent
|
||||
{
|
||||
return [[self alloc] initWithToken:token expiresIn:expiry refreshWith:refreshRequest parent:parent];
|
||||
return [[self alloc] initWithToken:token accountId:accountId expiresIn:expiry refreshWith:refreshRequest parent:parent];
|
||||
}
|
||||
|
||||
- (instancetype)initWithToken:(NSString*)token
|
||||
accountId:(NSUUID*)accountId
|
||||
expiresIn:(NSTimeInterval)expiry
|
||||
refreshWith:(MSATokenRequest*)refreshRequest
|
||||
parent:(MSATokenCache*)parent
|
||||
|
@ -95,6 +101,7 @@ static const NSTimeInterval MsaAccessTokenCloseToExpiryInterval = 5 * 60;
|
|||
if (self = [super init])
|
||||
{
|
||||
_token = [token copy];
|
||||
_accountId = [accountId copy];
|
||||
_expirationDate = [NSDate dateWithTimeIntervalSinceNow:expiry];
|
||||
_refreshRequest = refreshRequest;
|
||||
_parent = parent;
|
||||
|
@ -102,6 +109,11 @@ static const NSTimeInterval MsaAccessTokenCloseToExpiryInterval = 5 * 60;
|
|||
return self;
|
||||
}
|
||||
|
||||
- (nullable NSUUID*)getAccountId
|
||||
{
|
||||
return _accountId;
|
||||
}
|
||||
|
||||
- (void)getTokenAsync:(void (^)(NSString*))callback
|
||||
{
|
||||
[self getTokenAsync:callback maxRetries:MsaTokenRefreshMaxRetries];
|
||||
|
@ -246,6 +258,8 @@ static const NSTimeInterval MsaAccessTokenCloseToExpiryInterval = 5 * 60;
|
|||
|
||||
NSDictionary* tokenDict = (NSDictionary*)deserializedTokenData;
|
||||
NSString* loadedRefreshToken = (NSString*)(tokenDict[JsonTokenKey]);
|
||||
NSString* uuidString = tokenDict[JsonAccountIdKey];
|
||||
NSUUID* loadedAccountId = [[NSUUID alloc] initWithUUIDString:uuidString];
|
||||
|
||||
NSDateFormatter* dateFormatter = [NSDateFormatter new];
|
||||
dateFormatter.dateStyle = NSDateFormatterFullStyle;
|
||||
|
@ -262,8 +276,11 @@ static const NSTimeInterval MsaAccessTokenCloseToExpiryInterval = 5 * 60;
|
|||
grantType:MsaTokenRequestGrantTypeRefresh
|
||||
scope:MsaOfflineAccessScope
|
||||
redirectUri:nil];
|
||||
MSARefreshTokenCacheItem* ret =
|
||||
[self cacheItemWithToken:loadedRefreshToken expiresIn:timeUntilExpiration refreshWith:refreshRequest parent:parent];
|
||||
MSARefreshTokenCacheItem* ret = [self cacheItemWithToken:loadedRefreshToken
|
||||
accountId:loadedAccountId
|
||||
expiresIn:timeUntilExpiration
|
||||
refreshWith:refreshRequest
|
||||
parent:parent];
|
||||
|
||||
NSLog(@"Successfully loaded refresh token from keychain.");
|
||||
return ret;
|
||||
|
@ -276,7 +293,11 @@ static const NSTimeInterval MsaAccessTokenCloseToExpiryInterval = 5 * 60;
|
|||
NSDateFormatter* dateFormatter = [NSDateFormatter new];
|
||||
dateFormatter.dateStyle = NSDateFormatterFullStyle;
|
||||
dateFormatter.timeStyle = NSDateFormatterFullStyle;
|
||||
NSDictionary* tokenDict = @{ JsonTokenKey : self.token, JsonExpirationKey : [dateFormatter stringFromDate:self.expirationDate] };
|
||||
NSDictionary* tokenDict = @{
|
||||
JsonTokenKey : self.token,
|
||||
JsonAccountIdKey : self.accountId.UUIDString,
|
||||
JsonExpirationKey : [dateFormatter stringFromDate:self.expirationDate]
|
||||
};
|
||||
|
||||
NSError* jsonError = nil;
|
||||
NSData* tokenData = [NSJSONSerialization dataWithJSONObject:tokenDict options:0 error:&jsonError];
|
||||
|
@ -342,7 +363,6 @@ static const NSTimeInterval MsaAccessTokenCloseToExpiryInterval = 5 * 60;
|
|||
[self.parent markAccessTokensExpired];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
// MSATokenCache implementation
|
||||
|
@ -364,7 +384,7 @@ static const NSTimeInterval MsaAccessTokenCloseToExpiryInterval = 5 * 60;
|
|||
return self;
|
||||
}
|
||||
|
||||
- (void)setRefreshToken:(NSString*)refreshToken
|
||||
- (void)setRefreshToken:(NSString*)refreshToken withAccountId:(NSUUID*)accountId
|
||||
{
|
||||
MSATokenRequest* refreshRequest = [MSATokenRequest tokenRequestWithClientId:_clientId
|
||||
grantType:MsaTokenRequestGrantTypeRefresh
|
||||
|
@ -373,6 +393,7 @@ static const NSTimeInterval MsaAccessTokenCloseToExpiryInterval = 5 * 60;
|
|||
@synchronized(self)
|
||||
{
|
||||
_cachedRefreshToken = [MSARefreshTokenCacheItem cacheItemWithToken:refreshToken
|
||||
accountId:accountId
|
||||
expiresIn:MsaRefreshTokenExpirationInterval
|
||||
refreshWith:refreshRequest
|
||||
parent:self];
|
||||
|
@ -381,16 +402,19 @@ static const NSTimeInterval MsaAccessTokenCloseToExpiryInterval = 5 * 60;
|
|||
}
|
||||
}
|
||||
|
||||
- (void)setAccessToken:(NSString*)accessToken forScope:(NSString*)scope expiresIn:(NSTimeInterval)expiry
|
||||
- (void)setAccessToken:(NSString*)accessToken withAccountId:(NSUUID*)accountId forScope:(NSString*)scope expiresIn:(NSTimeInterval)expiry
|
||||
{
|
||||
MSATokenRequest* refreshRequest =
|
||||
[MSATokenRequest tokenRequestWithClientId:_clientId grantType:MsaTokenRequestGrantTypeRefresh scope:scope redirectUri:nil];
|
||||
|
||||
@synchronized(self)
|
||||
{
|
||||
[_cachedAccessTokens
|
||||
setValue:[MSATokenCacheItem cacheItemWithToken:accessToken expiresIn:expiry refreshWith:refreshRequest parent:self]
|
||||
forKey:scope];
|
||||
[_cachedAccessTokens setObject:[MSATokenCacheItem cacheItemWithToken:accessToken
|
||||
accountId:accountId
|
||||
expiresIn:expiry
|
||||
refreshWith:refreshRequest
|
||||
parent:self]
|
||||
forKey:scope];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -425,6 +449,11 @@ static const NSTimeInterval MsaAccessTokenCloseToExpiryInterval = 5 * 60;
|
|||
}
|
||||
}
|
||||
|
||||
- (nullable NSUUID*)getAccountId
|
||||
{
|
||||
return _cachedRefreshToken.accountId;
|
||||
}
|
||||
|
||||
- (NSArray<NSString*>*)allScopes
|
||||
{
|
||||
return [_cachedAccessTokens allKeys];
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
typedef NS_ENUM(NSInteger, SampleAccountActionFailureReason)
|
||||
{
|
||||
SampleAccountActionNoFailure,
|
||||
SampleAccountActionFailureReasonGeneric,
|
||||
SampleAccountActionFailureReasonAlreadySignedIn,
|
||||
SampleAccountActionFailureReasonAlreadySignedOut,
|
||||
SampleAccountActionFailureReasonUserCancelled,
|
||||
|
@ -17,7 +18,8 @@ typedef NS_ENUM(NSInteger, SampleAccountActionFailureReason)
|
|||
SampleAccountActionFailureReasonFailToRetrieveRefreshToken,
|
||||
SampleAccountActionFailureReasonSigninSignOutInProgress,
|
||||
SampleAccountActionFailureReasonUnknown,
|
||||
SampleAccountActionFailureReasonInvalidAccountId,
|
||||
SampleAccountActionFailureReasonAccessTokenTemporaryError,
|
||||
SampleAccountActionFailureReasonAccessTokenPermanentError,
|
||||
SampleAccountActionFailureReasonADAL,
|
||||
};
|
||||
|
||||
typedef void (^SampleAccountProviderCompletionBlock)(BOOL successful, SampleAccountActionFailureReason reason);
|
|
@ -0,0 +1,24 @@
|
|||
//
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <ConnectedDevices/ConnectedDevices/ConnectedDevices.h>
|
||||
|
||||
#import "SampleAccountActionFailureReason.h"
|
||||
|
||||
@protocol SignInAccount <NSObject>
|
||||
|
||||
- (void)signInWithCompletionCallback:(nonnull void (^)(MCDConnectedDevicesAccount* _Nonnull, NSError* _Nullable))callback;
|
||||
- (void)signOutWithCompletionCallback:(nonnull void (^)(MCDConnectedDevicesAccount* _Nonnull, NSError* _Nullable))callback;
|
||||
|
||||
- (void)getAccessTokenForUserAccountIdAsync:(nonnull NSString*)accountId
|
||||
scopes:(nonnull NSArray<NSString*>*)scopes
|
||||
completion:(nonnull void (^)(NSString* _Nonnull, NSError* _Nullable))scompletionBlock;
|
||||
|
||||
- (BOOL)isSignedIn;
|
||||
|
||||
@end
|
|
@ -4,12 +4,12 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#import <ConnectedDevices/Core/Core.h>
|
||||
#import <ConnectedDevices/ConnectedDevices.h>
|
||||
|
||||
#import "SampleAccountActionFailureReason.h"
|
||||
|
||||
// @brief Protocol for a MCDUserAccountProvider that supports logging into/out of a single user account.
|
||||
@protocol SingleUserAccountProvider <MCDUserAccountProvider>
|
||||
@protocol SingleUserAccountProvider
|
||||
- (void)signInWithCompletionCallback:(nonnull SampleAccountProviderCompletionBlock)callback;
|
||||
- (void)signOutWithCompletionCallback:(nonnull SampleAccountProviderCompletionBlock)callback;
|
||||
@property(readonly, atomic) BOOL signedIn;
|
||||
|
|
Загрузка…
Ссылка в новой задаче