Skip to content

Commit 20505ff

Browse files
authored
Support custom fetch function #367
ref DEV-2890
2 parents 612ba02 + e54ae63 commit 20505ff

File tree

8 files changed

+131
-87
lines changed

8 files changed

+131
-87
lines changed

example/capacitor/src/pages/Home.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,15 @@ function isPlatformWeb(): boolean {
9393
return Capacitor.getPlatform() === "web";
9494
}
9595

96+
async function myfetch(
97+
input: URL | RequestInfo,
98+
init?: RequestInit
99+
): Promise<Response> {
100+
const request = new Request(input, init);
101+
request.headers.set("x-custom-header", "42");
102+
return fetch(request);
103+
}
104+
96105
function AuthgearDemo() {
97106
const [isAlertOpen, setIsAlertOpen] = useState(false);
98107
const [alertHeader, setAlertHeader] = useState("");
@@ -228,6 +237,7 @@ function AuthgearDemo() {
228237
endpoint,
229238
sessionType: "refresh_token",
230239
isSSOEnabled,
240+
fetch: myfetch,
231241
});
232242
} else {
233243
await authgearCapacitor.configure({
@@ -246,6 +256,7 @@ function AuthgearDemo() {
246256
: undefined,
247257
isSSOEnabled,
248258
preAuthenticatedURLEnabled,
259+
fetch: myfetch,
249260
});
250261
}
251262
await postConfigure();

example/reactnative/src/screens/MainScreen.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,16 @@ const biometricOptions: BiometricOptions = {
145145
},
146146
};
147147

