react-native-oauth-essentials 1.0.11 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +384 -362
- package/android/src/main/java/com/oauthessentials/CredentialError.kt +4 -1
- package/android/src/main/java/com/oauthessentials/CredentialFactory.kt +24 -2
- package/android/src/main/java/com/oauthessentials/CustomTabsActivity.kt +14 -1
- package/android/src/main/java/com/oauthessentials/OauthEssentialsModule.kt +448 -192
- package/app.plugin.js +1 -0
- package/ios/AppleIdRetriever.swift +25 -26
- package/ios/CredentialError.swift +3 -0
- package/ios/CredentialFactory.swift +38 -20
- package/ios/CredentialsType.swift +1 -0
- package/ios/GoogleIdRetriever.swift +132 -56
- package/ios/HybridRetriever.swift +12 -18
- package/ios/OauthEssentials.h +0 -3
- package/ios/OauthEssentials.mm +45 -2
- package/ios/OauthEssentialsModule.swift +266 -57
- package/ios/PasskeyRetriever.swift +31 -49
- package/ios/PasswordRetriever.swift +10 -30
- package/ios/RCTPromiseSettler.swift +125 -0
- package/jest/index.js +149 -0
- package/lib/module/NativeOauthEssentials.js +10 -1
- package/lib/module/NativeOauthEssentials.js.map +1 -1
- package/lib/module/errors.js +38 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/index.js +432 -58
- package/lib/module/index.js.map +1 -1
- package/lib/module/oauth.js +97 -0
- package/lib/module/oauth.js.map +1 -0
- package/lib/module/platform.js +108 -0
- package/lib/module/platform.js.map +1 -0
- package/lib/module/types.js +46 -1
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/src/NativeOauthEssentials.d.ts +70 -16
- package/lib/typescript/src/NativeOauthEssentials.d.ts.map +1 -1
- package/lib/typescript/src/errors.d.ts +18 -0
- package/lib/typescript/src/errors.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +235 -40
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/oauth.d.ts +32 -0
- package/lib/typescript/src/oauth.d.ts.map +1 -0
- package/lib/typescript/src/platform.d.ts +39 -0
- package/lib/typescript/src/platform.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +190 -3
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/package.json +8 -4
- package/plugin/withOauthEssentials.js +291 -0
- package/src/NativeOauthEssentials.ts +91 -17
- package/src/errors.ts +49 -0
- package/src/index.tsx +587 -80
- package/src/oauth.ts +140 -0
- package/src/platform.ts +121 -0
- package/src/types.ts +200 -2
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import AuthenticationServices
|
|
1
2
|
import React
|
|
3
|
+
import UIKit
|
|
2
4
|
|
|
3
5
|
public typealias EventEmitter = ((_ eventName: String, _ body: [String: Any]) -> Void)?
|
|
4
6
|
public class RCTPromiseSettler: NSObject {
|
|
@@ -20,9 +22,132 @@ public class RCTPromiseSettler: NSObject {
|
|
|
20
22
|
return self.resolve == nil && self.reject == nil
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
internal func succeed(_ result: [String: Any]) {
|
|
26
|
+
self.resolve?(result)
|
|
27
|
+
self.emitEvent?("onCredentialSuccess", result as NSDictionary)
|
|
28
|
+
cleanup()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
internal func fail(_ code: CredentialError, _ message: String, _ error: Error? = nil) {
|
|
32
|
+
self.reject?(code.rawValue, message, error)
|
|
33
|
+
self.emitEvent?("onCredentialFailure", [
|
|
34
|
+
"code": code.rawValue,
|
|
35
|
+
"message": message
|
|
36
|
+
])
|
|
37
|
+
cleanup()
|
|
38
|
+
}
|
|
39
|
+
|
|
23
40
|
internal func cleanup() -> Void {
|
|
24
41
|
self.resolve = nil
|
|
25
42
|
self.reject = nil
|
|
26
43
|
self.emitEvent = nil
|
|
27
44
|
}
|
|
45
|
+
|
|
46
|
+
/// ASAuthorizationController failure: a cancellation resolves as CANCELLED, anything else rejects.
|
|
47
|
+
internal func finish(authorizationError error: Error) {
|
|
48
|
+
let nsError = error as NSError
|
|
49
|
+
if nsError.domain == ASAuthorizationError.errorDomain,
|
|
50
|
+
nsError.code == ASAuthorizationError.canceled.rawValue {
|
|
51
|
+
succeed(CredentialFactory.fromCancelled())
|
|
52
|
+
} else {
|
|
53
|
+
fail(.invalidResult, error.localizedDescription, error)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Checks what ASWebAuthenticationSession (also used by GoogleSignIn) needs to start: no Guided
|
|
58
|
+
/// Access, the app active (waiting up to `browserActiveTimeout`) and a view controller in a
|
|
59
|
+
/// foreground active scene. A failed start is then reported as BROWSER_UNAVAILABLE without relying
|
|
60
|
+
/// on the error the session returns. Calls `open` with that view controller, or fails.
|
|
61
|
+
internal func whenBrowserCanOpen(_ name: String, _ open: @escaping (UIViewController) -> Void) {
|
|
62
|
+
if UIAccessibility.isGuidedAccessEnabled {
|
|
63
|
+
fail(.browserUnavailable, "\(name) cannot open the browser while Guided Access is enabled")
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
PresentationContext.whenActive(timeout: Self.browserActiveTimeout) { active in
|
|
68
|
+
guard active else {
|
|
69
|
+
self.fail(.browserUnavailable, "\(name) requires the app to be active")
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
guard let presenter = PresentationContext.topViewController() else {
|
|
73
|
+
self.fail(.noActivity, "No view controller available to present \(name)")
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
guard presenter.view.window?.windowScene?.activationState == .foregroundActive else {
|
|
77
|
+
self.fail(.browserUnavailable, "\(name) cannot open the browser: the app window is not in the foreground")
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
open(presenter)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// How long to wait for the app to become active before giving up on opening the browser.
|
|
85
|
+
private static let browserActiveTimeout: TimeInterval = 2
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Finds where to present the sign in UI. Must be called on the main thread.
|
|
89
|
+
enum PresentationContext {
|
|
90
|
+
|
|
91
|
+
static func activeWindowScene() -> UIWindowScene? {
|
|
92
|
+
let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
|
|
93
|
+
return scenes.first { $0.activationState == .foregroundActive }
|
|
94
|
+
?? scenes.first { $0.activationState == .foregroundInactive }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
static func activeWindow() -> UIWindow? {
|
|
98
|
+
guard let scene = activeWindowScene() else { return nil }
|
|
99
|
+
return scene.windows.first { $0.isKeyWindow }
|
|
100
|
+
?? scene.windows.first { !$0.isHidden && $0.windowLevel == .normal }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// The top most view controller that is attached to a window.
|
|
104
|
+
static func topViewController() -> UIViewController? {
|
|
105
|
+
var top = activeWindow()?.rootViewController
|
|
106
|
+
while let presented = top?.presentedViewController, !presented.isBeingDismissed {
|
|
107
|
+
top = presented
|
|
108
|
+
}
|
|
109
|
+
guard let viewController = top, viewController.viewIfLoaded?.window != nil else {
|
|
110
|
+
return nil
|
|
111
|
+
}
|
|
112
|
+
return viewController
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
static func anchor() -> ASPresentationAnchor {
|
|
116
|
+
if let window = activeWindow() {
|
|
117
|
+
return window
|
|
118
|
+
}
|
|
119
|
+
if let scene = activeWindowScene() {
|
|
120
|
+
return UIWindow(windowScene: scene)
|
|
121
|
+
}
|
|
122
|
+
return ASPresentationAnchor()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Calls completion(true) once the app is active, or completion(false) after the timeout.
|
|
126
|
+
static func whenActive(timeout: TimeInterval, _ completion: @escaping (Bool) -> Void) {
|
|
127
|
+
if UIApplication.shared.applicationState == .active {
|
|
128
|
+
completion(true)
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
var observer: NSObjectProtocol?
|
|
133
|
+
var finished = false
|
|
134
|
+
let finish: (Bool) -> Void = { active in
|
|
135
|
+
if finished { return }
|
|
136
|
+
finished = true
|
|
137
|
+
if let observer = observer {
|
|
138
|
+
NotificationCenter.default.removeObserver(observer)
|
|
139
|
+
}
|
|
140
|
+
completion(active)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
observer = NotificationCenter.default.addObserver(
|
|
144
|
+
forName: UIApplication.didBecomeActiveNotification,
|
|
145
|
+
object: nil,
|
|
146
|
+
queue: .main
|
|
147
|
+
) { _ in finish(true) }
|
|
148
|
+
|
|
149
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + timeout) {
|
|
150
|
+
finish(UIApplication.shared.applicationState == .active)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
28
153
|
}
|
package/jest/index.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/* global jest */
|
|
2
|
+
/**
|
|
3
|
+
* Jest mock of react-native-oauth-essentials. Every method is a `jest.fn()` resolving a plausible
|
|
4
|
+
* result; override it per test with `mockResolvedValueOnce` / `mockRejectedValueOnce`.
|
|
5
|
+
*
|
|
6
|
+
* jest.mock('react-native-oauth-essentials', () =>
|
|
7
|
+
* require('react-native-oauth-essentials/jest')
|
|
8
|
+
* );
|
|
9
|
+
*
|
|
10
|
+
* Plain CommonJS on purpose: the React Native Jest preset does not transform this package.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const CredentialsType = {
|
|
14
|
+
GOOGLE_ID: 'GOOGLE_ID',
|
|
15
|
+
PASSWORD: 'PASSWORD',
|
|
16
|
+
APPLE_ID: 'APPLE_ID',
|
|
17
|
+
WEB_APPLE_ID: 'WEB_APPLE_ID',
|
|
18
|
+
CANCELLED: 'CANCELLED',
|
|
19
|
+
PASSKEY: 'PASSKEY',
|
|
20
|
+
OAUTH: 'OAUTH',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const AppleIdCredentialState = {
|
|
24
|
+
AUTHORIZED: 'AUTHORIZED',
|
|
25
|
+
REVOKED: 'REVOKED',
|
|
26
|
+
NOT_FOUND: 'NOT_FOUND',
|
|
27
|
+
TRANSFERRED: 'TRANSFERRED',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const CredentialError = {
|
|
31
|
+
NO_PLAY_SERVICES_ERROR: 'NO_PLAY_SERVICES_ERROR',
|
|
32
|
+
NO_ACTIVITY_ERROR: 'NO_ACTIVITY_ERROR',
|
|
33
|
+
INVALID_RESULT_ERROR: 'INVALID_RESULT_ERROR',
|
|
34
|
+
NO_SIGN_IN_AVAILABLE: 'NO_SIGN_IN_AVAILABLE',
|
|
35
|
+
SIMULTANEOUS_CALL_ERROR: 'SIMULTANEOUS_CALL_ERROR',
|
|
36
|
+
NOT_SUPPORTED_ERROR: 'NOT_SUPPORTED_ERROR',
|
|
37
|
+
BROWSER_UNAVAILABLE: 'BROWSER_UNAVAILABLE',
|
|
38
|
+
CONFIGURATION_ERROR: 'CONFIGURATION_ERROR',
|
|
39
|
+
NETWORK_ERROR: 'NETWORK_ERROR',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const CODES = new Set(Object.values(CredentialError));
|
|
43
|
+
|
|
44
|
+
function isCredentialError(error, code) {
|
|
45
|
+
if (typeof error !== 'object' || error === null) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
const value = error.code;
|
|
49
|
+
if (typeof value !== 'string' || !CODES.has(value)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return code === undefined || value === code;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const OAUTH_REDIRECT_URI = 'com.example.app://oauth/callback';
|
|
56
|
+
|
|
57
|
+
const googleResult = () => ({
|
|
58
|
+
type: CredentialsType.GOOGLE_ID,
|
|
59
|
+
data: {
|
|
60
|
+
id: 'mock-google-id',
|
|
61
|
+
idToken: 'mock-google-id-token',
|
|
62
|
+
displayName: 'Mock User',
|
|
63
|
+
givenName: 'Mock',
|
|
64
|
+
familyName: 'User',
|
|
65
|
+
email: 'mock.user@example.com',
|
|
66
|
+
profilePictureUri: null,
|
|
67
|
+
phoneNumber: null,
|
|
68
|
+
accessToken: null,
|
|
69
|
+
serverAuthCode: null,
|
|
70
|
+
grantedScopes: null,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const appleResult = () => ({
|
|
75
|
+
type: CredentialsType.APPLE_ID,
|
|
76
|
+
data: {
|
|
77
|
+
idToken: 'mock-apple-id-token',
|
|
78
|
+
authorizationCode: 'mock-apple-authorization-code',
|
|
79
|
+
user: 'mock-apple-user',
|
|
80
|
+
email: 'mock.user@example.com',
|
|
81
|
+
fullName: 'Mock User',
|
|
82
|
+
likelyReal: true,
|
|
83
|
+
state: null,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const passwordResult = () => ({
|
|
88
|
+
type: CredentialsType.PASSWORD,
|
|
89
|
+
data: { username: 'mock.user@example.com', password: 'mock-password' },
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const passkeyResult = () => ({
|
|
93
|
+
type: CredentialsType.PASSKEY,
|
|
94
|
+
data: JSON.stringify({ id: 'mock-passkey', type: 'public-key' }),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const subscription = () => ({ remove: jest.fn() });
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
__esModule: true,
|
|
101
|
+
|
|
102
|
+
// Methods
|
|
103
|
+
googleSignIn: jest.fn(async () => googleResult()),
|
|
104
|
+
googleSilentSignIn: jest.fn(async () => googleResult()),
|
|
105
|
+
signOut: jest.fn(async () => true),
|
|
106
|
+
getPassword: jest.fn(async () => passwordResult()),
|
|
107
|
+
passwordSignIn: jest.fn(async () => true),
|
|
108
|
+
appleSignIn: jest.fn(async () => appleResult()),
|
|
109
|
+
getAppleIdCredentialState: jest.fn(
|
|
110
|
+
async () => AppleIdCredentialState.AUTHORIZED
|
|
111
|
+
),
|
|
112
|
+
hybridSignIn: jest.fn(async () => googleResult()),
|
|
113
|
+
createPassKey: jest.fn(async () => passkeyResult()),
|
|
114
|
+
getPassKey: jest.fn(async () => passkeyResult()),
|
|
115
|
+
cancelPassKeyAutoFill: jest.fn(async () => false),
|
|
116
|
+
oauthSignIn: jest.fn(async (options) => ({
|
|
117
|
+
type: CredentialsType.OAUTH,
|
|
118
|
+
data: {
|
|
119
|
+
code: 'mock-authorization-code',
|
|
120
|
+
state: 'mock-state',
|
|
121
|
+
redirectUri: (options && options.redirectUri) || OAUTH_REDIRECT_URI,
|
|
122
|
+
codeVerifier: 'mock-code-verifier',
|
|
123
|
+
params: { code: 'mock-authorization-code', state: 'mock-state' },
|
|
124
|
+
tokens: null,
|
|
125
|
+
},
|
|
126
|
+
})),
|
|
127
|
+
|
|
128
|
+
// Events
|
|
129
|
+
addCredentialSuccessListener: jest.fn(subscription),
|
|
130
|
+
addCredentialFailureListener: jest.fn(subscription),
|
|
131
|
+
addAppleIdCredentialRevokedListener: jest.fn(subscription),
|
|
132
|
+
|
|
133
|
+
// Errors
|
|
134
|
+
isCredentialError,
|
|
135
|
+
|
|
136
|
+
// Enums
|
|
137
|
+
CredentialsType,
|
|
138
|
+
AppleIdCredentialState,
|
|
139
|
+
CredentialError,
|
|
140
|
+
|
|
141
|
+
// Constants
|
|
142
|
+
GOOGLE_PLAY_SERVICES_SUPPORTED: true,
|
|
143
|
+
PASSWORD_SUPPORTED: true,
|
|
144
|
+
GOOGLE_ID_SUPPORTED: true,
|
|
145
|
+
APPLE_ID_SUPPORTED: true,
|
|
146
|
+
HYBRID_SUPPORTED: true,
|
|
147
|
+
PASSKEYS_SUPPORTED: true,
|
|
148
|
+
OAUTH_REDIRECT_URI,
|
|
149
|
+
};
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
import { TurboModuleRegistry } from 'react-native';
|
|
4
|
-
|
|
4
|
+
import { missingNativeModuleMessage } from "./platform.js";
|
|
5
|
+
// `get` instead of `getEnforcing` to explain Expo Go and missing rebuilds instead of a generic error.
|
|
6
|
+
function getNativeModule() {
|
|
7
|
+
const module = TurboModuleRegistry.get('OauthEssentials');
|
|
8
|
+
if (!module) {
|
|
9
|
+
throw new Error(missingNativeModuleMessage());
|
|
10
|
+
}
|
|
11
|
+
return module;
|
|
12
|
+
}
|
|
13
|
+
export default getNativeModule();
|
|
5
14
|
//# sourceMappingURL=NativeOauthEssentials.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["TurboModuleRegistry","
|
|
1
|
+
{"version":3,"names":["TurboModuleRegistry","missingNativeModuleMessage","getNativeModule","module","get","Error"],"sourceRoot":"../../src","sources":["NativeOauthEssentials.ts"],"mappings":";;AAAA,SAGEA,mBAAmB,QACd,cAAc;AACrB,SAASC,0BAA0B,QAAQ,eAAY;AAwHvD;AACA,SAASC,eAAeA,CAAA,EAAS;EAC/B,MAAMC,MAAM,GAAGH,mBAAmB,CAACI,GAAG,CAAO,iBAAiB,CAAC;EAC/D,IAAI,CAACD,MAAM,EAAE;IACX,MAAM,IAAIE,KAAK,CAACJ,0BAA0B,CAAC,CAAC,CAAC;EAC/C;EACA,OAAOE,MAAM;AACf;AAEA,eAAeD,eAAe,CAAC,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { CredentialError } from "./types.js";
|
|
4
|
+
const CODES = new Set(Object.values(CredentialError));
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Narrows an unknown error (e.g. in a `catch`) to an error rejected by this library, optionally
|
|
8
|
+
* with a specific code.
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* try {
|
|
12
|
+
* await googleSignIn({ iosClientId, webClientId });
|
|
13
|
+
* } catch (e) {
|
|
14
|
+
* if (isCredentialError(e, CredentialError.BROWSER_UNAVAILABLE)) retryLater();
|
|
15
|
+
* else if (isCredentialError(e)) report(e.code, e.message);
|
|
16
|
+
* else throw e;
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function isCredentialError(error, code) {
|
|
21
|
+
if (typeof error !== 'object' || error === null) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const value = error.code;
|
|
25
|
+
if (typeof value !== 'string' || !CODES.has(value)) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return code === undefined || value === code;
|
|
29
|
+
}
|
|
30
|
+
export function createCredentialError(code, message, cause) {
|
|
31
|
+
const error = new Error(message);
|
|
32
|
+
error.code = code;
|
|
33
|
+
if (cause !== undefined) {
|
|
34
|
+
error.cause = cause;
|
|
35
|
+
}
|
|
36
|
+
return error;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["CredentialError","CODES","Set","Object","values","isCredentialError","error","code","value","has","undefined","createCredentialError","message","cause","Error"],"sourceRoot":"../../src","sources":["errors.ts"],"mappings":";;AAAA,SACEA,eAAe,QAGV,YAAS;AAEhB,MAAMC,KAAK,GAAG,IAAIC,GAAG,CAASC,MAAM,CAACC,MAAM,CAACJ,eAAe,CAAC,CAAC;;AAE7D;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASK,iBAAiBA,CAE/BC,KAAc,EAAEC,IAAQ,EAAoC;EAC5D,IAAI,OAAOD,KAAK,KAAK,QAAQ,IAAIA,KAAK,KAAK,IAAI,EAAE;IAC/C,OAAO,KAAK;EACd;EACA,MAAME,KAAK,GAAIF,KAAK,CAAwBC,IAAI;EAChD,IAAI,OAAOC,KAAK,KAAK,QAAQ,IAAI,CAACP,KAAK,CAACQ,GAAG,CAACD,KAAK,CAAC,EAAE;IAClD,OAAO,KAAK;EACd;EACA,OAAOD,IAAI,KAAKG,SAAS,IAAIF,KAAK,KAAKD,IAAI;AAC7C;AAEA,OAAO,SAASI,qBAAqBA,CACnCJ,IAAO,EACPK,OAAe,EACfC,KAAe,EACU;EACzB,MAAMP,KAAK,GAAG,IAAIQ,KAAK,CAACF,OAAO,CAE9B;EACDN,KAAK,CAACC,IAAI,GAAGA,IAAI;EACjB,IAAIM,KAAK,KAAKH,SAAS,EAAE;IACvBJ,KAAK,CAACO,KAAK,GAAGA,KAAK;EACrB;EACA,OAAOP,KAAK;AACd","ignoreList":[]}
|