@rownd/react-native 0.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/LICENSE +21 -0
- package/README.md +213 -0
- package/android/build.gradle +59 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/com/reactnative/ReactNativePackage.java +22 -0
- package/android/src/main/java/com/reactnative/ReactNativeViewManager.java +31 -0
- package/ios/ReactNative.xcodeproj/project.pbxproj +282 -0
- package/ios/ReactNative.xcodeproj/project.xcworkspace/contents.xcworkspacedata +4 -0
- package/ios/ReactNativeViewManager.m +34 -0
- package/lib/commonjs/assets/images/checkmark--filled.svg +12 -0
- package/lib/commonjs/assets/images/email-verify-waiting.svg +36 -0
- package/lib/commonjs/assets/images/phone-verify-waiting.svg +26 -0
- package/lib/commonjs/components/AuthenticatedComponent.js +35 -0
- package/lib/commonjs/components/AuthenticatedComponent.js.map +1 -0
- package/lib/commonjs/components/AutoSigninDialog.js +119 -0
- package/lib/commonjs/components/AutoSigninDialog.js.map +1 -0
- package/lib/commonjs/components/DefaultContext.js +269 -0
- package/lib/commonjs/components/DefaultContext.js.map +1 -0
- package/lib/commonjs/components/GlobalContext.js +340 -0
- package/lib/commonjs/components/GlobalContext.js.map +1 -0
- package/lib/commonjs/components/RowndComponents.js +29 -0
- package/lib/commonjs/components/RowndComponents.js.map +1 -0
- package/lib/commonjs/components/RowndProvider.js +55 -0
- package/lib/commonjs/components/RowndProvider.js.map +1 -0
- package/lib/commonjs/components/SignIn.js +622 -0
- package/lib/commonjs/components/SignIn.js.map +1 -0
- package/lib/commonjs/data/actions.js +26 -0
- package/lib/commonjs/data/actions.js.map +1 -0
- package/lib/commonjs/hooks/api.js +157 -0
- package/lib/commonjs/hooks/api.js.map +1 -0
- package/lib/commonjs/hooks/debounce.js +38 -0
- package/lib/commonjs/hooks/debounce.js.map +1 -0
- package/lib/commonjs/hooks/fingerprint.js +176 -0
- package/lib/commonjs/hooks/fingerprint.js.map +1 -0
- package/lib/commonjs/hooks/index.js +48 -0
- package/lib/commonjs/hooks/index.js.map +1 -0
- package/lib/commonjs/hooks/interval.js +31 -0
- package/lib/commonjs/hooks/interval.js.map +1 -0
- package/lib/commonjs/hooks/nav.js +39 -0
- package/lib/commonjs/hooks/nav.js.map +1 -0
- package/lib/commonjs/hooks/rownd.js +163 -0
- package/lib/commonjs/hooks/rownd.js.map +1 -0
- package/lib/commonjs/index.js +32 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/index.tsx.bak +26 -0
- package/lib/commonjs/types.js +2 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/utils/config.js +28 -0
- package/lib/commonjs/utils/config.js.map +1 -0
- package/lib/commonjs/utils/events.js +57 -0
- package/lib/commonjs/utils/events.js.map +1 -0
- package/lib/commonjs/utils/form.js +46 -0
- package/lib/commonjs/utils/form.js.map +1 -0
- package/lib/commonjs/utils/queue.js +117 -0
- package/lib/commonjs/utils/queue.js.map +1 -0
- package/lib/commonjs/utils/storage.js +15 -0
- package/lib/commonjs/utils/storage.js.map +1 -0
- package/lib/commonjs/utils/tailwind.js +17 -0
- package/lib/commonjs/utils/tailwind.js.map +1 -0
- package/lib/commonjs/utils/tokens.js +35 -0
- package/lib/commonjs/utils/tokens.js.map +1 -0
- package/lib/commonjs/utils/user-data.js +21 -0
- package/lib/commonjs/utils/user-data.js.map +1 -0
- package/lib/module/assets/images/checkmark--filled.svg +12 -0
- package/lib/module/assets/images/email-verify-waiting.svg +36 -0
- package/lib/module/assets/images/phone-verify-waiting.svg +26 -0
- package/lib/module/components/AuthenticatedComponent.js +24 -0
- package/lib/module/components/AuthenticatedComponent.js.map +1 -0
- package/lib/module/components/AutoSigninDialog.js +100 -0
- package/lib/module/components/AutoSigninDialog.js.map +1 -0
- package/lib/module/components/DefaultContext.js +244 -0
- package/lib/module/components/DefaultContext.js.map +1 -0
- package/lib/module/components/GlobalContext.js +318 -0
- package/lib/module/components/GlobalContext.js.map +1 -0
- package/lib/module/components/RowndComponents.js +14 -0
- package/lib/module/components/RowndComponents.js.map +1 -0
- package/lib/module/components/RowndProvider.js +39 -0
- package/lib/module/components/RowndProvider.js.map +1 -0
- package/lib/module/components/SignIn.js +593 -0
- package/lib/module/components/SignIn.js.map +1 -0
- package/lib/module/data/actions.js +19 -0
- package/lib/module/data/actions.js.map +1 -0
- package/lib/module/hooks/api.js +138 -0
- package/lib/module/hooks/api.js.map +1 -0
- package/lib/module/hooks/debounce.js +29 -0
- package/lib/module/hooks/debounce.js.map +1 -0
- package/lib/module/hooks/fingerprint.js +157 -0
- package/lib/module/hooks/fingerprint.js.map +1 -0
- package/lib/module/hooks/index.js +7 -0
- package/lib/module/hooks/index.js.map +1 -0
- package/lib/module/hooks/interval.js +23 -0
- package/lib/module/hooks/interval.js.map +1 -0
- package/lib/module/hooks/nav.js +30 -0
- package/lib/module/hooks/nav.js.map +1 -0
- package/lib/module/hooks/rownd.js +148 -0
- package/lib/module/hooks/rownd.js.map +1 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/index.tsx.bak +26 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utils/config.js +17 -0
- package/lib/module/utils/config.js.map +1 -0
- package/lib/module/utils/events.js +45 -0
- package/lib/module/utils/events.js.map +1 -0
- package/lib/module/utils/form.js +34 -0
- package/lib/module/utils/form.js.map +1 -0
- package/lib/module/utils/queue.js +109 -0
- package/lib/module/utils/queue.js.map +1 -0
- package/lib/module/utils/storage.js +6 -0
- package/lib/module/utils/storage.js.map +1 -0
- package/lib/module/utils/tailwind.js +5 -0
- package/lib/module/utils/tailwind.js.map +1 -0
- package/lib/module/utils/tokens.js +24 -0
- package/lib/module/utils/tokens.js.map +1 -0
- package/lib/module/utils/user-data.js +14 -0
- package/lib/module/utils/user-data.js.map +1 -0
- package/lib/typescript/example2/App.d.ts +11 -0
- package/lib/typescript/src/components/AuthenticatedComponent.d.ts +7 -0
- package/lib/typescript/src/components/AutoSigninDialog.d.ts +1 -0
- package/lib/typescript/src/components/DefaultContext.d.ts +12 -0
- package/lib/typescript/src/components/GlobalContext.d.ts +111 -0
- package/lib/typescript/src/components/RowndComponents.d.ts +1 -0
- package/lib/typescript/src/components/RowndProvider.d.ts +8 -0
- package/lib/typescript/src/components/SignIn.d.ts +1 -0
- package/lib/typescript/src/data/actions.d.ts +20 -0
- package/lib/typescript/src/hooks/api.d.ts +12 -0
- package/lib/typescript/src/hooks/debounce.d.ts +5 -0
- package/lib/typescript/src/hooks/fingerprint.d.ts +12 -0
- package/lib/typescript/src/hooks/index.d.ts +6 -0
- package/lib/typescript/src/hooks/interval.d.ts +2 -0
- package/lib/typescript/src/hooks/nav.d.ts +6 -0
- package/lib/typescript/src/hooks/rownd.d.ts +37 -0
- package/lib/typescript/src/index.d.ts +4 -0
- package/lib/typescript/src/types.d.ts +26 -0
- package/lib/typescript/src/utils/config.d.ts +18 -0
- package/lib/typescript/src/utils/events.d.ts +22 -0
- package/lib/typescript/src/utils/form.d.ts +17 -0
- package/lib/typescript/src/utils/queue.d.ts +21 -0
- package/lib/typescript/src/utils/storage.d.ts +3 -0
- package/lib/typescript/src/utils/tailwind.d.ts +2 -0
- package/lib/typescript/src/utils/tokens.d.ts +4 -0
- package/lib/typescript/src/utils/user-data.d.ts +3 -0
- package/lib/typescript/tailwind.config.d.ts +10 -0
- package/package.json +177 -0
- package/react-native.podspec +19 -0
- package/src/assets/images/checkmark--filled.svg +12 -0
- package/src/assets/images/email-verify-waiting.svg +36 -0
- package/src/assets/images/phone-verify-waiting.svg +26 -0
- package/src/components/AuthenticatedComponent.tsx +30 -0
- package/src/components/AutoSigninDialog.tsx +125 -0
- package/src/components/DefaultContext.tsx +278 -0
- package/src/components/GlobalContext.tsx +485 -0
- package/src/components/RowndComponents.tsx +21 -0
- package/src/components/RowndProvider.tsx +56 -0
- package/src/components/SignIn.tsx +770 -0
- package/src/data/actions.ts +21 -0
- package/src/hooks/api.ts +163 -0
- package/src/hooks/debounce.ts +36 -0
- package/src/hooks/fingerprint.ts +217 -0
- package/src/hooks/index.ts +7 -0
- package/src/hooks/interval.ts +25 -0
- package/src/hooks/nav.tsx +29 -0
- package/src/hooks/rownd.ts +184 -0
- package/src/index.tsx +6 -0
- package/src/index.tsx.bak +26 -0
- package/src/types.ts +27 -0
- package/src/utils/config.ts +36 -0
- package/src/utils/events.ts +54 -0
- package/src/utils/form.tsx +64 -0
- package/src/utils/queue.ts +75 -0
- package/src/utils/storage.ts +7 -0
- package/src/utils/tailwind.ts +6 -0
- package/src/utils/tokens.ts +26 -0
- package/src/utils/user-data.ts +15 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export enum ActionType {
|
|
2
|
+
SET_CONTAINER_VISIBLE = 'SET_CONTAINER_VISIBLE',
|
|
3
|
+
CHANGE_ROUTE = 'CHANGE_ROUTE',
|
|
4
|
+
LOGIN_SUCCESS = 'LOGIN_SUCCESS',
|
|
5
|
+
LOAD_USER = 'LOAD_USER',
|
|
6
|
+
REFRESH_TOKEN = 'REFRESH_TOKEN',
|
|
7
|
+
SIGN_OUT = 'SIGN_OUT',
|
|
8
|
+
UPDATE_LOCAL_ACLS = 'UPDATE_LOCAL_ACLS',
|
|
9
|
+
SET_USER_DATA_FIELD = 'SET_USER_DATA_FIELD',
|
|
10
|
+
SET_USER_DATA = 'SET_USER_DATA',
|
|
11
|
+
SET_REFRESH_USER_DATA = 'SET_REFRESH_USER_DATA',
|
|
12
|
+
LOAD_STATE = 'LOAD_STATE',
|
|
13
|
+
SET_SECTION = 'SET_SECTION',
|
|
14
|
+
SET_IS_SAVING_USER_DATA = 'SET_IS_SAVING_USER_DATA',
|
|
15
|
+
SET_APP_CONFIG = 'SET_APP_CONFIG',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type TAction = {
|
|
19
|
+
type: ActionType;
|
|
20
|
+
payload?: any;
|
|
21
|
+
};
|
package/src/hooks/api.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import ky from 'ky';
|
|
2
|
+
import jwt_decode, { JwtPayload } from 'jwt-decode';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
import { useGlobalContext, GlobalState } from '../components/GlobalContext';
|
|
6
|
+
import AutoQueue from '../utils/queue';
|
|
7
|
+
import { useRef, useEffect } from 'react';
|
|
8
|
+
import { Platform } from 'react-native';
|
|
9
|
+
import { ActionType } from '../data/actions';
|
|
10
|
+
|
|
11
|
+
const packageJson = require(path.join(__dirname, '../../package.json'));
|
|
12
|
+
|
|
13
|
+
type RefreshTokenResp = {
|
|
14
|
+
access_token: string;
|
|
15
|
+
refresh_token: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const refreshQueue = new AutoQueue<RefreshTokenResp>();
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_USER_AGENT = `Rownd SDK for React Native/${packageJson.version} (Language: TypeScript/JavaScript; Platform=${Platform.OS};)`;
|
|
21
|
+
|
|
22
|
+
export default function useApi() {
|
|
23
|
+
const { state, dispatch } = useGlobalContext();
|
|
24
|
+
|
|
25
|
+
const authRef = useRef({
|
|
26
|
+
access_token: state.auth.access_token,
|
|
27
|
+
refresh_token: state.auth.refresh_token,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
authRef.current = {
|
|
32
|
+
access_token: state.auth.access_token,
|
|
33
|
+
refresh_token: state.auth.refresh_token,
|
|
34
|
+
};
|
|
35
|
+
}, [state.auth.access_token, state.auth.refresh_token]);
|
|
36
|
+
|
|
37
|
+
function isNewAccessTokenNeeded(request?: Request) {
|
|
38
|
+
// stateCopy = stateCopy || state;
|
|
39
|
+
// Skip requests that don't need authentication
|
|
40
|
+
if (
|
|
41
|
+
(!!request && !request?.headers.get('authorization')) ||
|
|
42
|
+
!authRef.current?.access_token
|
|
43
|
+
) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const tokenPayload: JwtPayload = jwt_decode(authRef.current?.access_token);
|
|
48
|
+
|
|
49
|
+
// Shave 5 minutes off the token expiration to account for clock skew
|
|
50
|
+
const tokenExpiration = (tokenPayload.exp! - 5 * 60) * 1000;
|
|
51
|
+
if (tokenExpiration > Date.now()) {
|
|
52
|
+
return false; // shouldn't be expired
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function _newAccessTokenFromRefreshToken(
|
|
59
|
+
this: AutoQueue<RefreshTokenResp>,
|
|
60
|
+
stateCopy?: GlobalState
|
|
61
|
+
) {
|
|
62
|
+
stateCopy = stateCopy || state;
|
|
63
|
+
if (this?._cache?.resp) {
|
|
64
|
+
// logger.log('using cached refresh response');
|
|
65
|
+
return this._cache.resp;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
// logger.log('requesting new refresh token');
|
|
70
|
+
const resp: RefreshTokenResp = await ky
|
|
71
|
+
.post(`${stateCopy.config?.apiUrl}/hub/auth/token`, {
|
|
72
|
+
json: {
|
|
73
|
+
refresh_token: stateCopy.auth?.refresh_token,
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
.json();
|
|
77
|
+
|
|
78
|
+
this._cache.resp = resp;
|
|
79
|
+
|
|
80
|
+
// Update local cache ref immediately to prevent stale auth checks
|
|
81
|
+
authRef.current = {
|
|
82
|
+
access_token: resp.access_token,
|
|
83
|
+
refresh_token: resp.refresh_token,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
dispatch({
|
|
87
|
+
type: ActionType.REFRESH_TOKEN,
|
|
88
|
+
payload: resp,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return resp;
|
|
92
|
+
} catch (err) {
|
|
93
|
+
dispatch({
|
|
94
|
+
type: ActionType.SIGN_OUT,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function newAccessTokenFromRefreshToken(
|
|
102
|
+
stateCopy?: GlobalState
|
|
103
|
+
): Promise<RefreshTokenResp> {
|
|
104
|
+
return await refreshQueue.enqueue(
|
|
105
|
+
_newAccessTokenFromRefreshToken.bind(refreshQueue, stateCopy)
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const client = useRef(
|
|
110
|
+
ky.extend({
|
|
111
|
+
prefixUrl: state.config?.apiUrl,
|
|
112
|
+
headers: {
|
|
113
|
+
'Content-Type': 'application/json',
|
|
114
|
+
'User-Agent': DEFAULT_USER_AGENT,
|
|
115
|
+
},
|
|
116
|
+
retry: {
|
|
117
|
+
limit: 2,
|
|
118
|
+
statusCodes: [401, 408, 429, 500, 502, 503, 504],
|
|
119
|
+
},
|
|
120
|
+
hooks: {
|
|
121
|
+
beforeRequest: [
|
|
122
|
+
// Auto-refresh tokens
|
|
123
|
+
async (request) => {
|
|
124
|
+
// Skip requests that don't need authentication
|
|
125
|
+
if (!isNewAccessTokenNeeded(request)) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const tokenResp: RefreshTokenResp =
|
|
130
|
+
await newAccessTokenFromRefreshToken();
|
|
131
|
+
|
|
132
|
+
request.headers.set(
|
|
133
|
+
'Authorization',
|
|
134
|
+
`Bearer ${tokenResp.access_token}`
|
|
135
|
+
);
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
beforeRetry: [
|
|
139
|
+
async ({ request /*, options, error, retryCount*/ }) => {
|
|
140
|
+
// Skip requests that don't need authentication
|
|
141
|
+
if (!isNewAccessTokenNeeded(request)) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const tokenResp: RefreshTokenResp =
|
|
146
|
+
await newAccessTokenFromRefreshToken();
|
|
147
|
+
|
|
148
|
+
request.headers.set(
|
|
149
|
+
'Authorization',
|
|
150
|
+
`Bearer ${tokenResp.access_token}`
|
|
151
|
+
);
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
})
|
|
156
|
+
).current;
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
client,
|
|
160
|
+
newAccessTokenFromRefreshToken,
|
|
161
|
+
isNewAccessTokenNeeded,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { debounce } from 'debounce';
|
|
2
|
+
import { useMemo, useEffect, useRef } from 'react';
|
|
3
|
+
|
|
4
|
+
export default function useDebounce(cb: any, delay: any) {
|
|
5
|
+
const immediate = false;
|
|
6
|
+
const inputsRef = useRef(cb);
|
|
7
|
+
const isMounted = useIsMounted();
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
inputsRef.current = { cb, delay };
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
return useMemo(
|
|
13
|
+
() =>
|
|
14
|
+
debounce(
|
|
15
|
+
(...args: any) => {
|
|
16
|
+
// Don't execute callback, if (1) component in the meanwhile
|
|
17
|
+
// has been unmounted or (2) delay has changed
|
|
18
|
+
if (inputsRef.current.delay === delay && isMounted())
|
|
19
|
+
inputsRef.current.cb(...args);
|
|
20
|
+
},
|
|
21
|
+
delay,
|
|
22
|
+
immediate
|
|
23
|
+
),
|
|
24
|
+
[delay, immediate, isMounted]
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function useIsMounted() {
|
|
29
|
+
const isMountedRef = useRef(true);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
return () => {
|
|
32
|
+
isMountedRef.current = false;
|
|
33
|
+
};
|
|
34
|
+
}, []);
|
|
35
|
+
return () => isMountedRef.current;
|
|
36
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import DeviceInfo from 'react-native-device-info';
|
|
2
|
+
import { useEffect, useCallback } from 'react';
|
|
3
|
+
import useApi from './api';
|
|
4
|
+
import { useGlobalContext } from '../components/GlobalContext';
|
|
5
|
+
import storage from '../utils/storage';
|
|
6
|
+
import { sha256 } from 'react-native-sha256';
|
|
7
|
+
|
|
8
|
+
interface IFingerprint {
|
|
9
|
+
message: string;
|
|
10
|
+
hash: string;
|
|
11
|
+
challenge: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface IChallenge {
|
|
15
|
+
key: string;
|
|
16
|
+
value: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let isFingerprintingInProgress = false;
|
|
20
|
+
|
|
21
|
+
export default function () {
|
|
22
|
+
const { state } = useGlobalContext();
|
|
23
|
+
const { client: api } = useApi();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Computes hashes for all possible lookup fields values coupled with the app ID
|
|
27
|
+
* @param appId
|
|
28
|
+
* @param rawLookupValues
|
|
29
|
+
* @returns array of challenge hashes
|
|
30
|
+
*/
|
|
31
|
+
async function computePossibleChallengeLookupValues(
|
|
32
|
+
appId: string,
|
|
33
|
+
rawLookupValues: string[]
|
|
34
|
+
): Promise<string[]> {
|
|
35
|
+
const lookupHashes = [];
|
|
36
|
+
for (const value of rawLookupValues) {
|
|
37
|
+
lookupHashes.push(await sha256(`${appId}:${value}`));
|
|
38
|
+
}
|
|
39
|
+
return lookupHashes;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Looks for a challenge in storage based on presented lookup values
|
|
44
|
+
*/
|
|
45
|
+
const getChallengeIfPresent = useCallback(
|
|
46
|
+
async (
|
|
47
|
+
appId: string | undefined,
|
|
48
|
+
challengeLookupValues: string[]
|
|
49
|
+
): Promise<IChallenge | null> => {
|
|
50
|
+
if (!appId) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const challenges: Record<string, string[]> = JSON.parse(
|
|
55
|
+
storage.getString('challenges') || '{}'
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
if (!Object.keys(challenges).length) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let challengeKey = '';
|
|
63
|
+
let challengeValue = '';
|
|
64
|
+
for (const value of challengeLookupValues) {
|
|
65
|
+
const hash = await sha256(`${appId}:${value}`);
|
|
66
|
+
const challenge = Object.entries(challenges).find(([, hashes]) =>
|
|
67
|
+
hashes.includes(hash)
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (challenge) {
|
|
71
|
+
challengeKey = hash;
|
|
72
|
+
challengeValue = challenge[0];
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!challengeKey || !challengeValue) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
key: challengeKey,
|
|
83
|
+
value: challengeValue,
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
[]
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const getFingerprint = useCallback(async () => {
|
|
90
|
+
const visitorId = DeviceInfo.getDeviceId();
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
visitorId,
|
|
94
|
+
};
|
|
95
|
+
}, []);
|
|
96
|
+
|
|
97
|
+
const registerFingerprint = useCallback(async () => {
|
|
98
|
+
if (
|
|
99
|
+
!state.auth.access_token ||
|
|
100
|
+
!state.auth.is_verified_user ||
|
|
101
|
+
!state.app.id ||
|
|
102
|
+
isFingerprintingInProgress
|
|
103
|
+
) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
isFingerprintingInProgress = true;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
// Check for existing challenge
|
|
111
|
+
const challengeEntry = await getChallengeIfPresent(
|
|
112
|
+
state.app.id,
|
|
113
|
+
[state.user?.data?.email, state.user?.data?.phone_number].filter(
|
|
114
|
+
Boolean
|
|
115
|
+
)
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const fingerprint = await getFingerprint();
|
|
119
|
+
const payload: IFingerprint = await api
|
|
120
|
+
.post('hub/auth/fingerprints', {
|
|
121
|
+
headers: {
|
|
122
|
+
Authorization: `Bearer ${state.auth.access_token}`,
|
|
123
|
+
},
|
|
124
|
+
json: {
|
|
125
|
+
hash: fingerprint.visitorId,
|
|
126
|
+
challenge: challengeEntry?.value || null, // Might exist from a previous run, in which case this request will be a no-op
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
.json();
|
|
130
|
+
|
|
131
|
+
if (payload.challenge === challengeEntry?.key) {
|
|
132
|
+
// This is a no-op
|
|
133
|
+
console.debug('Fingerprint already registered');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Save the challenge for future sign-ins if we don't have one already or we got a new one from the server
|
|
138
|
+
const challengeLookupHashes = await computePossibleChallengeLookupValues(
|
|
139
|
+
state.app.id,
|
|
140
|
+
[state.user?.data?.email, state.user?.data?.phone_number].filter(
|
|
141
|
+
Boolean
|
|
142
|
+
)
|
|
143
|
+
);
|
|
144
|
+
if (payload.challenge && challengeLookupHashes.length > 0) {
|
|
145
|
+
const challenges = JSON.parse(storage.getString('challenges') || '{}');
|
|
146
|
+
storage.set(
|
|
147
|
+
'challenges',
|
|
148
|
+
JSON.stringify({
|
|
149
|
+
...challenges,
|
|
150
|
+
[payload.challenge]: challengeLookupHashes,
|
|
151
|
+
})
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.warn('Failed to register fingerprint', err);
|
|
156
|
+
} finally {
|
|
157
|
+
isFingerprintingInProgress = false;
|
|
158
|
+
}
|
|
159
|
+
}, [
|
|
160
|
+
api,
|
|
161
|
+
state.app.id,
|
|
162
|
+
state.auth.access_token,
|
|
163
|
+
state.auth.is_verified_user,
|
|
164
|
+
state.user?.data?.email,
|
|
165
|
+
state.user?.data?.phone_number,
|
|
166
|
+
getChallengeIfPresent,
|
|
167
|
+
getFingerprint,
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
const clearFingerprint = useCallback((challenge: string) => {
|
|
171
|
+
const challenges: Record<string, string[]> = JSON.parse(
|
|
172
|
+
storage.getString('challenges') || '{}'
|
|
173
|
+
);
|
|
174
|
+
delete challenges[challenge];
|
|
175
|
+
storage.set('challenges', JSON.stringify(challenges));
|
|
176
|
+
}, []);
|
|
177
|
+
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
if (!state.auth.access_token) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
(async () => {
|
|
184
|
+
// If already fingerprinted, don't try again
|
|
185
|
+
const existingChallenge = await getChallengeIfPresent(
|
|
186
|
+
state.app.id,
|
|
187
|
+
[state.user?.data?.email, state.user?.data?.phone_number].filter(
|
|
188
|
+
Boolean
|
|
189
|
+
)
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// Don't need to re-register a fingerprint if we already have one registered
|
|
193
|
+
if (existingChallenge) {
|
|
194
|
+
console.debug(
|
|
195
|
+
'Found existing challenge, so not requesting fingerprint registration.'
|
|
196
|
+
);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// We have an access token, so we can use it to get the fingerprint
|
|
201
|
+
registerFingerprint();
|
|
202
|
+
})();
|
|
203
|
+
}, [
|
|
204
|
+
getChallengeIfPresent,
|
|
205
|
+
registerFingerprint,
|
|
206
|
+
state.app.id,
|
|
207
|
+
state.auth.access_token,
|
|
208
|
+
state.user?.data?.email,
|
|
209
|
+
state.user?.data?.phone_number,
|
|
210
|
+
]);
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
getFingerprint,
|
|
214
|
+
getChallengeIfPresent,
|
|
215
|
+
clearFingerprint,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import useApi from './api';
|
|
2
|
+
import useDebounce from './debounce';
|
|
3
|
+
import useInterval from './interval';
|
|
4
|
+
import useNav from './nav';
|
|
5
|
+
import useDeviceFingerprint from './fingerprint';
|
|
6
|
+
|
|
7
|
+
export { useApi, useDebounce, useInterval, useNav, useDeviceFingerprint };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
function useInterval(callback: () => void, delay: number | null) {
|
|
4
|
+
const savedCallback = useRef(callback);
|
|
5
|
+
|
|
6
|
+
// Remember the latest callback if it changes.
|
|
7
|
+
useLayoutEffect(() => {
|
|
8
|
+
savedCallback.current = callback;
|
|
9
|
+
}, [callback]);
|
|
10
|
+
|
|
11
|
+
// Set up the interval.
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
// Don't schedule if no delay is specified.
|
|
14
|
+
// Note: 0 is a valid value for delay.
|
|
15
|
+
if (!delay && delay !== 0) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const id = setInterval(() => savedCallback.current(), delay);
|
|
20
|
+
|
|
21
|
+
return () => clearInterval(id);
|
|
22
|
+
}, [delay]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default useInterval;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useGlobalContext } from '../components/GlobalContext';
|
|
2
|
+
import { ActionType } from '../data/actions';
|
|
3
|
+
|
|
4
|
+
interface INavOpts {
|
|
5
|
+
route?: string;
|
|
6
|
+
hide?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function useNav() {
|
|
10
|
+
const { dispatch } = useGlobalContext();
|
|
11
|
+
|
|
12
|
+
return ({ route, hide }: INavOpts) => {
|
|
13
|
+
if (hide) {
|
|
14
|
+
dispatch({
|
|
15
|
+
type: ActionType.SET_CONTAINER_VISIBLE,
|
|
16
|
+
payload: {
|
|
17
|
+
isVisible: false,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
} else if (route) {
|
|
21
|
+
dispatch({
|
|
22
|
+
type: ActionType.CHANGE_ROUTE,
|
|
23
|
+
payload: {
|
|
24
|
+
current_route: route,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { useGlobalContext } from '../components/GlobalContext';
|
|
2
|
+
import { ActionType } from '../data/actions';
|
|
3
|
+
import useApi from './api';
|
|
4
|
+
import { events, EventType } from '../utils/events';
|
|
5
|
+
|
|
6
|
+
export type TRowndContext = {
|
|
7
|
+
requestSignIn: (opts?: RequestSignInOpts) => void;
|
|
8
|
+
signOut: () => void;
|
|
9
|
+
getAccessToken: (opts?: GetAccessTokenOpts) => Promise<string | null>;
|
|
10
|
+
is_authenticated: boolean;
|
|
11
|
+
is_initializing: boolean;
|
|
12
|
+
access_token: string | null;
|
|
13
|
+
auth: AuthContext;
|
|
14
|
+
user: UserContext;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type AuthContext = {
|
|
18
|
+
access_token: string | null;
|
|
19
|
+
app_id: string | null;
|
|
20
|
+
is_verified_user?: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type UserContext = {
|
|
24
|
+
data: {
|
|
25
|
+
id?: string;
|
|
26
|
+
email?: string | null;
|
|
27
|
+
phone?: string | null;
|
|
28
|
+
[key: string]: any;
|
|
29
|
+
};
|
|
30
|
+
redacted_fields: string[];
|
|
31
|
+
set: (data: Record<string, any>) => Promise<Record<string, any>>;
|
|
32
|
+
setValue: (key: string, value: any) => Promise<Record<string, any>>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type RequestSignInOpts = {
|
|
36
|
+
identifier?: string;
|
|
37
|
+
auto_sign_in?: boolean;
|
|
38
|
+
init_data?: Record<string, any>;
|
|
39
|
+
post_login_redirect?: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type GetAccessTokenOpts = {
|
|
43
|
+
waitForToken?: boolean;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// const RowndContext = createContext<TRowndContext | null>(null);
|
|
47
|
+
|
|
48
|
+
export function useRownd(): TRowndContext {
|
|
49
|
+
const { state, dispatch } = useGlobalContext();
|
|
50
|
+
const { isNewAccessTokenNeeded, newAccessTokenFromRefreshToken } = useApi();
|
|
51
|
+
|
|
52
|
+
const stateCopy = JSON.parse(JSON.stringify(state));
|
|
53
|
+
|
|
54
|
+
const requestSignIn = (opts?: RequestSignInOpts): void => {
|
|
55
|
+
dispatch({
|
|
56
|
+
type: ActionType.CHANGE_ROUTE,
|
|
57
|
+
payload: {
|
|
58
|
+
route: '/account/login',
|
|
59
|
+
options: opts,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const signOut = () => {
|
|
65
|
+
dispatch({
|
|
66
|
+
type: ActionType.SIGN_OUT,
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const getAccessToken = async (
|
|
71
|
+
opts?: GetAccessTokenOpts
|
|
72
|
+
): Promise<string | null> => {
|
|
73
|
+
const { waitForToken } = opts || {};
|
|
74
|
+
let accessToken = state?.auth.access_token;
|
|
75
|
+
|
|
76
|
+
// Wait for an access token to be available if none exists yet
|
|
77
|
+
if (!accessToken && waitForToken) {
|
|
78
|
+
return new Promise((resolve, _reject) => {
|
|
79
|
+
console.debug('auth_wait: waiting for access token');
|
|
80
|
+
const listener = (evt: any) => {
|
|
81
|
+
console.debug('auth_wait: received access token');
|
|
82
|
+
const data = evt.detail;
|
|
83
|
+
resolve(data.access_token);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
events.addEventListener(EventType.AUTH, listener, { once: true });
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (isNewAccessTokenNeeded(undefined)) {
|
|
91
|
+
const resp = await newAccessTokenFromRefreshToken(stateCopy);
|
|
92
|
+
accessToken = resp.access_token;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return accessToken;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const setUserData = (
|
|
99
|
+
data: Record<string, any>
|
|
100
|
+
): Promise<Record<string, any>> => {
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
console.debug('user_data_save_wait: waiting for data to be saved');
|
|
103
|
+
const listener = (evt: any) => {
|
|
104
|
+
console.debug('user_data_save_wait: received data saved event');
|
|
105
|
+
if (evt.error) {
|
|
106
|
+
return reject(evt.error);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
resolve(evt.data);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
events.addEventListener(EventType.USER_DATA_SAVED, listener, {
|
|
113
|
+
once: true,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
dispatch({
|
|
117
|
+
type: ActionType.SET_USER_DATA,
|
|
118
|
+
payload: {
|
|
119
|
+
data,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const setUserDataValue = (
|
|
126
|
+
key: string,
|
|
127
|
+
value: any
|
|
128
|
+
): Promise<Record<string, any>> => {
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
console.debug('user_data_save_wait: waiting for data to be saved');
|
|
131
|
+
const listener = (evt: any) => {
|
|
132
|
+
console.debug('user_data_save_wait: received data saved event');
|
|
133
|
+
if (evt.error) {
|
|
134
|
+
return reject(evt.error);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
resolve(evt.data);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
events.addEventListener(EventType.USER_DATA_SAVED, listener, {
|
|
141
|
+
once: true,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
dispatch({
|
|
145
|
+
type: ActionType.SET_USER_DATA_FIELD,
|
|
146
|
+
payload: {
|
|
147
|
+
field: key,
|
|
148
|
+
value,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (value) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const valueAcl = {
|
|
157
|
+
[key]: {
|
|
158
|
+
shared: true,
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
dispatch({
|
|
163
|
+
type: ActionType.UPDATE_LOCAL_ACLS,
|
|
164
|
+
payload: valueAcl,
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
requestSignIn,
|
|
171
|
+
signOut,
|
|
172
|
+
getAccessToken,
|
|
173
|
+
is_authenticated: !!state.auth.access_token,
|
|
174
|
+
is_initializing: !!state.app.id,
|
|
175
|
+
access_token: state.auth?.access_token,
|
|
176
|
+
auth: state.auth,
|
|
177
|
+
user: {
|
|
178
|
+
data: state.user?.data,
|
|
179
|
+
redacted_fields: state.user?.redacted,
|
|
180
|
+
set: setUserData,
|
|
181
|
+
setValue: setUserDataValue,
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// import { requestSignIn } from './components/SignIn';
|
|
2
|
+
import { RowndProvider } from './components/RowndProvider';
|
|
3
|
+
import { useRownd } from './hooks/rownd';
|
|
4
|
+
import AuthenticatedComponent from './components/AuthenticatedComponent';
|
|
5
|
+
|
|
6
|
+
export { RowndProvider, useRownd, AuthenticatedComponent };
|