148+
async function myfetch(
149+
input: URL | RequestInfo,
150+
init?: RequestInit,
151+
): Promise<Response> {
152+
// @ts-expect-error The Request class in React Native does not take URL in the first argument.
153+
const request = new Request(input, init);
154+
request.headers.set('x-custom-header', '42');
155+
return fetch(request);
156+
}
157+
148158
const HomeScreen: React.FC = () => {
149159
const [initialized, setInitialized] = useState(false);
150160
const [loading, setLoading] = useState(false);
@@ -383,6 +393,7 @@ const HomeScreen: React.FC = () => {
383393
: undefined,
384394
isSSOEnabled,
385395
preAuthenticatedURLEnabled,
396+
fetch: myfetch,
386397
})
387398
.then(() => {
388399
postConfigure();

example/reactweb/src/App.tsx

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,15 @@ function ShowError(props: { error: unknown }) {
8383
return <pre>{JSON.stringify(data, null, 2)}</pre>;
8484
}
8585

86+
async function myfetch(
87+
input: URL | RequestInfo,
88+
init?: RequestInit
89+
): Promise<Response> {
90+
const request = new Request(input, init);
91+
request.headers.set("x-custom-header", "42");
92+
return fetch(request);
93+
}
94+
8695
function Root() {
8796
const initialClientID = readClientID();
8897
const initialEndpoint = readEndpoint();
@@ -94,7 +103,8 @@ function Root() {
94103
const [endpoint, setEndpoint] = useState(initialEndpoint);
95104
const [isSSOEnabled, setIsSSOEnabled] = useState(initialIsSSOEnabled);
96105
const [page, setPage] = useState<string>();
97-
const [authenticationFlowGroup, setAuthenticationflowGroup] = useState<string>("");
106+
const [authenticationFlowGroup, setAuthenticationflowGroup] =
107+
useState<string>("");
98108

99109
const [error, setError] = useState<unknown>(null);
100110
const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
@@ -122,6 +132,7 @@ function Root() {
122132
clientID,
123133
sessionType,
124134
isSSOEnabled,
135+
fetch: myfetch,
125136
})
126137
.then(
127138
() => {
@@ -225,21 +236,24 @@ function Root() {
225236
);
226237
}, []);
227238

228-
const onClickSignIn = useCallback((e: React.MouseEvent<HTMLElement>) => {
229-
e.preventDefault();
230-
e.stopPropagation();
231-
authgear
232-
.startAuthentication({
233-
redirectURI: makeRedirectURI(),
234-
state: "authenticate",
235-
page,
236-
authenticationFlowGroup,
237-
})
238-
.then(
239-
() => {},
240-
(err) => setError(err)
241-
);
242-
}, [page, authenticationFlowGroup]);
239+
const onClickSignIn = useCallback(
240+
(e: React.MouseEvent<HTMLElement>) => {
241+
e.preventDefault();
242+
e.stopPropagation();
243+
authgear
244+
.startAuthentication({
245+
redirectURI: makeRedirectURI(),
246+
state: "authenticate",
247+
page,
248+
authenticationFlowGroup,
249+
})
250+
.then(
251+
() => {},
252+
(err) => setError(err)
253+
);
254+
},
255+
[page, authenticationFlowGroup]
256+
);
243257

244258
const onClickSignInAnonymously = useCallback(
245259
(e: React.MouseEvent<HTMLElement>) => {

packages/authgear-capacitor/src/index.ts

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -119,14 +119,16 @@ export interface ConfigureOptions {
119119
* The UIImplementation.
120120
*/
121121
uiImplementation?: UIImplementation;
122-
}
123122

124-
/**
125-
* @internal
126-
*/
127-
export class _CapacitorAPIClient extends _BaseAPIClient {
128-
_fetchFunction = window.fetch.bind(window);
129-
_requestClass = Request;
123+
/*
124+
* Override the fetch() function the SDK uses to access the endpoint.
125+
* If this is not specified, the global fetch() function is used.
126+
* Note that the fetch() function IS NOT used in the UIImplementation,
127+
* it is only used to access endpoints like the Token endpoint.
128+
* For example, you want to provide a custom fetch() function to include additional headers
129+
* in all requests that are sent to your self-hosted deployment.
130+
*/
131+
fetch?: typeof window.fetch;
130132
}
131133

132134
async function getXDeviceInfo(): Promise<string> {
@@ -151,7 +153,7 @@ export class CapacitorContainer {
151153
/**
152154
* @internal
153155
*/
154-
baseContainer: _BaseContainer<_CapacitorAPIClient>;
156+
baseContainer: _BaseContainer<_BaseAPIClient>;
155157

156158
/**
157159
* @internal
@@ -270,13 +272,14 @@ export class CapacitorContainer {
270272
},
271273
});
272274
this.dpopProvider = dpopProvider;
273-
const apiClient = new _CapacitorAPIClient(dpopProvider);
274275

275-
this.baseContainer = new _BaseContainer<_CapacitorAPIClient>(
276-
o,
277-
apiClient,
278-
this
279-
);
276+
const apiClient = new _BaseAPIClient({
277+
fetch: window.fetch.bind(window),
278+
Request: Request,
279+
dpopProvider,
280+
});
281+
282+
this.baseContainer = new _BaseContainer<_BaseAPIClient>(o, apiClient, this);
280283
this.baseContainer.apiClient._delegate = this;
281284

282285
this.storage = new PersistentContainerStorage();
@@ -373,7 +376,11 @@ export class CapacitorContainer {
373376
const refreshToken = await this.tokenStorage.getRefreshToken(this.name);
374377

375378
this.clientID = options.clientID;
379+
380+
this.baseContainer.apiClient._fetch =
381+
options.fetch ?? window.fetch.bind(window);
376382
this.baseContainer.apiClient.endpoint = options.endpoint;
383+
377384
this.baseContainer.refreshToken = refreshToken ?? undefined;
378385
this.baseContainer.accessToken = undefined;
379386

packages/authgear-core/src/client.ts

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export function _removeTrailingSlash(s: string): string {
2626
/**
2727
* @internal
2828
*/
29-
export abstract class _BaseAPIClient {
29+
export class _BaseAPIClient {
3030
private dpopProvider: DPoPProvider | null;
3131
userAgent?: string;
3232

@@ -47,15 +47,21 @@ export abstract class _BaseAPIClient {
4747
}
4848

4949
// eslint-disable-next-line no-undef
50-
abstract _fetchFunction?: typeof fetch;
51-
50+
_fetch: typeof fetch;
5251
// eslint-disable-next-line no-undef
53-
abstract _requestClass?: typeof Request;
52+
_Request: typeof Request;
5453

5554
_config?: _OIDCConfiguration;
5655

57-
constructor(dpopProvider: DPoPProvider | null) {
58-
this.dpopProvider = dpopProvider;
56+
constructor(options: {
57+
// eslint-disable-next-line no-undef
58+
fetch: typeof fetch;
59+
Request: typeof Request;
60+
dpopProvider: DPoPProvider | null;
61+
}) {
62+
this._fetch = options.fetch;
63+
this._Request = options.Request;
64+
this.dpopProvider = options.dpopProvider;
5965
}
6066

6167
protected async _prepareHeaders(): Promise<Record<string, string>> {
@@ -71,10 +77,6 @@ export abstract class _BaseAPIClient {
7177
}
7278

7379
async _doFetch(request: Request): Promise<Response> {
74-
if (!this._fetchFunction) {
75-
throw new AuthgearError("missing fetchFunction in api client");
76-
}
77-
7880
if (this.dpopProvider != null) {
7981
const dpopJWT = await this.dpopProvider.generateDPoPProof({
8082
htm: request.method,
@@ -85,25 +87,18 @@ export abstract class _BaseAPIClient {
8587
}
8688
}
8789

88-
return this._fetchFunction(request);
90+
return this._fetch(request);
8991
}
9092

9193
async _fetchWithoutRefresh(
9294
url: string,
9395
init?: RequestInit
9496
): Promise<Response> {
95-
if (!this._requestClass) {
96-
throw new AuthgearError("missing requestClass in api client");
97-
}
98-
const request = new this._requestClass(url, init);
97+
const request = new this._Request(url, init);
9998
return this._doFetch(request);
10099
}
101100

102101
async fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
103-
if (this._requestClass == null) {
104-
throw new AuthgearError("missing requestClass in api client");
105-
}
106-
107102
if (this._delegate == null) {
108103
throw new AuthgearError("missing delegate in api client");
109104
}
@@ -113,7 +108,7 @@ export abstract class _BaseAPIClient {
113108
await this._delegate.refreshAccessToken();
114109
}
115110

116-
const request = new this._requestClass(input, init);
111+
const request = new this._Request(input, init);
117112

118113
const headers = await this._prepareHeaders();
119114
for (const key of Object.keys(headers)) {

packages/authgear-react-native/src/index.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -111,14 +111,16 @@ export interface ConfigureOptions {
111111
* The implementation of UIImplementation.
112112
*/
113113
uiImplementation?: UIImplementation;
114-
}
115114

116-
/**
117-
* @internal
118-
*/
119-
export class _ReactNativeAPIClient extends _BaseAPIClient {
120-
_fetchFunction = fetch;
121-
_requestClass = Request;
115+
/*
116+
* Override the fetch() function the SDK uses to access the endpoint.
117+
* If this is not specified, the global fetch() function is used.
118+
* Note that the fetch() function IS NOT used in the UIImplementation,
119+
* it is only used to access endpoints like the Token endpoint.
120+
* For example, you want to provide a custom fetch() function to include additional headers
121+
* in all requests that are sent to your self-hosted deployment.
122+
*/
123+
fetch?: typeof fetch;
122124
}
123125

124126
async function getXDeviceInfo(): Promise<string> {
@@ -143,7 +145,7 @@ export class ReactNativeContainer {
143145
/**
144146
* @internal
145147
*/
146-
baseContainer: _BaseContainer<_ReactNativeAPIClient>;
148+
baseContainer: _BaseContainer<_BaseAPIClient>;
147149

148150
/**
149151
* @internal
@@ -280,13 +282,13 @@ export class ReactNativeContainer {
280282
});
281283

282284
this.dpopProvider = dpopProvider;
283-
const apiClient = new _ReactNativeAPIClient(dpopProvider);
285+
const apiClient = new _BaseAPIClient({
286+
fetch: fetch,
287+
Request: Request,
288+
dpopProvider: dpopProvider,
289+
});
284290

285-
this.baseContainer = new _BaseContainer<_ReactNativeAPIClient>(
286-
o,
287-
apiClient,
288-
this
289-
);
291+
this.baseContainer = new _BaseContainer<_BaseAPIClient>(o, apiClient, this);
290292
this.baseContainer.apiClient._delegate = this;
291293

292294
this.storage = new PersistentContainerStorage();
@@ -395,6 +397,7 @@ export class ReactNativeContainer {
395397
const refreshToken = await this.tokenStorage.getRefreshToken(this.name);
396398

397399
this.clientID = options.clientID;
400+
this.baseContainer.apiClient._fetch = options.fetch ?? fetch;
398401
this.baseContainer.apiClient.endpoint = options.endpoint;
399402
this.baseContainer.refreshToken = refreshToken ?? undefined;
400403
this.baseContainer.accessToken = undefined;

packages/authgear-web/src/client.ts

Lines changed: 0 additions & 19 deletions
This file was deleted.

0 commit comments

Comments
 (0)