@sudobility/building_blocks_rn 0.0.8 → 0.0.11
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/building-blocks-rn-macos.podspec +20 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/src/api/ApiContext.d.ts +10 -0
- package/dist/src/api/ApiContext.js +10 -0
- package/dist/src/app/SudobilityAppRN.d.ts +35 -1
- package/dist/src/app/SudobilityAppRN.js +30 -2
- package/dist/src/components/error/ErrorBoundary.d.ts +40 -0
- package/dist/src/components/error/ErrorBoundary.js +85 -0
- package/dist/src/components/error/index.d.ts +2 -0
- package/dist/src/components/error/index.js +1 -0
- package/dist/src/components/footer/AppFooter.js +10 -1
- package/dist/src/components/header/AppHeader.d.ts +8 -0
- package/dist/src/components/header/AppHeader.js +3 -3
- package/dist/src/components/layout/AppScreenLayout.d.ts +7 -0
- package/dist/src/components/pages/AppSubscriptionPage.js +19 -5
- package/dist/src/components/pages/AppTextScreen.d.ts +8 -0
- package/dist/src/components/pages/AppTextScreen.js +2 -2
- package/dist/src/components/pages/LoginScreen.js +18 -3
- package/dist/src/components/settings/AppearanceSettings.js +18 -14
- package/dist/src/components/settings/LanguagePicker.js +20 -5
- package/dist/src/components/settings/SettingsListScreen.js +4 -4
- package/dist/src/components/subscription/SafeSubscriptionContext.js +9 -0
- package/dist/src/components/subscription/SubscriptionScreen.js +17 -4
- package/dist/src/components/toast/ToastProvider.d.ts +23 -0
- package/dist/src/components/toast/ToastProvider.js +46 -6
- package/dist/src/constants/languages.d.ts +8 -0
- package/dist/src/constants/languages.js +8 -0
- package/dist/src/hooks/useResponsive.d.ts +25 -0
- package/dist/src/hooks/useResponsive.js +23 -0
- package/dist/src/i18n/index.d.ts +6 -3
- package/dist/src/i18n/index.js +6 -3
- package/dist/src/native/WebAuth.d.ts +3 -0
- package/dist/src/native/WebAuth.js +20 -0
- package/dist/src/theme/ThemeContext.d.ts +14 -0
- package/dist/src/theme/ThemeContext.js +35 -7
- package/dist/src/theme/colors.d.ts +6 -2
- package/dist/src/theme/colors.js +6 -2
- package/dist/src/theme/spacing.d.ts +5 -2
- package/dist/src/theme/spacing.js +5 -2
- package/dist/src/theme/typography.d.ts +6 -2
- package/dist/src/theme/typography.js +6 -2
- package/dist/src/utils/styles.d.ts +7 -0
- package/dist/src/utils/styles.js +13 -0
- package/macos/WebAuthModule.h +4 -0
- package/macos/WebAuthModule.m +104 -0
- package/package.json +19 -11
- package/react-native.config.js +11 -0
- package/windows/WebAuthModule.cpp +245 -0
- package/windows/WebAuthModule.h +29 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#import "WebAuthModule.h"
|
|
2
|
+
#import <AuthenticationServices/AuthenticationServices.h>
|
|
3
|
+
#import <CommonCrypto/CommonDigest.h>
|
|
4
|
+
#import <Security/Security.h>
|
|
5
|
+
|
|
6
|
+
@interface WebAuthModule () <ASWebAuthenticationPresentationContextProviding>
|
|
7
|
+
@property (nonatomic, strong) ASWebAuthenticationSession *authSession;
|
|
8
|
+
@end
|
|
9
|
+
|
|
10
|
+
@implementation WebAuthModule
|
|
11
|
+
|
|
12
|
+
RCT_EXPORT_MODULE();
|
|
13
|
+
|
|
14
|
+
+ (BOOL)requiresMainQueueSetup {
|
|
15
|
+
return YES;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
RCT_EXPORT_METHOD(authenticate:(NSString *)urlString
|
|
19
|
+
callbackURLScheme:(NSString *)callbackURLScheme
|
|
20
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
21
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
22
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
23
|
+
NSURL *authURL = [NSURL URLWithString:urlString];
|
|
24
|
+
if (!authURL) {
|
|
25
|
+
reject(@"INVALID_URL", @"Invalid authentication URL", nil);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
self.authSession =
|
|
30
|
+
[[ASWebAuthenticationSession alloc]
|
|
31
|
+
initWithURL:authURL
|
|
32
|
+
callbackURLScheme:callbackURLScheme
|
|
33
|
+
completionHandler:^(NSURL * _Nullable callbackURL,
|
|
34
|
+
NSError * _Nullable error) {
|
|
35
|
+
self.authSession = nil;
|
|
36
|
+
if (error) {
|
|
37
|
+
if (error.code == ASWebAuthenticationSessionErrorCodeCanceledLogin) {
|
|
38
|
+
resolve([NSNull null]);
|
|
39
|
+
} else {
|
|
40
|
+
reject(@"AUTH_ERROR", error.localizedDescription, error);
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (callbackURL) {
|
|
45
|
+
resolve(callbackURL.absoluteString);
|
|
46
|
+
} else {
|
|
47
|
+
resolve([NSNull null]);
|
|
48
|
+
}
|
|
49
|
+
}];
|
|
50
|
+
|
|
51
|
+
self.authSession.presentationContextProvider = self;
|
|
52
|
+
self.authSession.prefersEphemeralWebBrowserSession = NO;
|
|
53
|
+
|
|
54
|
+
if (![self.authSession start]) {
|
|
55
|
+
self.authSession = nil;
|
|
56
|
+
reject(@"SESSION_ERROR",
|
|
57
|
+
@"Failed to start authentication session", nil);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
RCT_EXPORT_METHOD(generateCodeVerifier:(RCTPromiseResolveBlock)resolve
|
|
63
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
64
|
+
uint8_t randomBytes[32];
|
|
65
|
+
OSStatus status = SecRandomCopyBytes(kSecRandomDefault, 32, randomBytes);
|
|
66
|
+
if (status != errSecSuccess) {
|
|
67
|
+
reject(@"RANDOM_ERROR", @"Failed to generate random bytes", nil);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
NSData *data = [NSData dataWithBytes:randomBytes length:32];
|
|
72
|
+
NSString *base64 = [data base64EncodedStringWithOptions:0];
|
|
73
|
+
NSString *base64url = base64;
|
|
74
|
+
base64url = [base64url stringByReplacingOccurrencesOfString:@"+" withString:@"-"];
|
|
75
|
+
base64url = [base64url stringByReplacingOccurrencesOfString:@"/" withString:@"_"];
|
|
76
|
+
base64url = [base64url stringByReplacingOccurrencesOfString:@"=" withString:@""];
|
|
77
|
+
resolve(base64url);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
RCT_EXPORT_METHOD(sha256:(NSString *)input
|
|
81
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
82
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
83
|
+
NSData *data = [input dataUsingEncoding:NSUTF8StringEncoding];
|
|
84
|
+
uint8_t digest[CC_SHA256_DIGEST_LENGTH];
|
|
85
|
+
CC_SHA256(data.bytes, (CC_LONG)data.length, digest);
|
|
86
|
+
|
|
87
|
+
// Base64url encode
|
|
88
|
+
NSData *hashData = [NSData dataWithBytes:digest length:CC_SHA256_DIGEST_LENGTH];
|
|
89
|
+
NSString *base64 = [hashData base64EncodedStringWithOptions:0];
|
|
90
|
+
NSString *base64url = base64;
|
|
91
|
+
base64url = [base64url stringByReplacingOccurrencesOfString:@"+" withString:@"-"];
|
|
92
|
+
base64url = [base64url stringByReplacingOccurrencesOfString:@"/" withString:@"_"];
|
|
93
|
+
base64url = [base64url stringByReplacingOccurrencesOfString:@"=" withString:@""];
|
|
94
|
+
resolve(base64url);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#pragma mark - ASWebAuthenticationPresentationContextProviding
|
|
98
|
+
|
|
99
|
+
- (ASPresentationAnchor)presentationAnchorForWebAuthenticationSession:
|
|
100
|
+
(ASWebAuthenticationSession *)session {
|
|
101
|
+
return NSApp.keyWindow ?: NSApp.windows.firstObject;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@end
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sudobility/building_blocks_rn",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"description": "Higher-level shared UI building blocks for Sudobility React Native apps",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
22
|
"dist",
|
|
23
|
+
"macos",
|
|
24
|
+
"windows",
|
|
25
|
+
"react-native.config.js",
|
|
26
|
+
"building-blocks-rn-macos.podspec",
|
|
23
27
|
"README.md"
|
|
24
28
|
],
|
|
25
29
|
"scripts": {
|
|
@@ -27,7 +31,7 @@
|
|
|
27
31
|
"dev": "tsc -p tsconfig.build.json --watch",
|
|
28
32
|
"clean": "rm -rf dist",
|
|
29
33
|
"typecheck": "tsc --noEmit",
|
|
30
|
-
"test": "
|
|
34
|
+
"test": "vitest run",
|
|
31
35
|
"lint": "eslint . --ext ts,tsx --ignore-pattern 'dist' --ignore-pattern 'node_modules' --report-unused-disable-directives --max-warnings 0",
|
|
32
36
|
"lint:fix": "eslint . --ext ts,tsx --ignore-pattern 'dist' --ignore-pattern 'node_modules' --fix",
|
|
33
37
|
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json}\"",
|
|
@@ -66,6 +70,13 @@
|
|
|
66
70
|
},
|
|
67
71
|
"devDependencies": {
|
|
68
72
|
"@eslint/js": "^9.38.0",
|
|
73
|
+
"@react-native-async-storage/async-storage": "2.2.0",
|
|
74
|
+
"@react-navigation/native": "^7.1.28",
|
|
75
|
+
"@react-navigation/native-stack": "^7.10.1",
|
|
76
|
+
"@sudobility/types": "^1.9.54",
|
|
77
|
+
"@tanstack/react-query": "^5.90.19",
|
|
78
|
+
"@types/react": "~19.1.0",
|
|
79
|
+
"@types/react-dom": "^19.2.3",
|
|
69
80
|
"@typescript-eslint/eslint-plugin": "^8.44.1",
|
|
70
81
|
"@typescript-eslint/parser": "^8.44.1",
|
|
71
82
|
"eslint": "^9.38.0",
|
|
@@ -73,19 +84,16 @@
|
|
|
73
84
|
"eslint-plugin-prettier": "^5.5.4",
|
|
74
85
|
"eslint-plugin-react-hooks": "^7.0.0",
|
|
75
86
|
"eslint-plugin-react-refresh": "^0.4.0",
|
|
87
|
+
"i18next": "^25.8.0",
|
|
88
|
+
"jsdom": "^28.1.0",
|
|
76
89
|
"prettier": "^3.6.2",
|
|
77
|
-
"@types/react": "~19.1.0",
|
|
78
|
-
"typescript": "~5.9.2",
|
|
79
90
|
"react": "19.1.0",
|
|
91
|
+
"react-dom": "19.1.0",
|
|
92
|
+
"react-i18next": "^16.5.3",
|
|
80
93
|
"react-native": "0.81.5",
|
|
81
94
|
"react-native-safe-area-context": "~5.6.0",
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"@react-native-async-storage/async-storage": "2.2.0",
|
|
85
|
-
"i18next": "^25.8.0",
|
|
86
|
-
"react-i18next": "^16.5.3",
|
|
87
|
-
"@tanstack/react-query": "^5.90.19",
|
|
88
|
-
"@sudobility/types": "^1.9.53"
|
|
95
|
+
"typescript": "~5.9.2",
|
|
96
|
+
"vitest": "^4.0.18"
|
|
89
97
|
},
|
|
90
98
|
"publishConfig": {
|
|
91
99
|
"access": "public"
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#include "pch.h"
|
|
2
|
+
#include "WebAuthModule.h"
|
|
3
|
+
|
|
4
|
+
#include <bcrypt.h>
|
|
5
|
+
#include <wincrypt.h>
|
|
6
|
+
#include <shellapi.h>
|
|
7
|
+
#include <winsock2.h>
|
|
8
|
+
#include <ws2tcpip.h>
|
|
9
|
+
|
|
10
|
+
#include <string>
|
|
11
|
+
#include <vector>
|
|
12
|
+
#include <thread>
|
|
13
|
+
#include <regex>
|
|
14
|
+
|
|
15
|
+
#pragma comment(lib, "bcrypt.lib")
|
|
16
|
+
#pragma comment(lib, "crypt32.lib")
|
|
17
|
+
#pragma comment(lib, "ws2_32.lib")
|
|
18
|
+
|
|
19
|
+
namespace BuildingBlocksRN {
|
|
20
|
+
|
|
21
|
+
void WebAuthModule::Initialize(
|
|
22
|
+
winrt::Microsoft::ReactNative::ReactContext const &reactContext) noexcept {
|
|
23
|
+
m_reactContext = reactContext;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
std::string WebAuthModule::Base64UrlEncode(const std::vector<uint8_t> &data) {
|
|
27
|
+
DWORD base64Len = 0;
|
|
28
|
+
CryptBinaryToStringA(data.data(), static_cast<DWORD>(data.size()),
|
|
29
|
+
CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, nullptr,
|
|
30
|
+
&base64Len);
|
|
31
|
+
|
|
32
|
+
std::string base64(base64Len, '\0');
|
|
33
|
+
CryptBinaryToStringA(data.data(), static_cast<DWORD>(data.size()),
|
|
34
|
+
CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF,
|
|
35
|
+
base64.data(), &base64Len);
|
|
36
|
+
base64.resize(base64Len);
|
|
37
|
+
|
|
38
|
+
// Convert base64 to base64url
|
|
39
|
+
std::string result;
|
|
40
|
+
result.reserve(base64.size());
|
|
41
|
+
for (char c : base64) {
|
|
42
|
+
if (c == '+')
|
|
43
|
+
result += '-';
|
|
44
|
+
else if (c == '/')
|
|
45
|
+
result += '_';
|
|
46
|
+
else if (c == '=')
|
|
47
|
+
continue; // strip padding
|
|
48
|
+
else
|
|
49
|
+
result += c;
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
void WebAuthModule::generateCodeVerifier(
|
|
55
|
+
React::ReactPromise<std::string> result) noexcept {
|
|
56
|
+
std::vector<uint8_t> randomBytes(32);
|
|
57
|
+
NTSTATUS status =
|
|
58
|
+
BCryptGenRandom(nullptr, randomBytes.data(),
|
|
59
|
+
static_cast<ULONG>(randomBytes.size()),
|
|
60
|
+
BCRYPT_USE_SYSTEM_PREFERRED_RNG);
|
|
61
|
+
if (!BCRYPT_SUCCESS(status)) {
|
|
62
|
+
result.Reject(React::ReactError{
|
|
63
|
+
"RANDOM_ERROR", "Failed to generate random bytes"});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
result.Resolve(Base64UrlEncode(randomBytes));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
void WebAuthModule::sha256(std::string input,
|
|
71
|
+
React::ReactPromise<std::string> result) noexcept {
|
|
72
|
+
BCRYPT_ALG_HANDLE hAlg = nullptr;
|
|
73
|
+
BCRYPT_HASH_HANDLE hHash = nullptr;
|
|
74
|
+
NTSTATUS status;
|
|
75
|
+
|
|
76
|
+
status = BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_SHA256_ALGORITHM, nullptr,
|
|
77
|
+
0);
|
|
78
|
+
if (!BCRYPT_SUCCESS(status)) {
|
|
79
|
+
result.Reject(
|
|
80
|
+
React::ReactError{"HASH_ERROR", "Failed to open algorithm provider"});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
status = BCryptCreateHash(hAlg, &hHash, nullptr, 0, nullptr, 0, 0);
|
|
85
|
+
if (!BCRYPT_SUCCESS(status)) {
|
|
86
|
+
BCryptCloseAlgorithmProvider(hAlg, 0);
|
|
87
|
+
result.Reject(
|
|
88
|
+
React::ReactError{"HASH_ERROR", "Failed to create hash"});
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
status = BCryptHashData(hHash,
|
|
93
|
+
reinterpret_cast<PUCHAR>(
|
|
94
|
+
const_cast<char *>(input.data())),
|
|
95
|
+
static_cast<ULONG>(input.size()), 0);
|
|
96
|
+
if (!BCRYPT_SUCCESS(status)) {
|
|
97
|
+
BCryptDestroyHash(hHash);
|
|
98
|
+
BCryptCloseAlgorithmProvider(hAlg, 0);
|
|
99
|
+
result.Reject(
|
|
100
|
+
React::ReactError{"HASH_ERROR", "Failed to hash data"});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
std::vector<uint8_t> hashValue(32); // SHA-256 = 32 bytes
|
|
105
|
+
status = BCryptFinishHash(hHash, hashValue.data(),
|
|
106
|
+
static_cast<ULONG>(hashValue.size()), 0);
|
|
107
|
+
BCryptDestroyHash(hHash);
|
|
108
|
+
BCryptCloseAlgorithmProvider(hAlg, 0);
|
|
109
|
+
|
|
110
|
+
if (!BCRYPT_SUCCESS(status)) {
|
|
111
|
+
result.Reject(
|
|
112
|
+
React::ReactError{"HASH_ERROR", "Failed to finish hash"});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
result.Resolve(Base64UrlEncode(hashValue));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
void WebAuthModule::authenticate(
|
|
120
|
+
std::string url, std::string callbackScheme,
|
|
121
|
+
React::ReactPromise<React::JSValue> result) noexcept {
|
|
122
|
+
// Launch authentication on a background thread to avoid blocking UI
|
|
123
|
+
std::thread([url = std::move(url), callbackScheme = std::move(callbackScheme),
|
|
124
|
+
result = std::move(result)]() mutable {
|
|
125
|
+
// Initialize Winsock for this thread
|
|
126
|
+
WSADATA wsaData;
|
|
127
|
+
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
|
|
128
|
+
result.Reject(
|
|
129
|
+
React::ReactError{"SOCKET_ERROR", "Failed to initialize Winsock"});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Create a TCP listener on a random port
|
|
134
|
+
SOCKET listenSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
|
135
|
+
if (listenSock == INVALID_SOCKET) {
|
|
136
|
+
WSACleanup();
|
|
137
|
+
result.Reject(
|
|
138
|
+
React::ReactError{"SOCKET_ERROR", "Failed to create socket"});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
sockaddr_in addr{};
|
|
143
|
+
addr.sin_family = AF_INET;
|
|
144
|
+
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
145
|
+
addr.sin_port = 0; // OS picks a free port
|
|
146
|
+
|
|
147
|
+
if (bind(listenSock, reinterpret_cast<sockaddr *>(&addr), sizeof(addr)) ==
|
|
148
|
+
SOCKET_ERROR) {
|
|
149
|
+
closesocket(listenSock);
|
|
150
|
+
WSACleanup();
|
|
151
|
+
result.Reject(
|
|
152
|
+
React::ReactError{"SOCKET_ERROR", "Failed to bind socket"});
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Get the assigned port
|
|
157
|
+
int addrLen = sizeof(addr);
|
|
158
|
+
getsockname(listenSock, reinterpret_cast<sockaddr *>(&addr), &addrLen);
|
|
159
|
+
int port = ntohs(addr.sin_port);
|
|
160
|
+
|
|
161
|
+
if (listen(listenSock, 1) == SOCKET_ERROR) {
|
|
162
|
+
closesocket(listenSock);
|
|
163
|
+
WSACleanup();
|
|
164
|
+
result.Reject(
|
|
165
|
+
React::ReactError{"SOCKET_ERROR", "Failed to listen on socket"});
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Build the redirect URI and full auth URL
|
|
170
|
+
std::string redirectUri =
|
|
171
|
+
"http://127.0.0.1:" + std::to_string(port) + "/callback";
|
|
172
|
+
|
|
173
|
+
// Append redirect_uri to the auth URL
|
|
174
|
+
std::string fullUrl = url;
|
|
175
|
+
if (fullUrl.find('?') != std::string::npos)
|
|
176
|
+
fullUrl += "&redirect_uri=" + redirectUri;
|
|
177
|
+
else
|
|
178
|
+
fullUrl += "?redirect_uri=" + redirectUri;
|
|
179
|
+
|
|
180
|
+
// Open browser
|
|
181
|
+
std::wstring wUrl(fullUrl.begin(), fullUrl.end());
|
|
182
|
+
ShellExecuteW(nullptr, L"open", wUrl.c_str(), nullptr, nullptr,
|
|
183
|
+
SW_SHOWNORMAL);
|
|
184
|
+
|
|
185
|
+
// Set a timeout on the listen socket (60 seconds)
|
|
186
|
+
DWORD timeout = 60000;
|
|
187
|
+
setsockopt(listenSock, SOL_SOCKET, SO_RCVTIMEO,
|
|
188
|
+
reinterpret_cast<const char *>(&timeout), sizeof(timeout));
|
|
189
|
+
|
|
190
|
+
// Accept the redirect connection
|
|
191
|
+
SOCKET clientSock = accept(listenSock, nullptr, nullptr);
|
|
192
|
+
if (clientSock == INVALID_SOCKET) {
|
|
193
|
+
closesocket(listenSock);
|
|
194
|
+
WSACleanup();
|
|
195
|
+
result.Resolve(React::JSValue{nullptr}); // timeout / cancelled
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Read the HTTP request
|
|
200
|
+
char buf[4096];
|
|
201
|
+
int bytesRead = recv(clientSock, buf, sizeof(buf) - 1, 0);
|
|
202
|
+
if (bytesRead <= 0) {
|
|
203
|
+
closesocket(clientSock);
|
|
204
|
+
closesocket(listenSock);
|
|
205
|
+
WSACleanup();
|
|
206
|
+
result.Resolve(React::JSValue{nullptr});
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
buf[bytesRead] = '\0';
|
|
210
|
+
|
|
211
|
+
// Send a simple HTML response to close the browser tab
|
|
212
|
+
const char *response =
|
|
213
|
+
"HTTP/1.1 200 OK\r\n"
|
|
214
|
+
"Content-Type: text/html\r\n"
|
|
215
|
+
"Connection: close\r\n\r\n"
|
|
216
|
+
"<html><body><p>Authentication complete. You may close this "
|
|
217
|
+
"tab.</p><script>window.close()</script></body></html>";
|
|
218
|
+
send(clientSock, response, static_cast<int>(strlen(response)), 0);
|
|
219
|
+
closesocket(clientSock);
|
|
220
|
+
closesocket(listenSock);
|
|
221
|
+
WSACleanup();
|
|
222
|
+
|
|
223
|
+
// Parse the request line to extract the callback URL
|
|
224
|
+
std::string request(buf);
|
|
225
|
+
// Extract GET /callback?... HTTP/1.1
|
|
226
|
+
std::regex requestLineRegex(R"(GET\s+(/\S+)\s+HTTP)");
|
|
227
|
+
std::smatch match;
|
|
228
|
+
if (std::regex_search(request, match, requestLineRegex)) {
|
|
229
|
+
std::string path = match[1].str();
|
|
230
|
+
auto qPos = path.find('?');
|
|
231
|
+
if (qPos == std::string::npos) {
|
|
232
|
+
result.Resolve(React::JSValue{nullptr});
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
// Reconstruct the full callback URL
|
|
236
|
+
std::string callbackUrl =
|
|
237
|
+
callbackScheme + "://callback" + path.substr(qPos);
|
|
238
|
+
result.Resolve(React::JSValue{callbackUrl});
|
|
239
|
+
} else {
|
|
240
|
+
result.Resolve(React::JSValue{nullptr});
|
|
241
|
+
}
|
|
242
|
+
}).detach();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
} // namespace BuildingBlocksRN
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include "pch.h"
|
|
4
|
+
#include "NativeModules.h"
|
|
5
|
+
#include <winrt/Microsoft.ReactNative.h>
|
|
6
|
+
|
|
7
|
+
namespace BuildingBlocksRN {
|
|
8
|
+
|
|
9
|
+
REACT_MODULE(WebAuthModule)
|
|
10
|
+
struct WebAuthModule {
|
|
11
|
+
REACT_INIT(Initialize)
|
|
12
|
+
void Initialize(winrt::Microsoft::ReactNative::ReactContext const &reactContext) noexcept;
|
|
13
|
+
|
|
14
|
+
REACT_METHOD(generateCodeVerifier)
|
|
15
|
+
void generateCodeVerifier(React::ReactPromise<std::string> result) noexcept;
|
|
16
|
+
|
|
17
|
+
REACT_METHOD(sha256)
|
|
18
|
+
void sha256(std::string input, React::ReactPromise<std::string> result) noexcept;
|
|
19
|
+
|
|
20
|
+
REACT_METHOD(authenticate)
|
|
21
|
+
void authenticate(std::string url, std::string callbackScheme,
|
|
22
|
+
React::ReactPromise<React::JSValue> result) noexcept;
|
|
23
|
+
|
|
24
|
+
private:
|
|
25
|
+
static std::string Base64UrlEncode(const std::vector<uint8_t> &data);
|
|
26
|
+
winrt::Microsoft::ReactNative::ReactContext m_reactContext;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
} // namespace BuildingBlocksRN
|