diff --git a/packages/react-native-web-examples/pages/image/index.js b/packages/react-native-web-examples/pages/image/index.js
index 5cc756bf4..7d95ae1b7 100644
--- a/packages/react-native-web-examples/pages/image/index.js
+++ b/packages/react-native-web-examples/pages/image/index.js
@@ -15,6 +15,18 @@ const dataBase64Svg =
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0nMjAwJyBoZWlnaHQ9JzIwMCcgZmlsbD0iIzAwMDAwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDEwMCAxMDAiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxnPjxwYXRoIGQ9Ik0yNS44NjcsNDguODUzQzMyLjgwNiw1MC4xNzYsNDYuNDYsNTIuNSw2MS4yMTUsNTIuNWgwLjAwNWM5LjcxLDAsMTguNDAxLTEuMDU3LDI1LjkzOC0yLjkxMyAgIGMwLjE1OS0wLjA0NiwwLjM1LTAuMTM1LDAuNTY1LTAuMTg3YzAuMjgyLTAuMDcyLDAuNTY1LTAuMTY0LDAuODQ0LTAuMjM4YzMuMTg0LTAuOTY0LDIuNTc3LTMuMDUxLDIuMTk5LTMuODUyICAgYy00LjE2Ni03LjcxOS0xNS4wODYtMjMuNDE1LTM1LjAyOC0yMy40MTVjLTIyLjE2OSwwLTMwLjI2MiwxMC42MzUtMzMuMTQsMTkuNTg5QzIyLjU0NSw0Mi4zMzMsMjIuNDA3LDQ3LjEzNSwyNS44NjcsNDguODUzeiAgICBNMjguNjc2LDM4LjAzMmMwLjAxMy0wLjAzNiwwLjYxNC0xLjYyNiwxLjkyMy0xLjAwOGMxLjEzMywwLjUzNSwwLjk2MSwxLjU2MywwLjg4NywxLjg1Yy0wLjAwNywwLjAyNC0wLjAxNCwwLjA0OC0wLjAyMSwwLjA3MyAgIGMwLDAuMDAxLTAuMDAxLDAuMDA0LTAuMDAxLDAuMDA0bDAsMGMtMC4yNDksMC45MjktMC40MDQsMi4wODYtMC4wMTcsMi44NmMwLjE2LDAuMzE5LDAuNDkyLDAuNzY4LDEuNTQyLDAuOTg3bDAuMzY2LDAuMDc3ICAgYzIwLjgxNiw0LjM2LDM2LDIuOTMzLDQ1LjY3OCwwLjYyNmwtMC4wMDQsMC4wMDJjMCwwLDAuMDA1LTAuMDAyLDAuMDA3LTAuMDAzYzAuMjEyLTAuMDUsMC40MjEtMC4xMDEsMC42MjgtMC4xNTIgICBjMC41MDktMC4wNSwxLjE3MywwLjA3OCwxLjM5OSwxYzAuMzUxLDEuNDI0LTAuOTczLDEuODk1LTEuMjE3LDEuOTY5Yy01LjMyNSwxLjI3OS0xMi4yNjYsMi4zMDYtMjAuODM1LDIuMzA3ICAgYy03LjUwNSwwLTE2LjI1NS0wLjc4Ny0yNi4yNTctMi44ODJsLTAuMzY0LTAuMDc3Yy0yLjEyLTAuNDQyLTMuMTExLTEuNjMzLTMuNTY5LTIuNTU1QzI3Ljk4NSw0MS40MjEsMjguMjgxLDM5LjQxNiwyOC42NzYsMzguMDMyICAgeiI+PC9wYXRoPjxjaXJjbGUgY3g9IjEwLjQ5MyIgY3k9IjIzLjQ1NSIgcj0iMC42MTkiPjwvY2lyY2xlPjxwYXRoIGQ9Ik0yLjA4LDI4LjMwOGMwLjY3Ni0wLjE3OCwwLjk4My0wLjM1MiwxLjE3NC0wLjVDNC42OSwyNi42OSw2LjUsMjcuNDgzLDcuNSwyOC4zNTd2MC4wMDJjMCwwLDEuNzExLDEuMjM1LDAuNzM3LDIuMjAyICAgYy0wLjk3NCwwLjk2NS0yLjMxOSwwLjAwNi0yLjMxOSwwLjAwNmwwLjAzNSwwLjAxNmMtMC4zMjctMC4yMDMtMC42LTAuNTYxLTAuNzgtMC41ODRjLTAuMzcsMC4yNi0wLjg3NiwwLjUtMS40NzYsMC41SDMuNyAgIGMwLDAtMS4zNDUsMC43MDksMC4xNzgsMS42NTJjMC4wMDEsMC4wMDEsMC4wMDIsMC4wNzIsMC4wMDQsMC4wNzNjMy45MzksMi4zNDIsOC4yNzEsNS43MDEsOC4yNzEsOC44OCAgIGMwLDAuNjkxLDAuMiwxNy4wNDIsMTcuNjI2LDI0LjczOWwwLjk2NywwLjQ0MmwtMC4xLDEuMDU5Yy0wLjQyMSw0LjM5LDEuMTQ1LDEwLjE5MSwxMC45OTMsMTIuODg4bDAuMTEzLDAuMDM4ICAgYzAuMDY3LDAuMDIzLDYuNzMyLDIuNDI5LDEwLjkwNywyLjQyOWMxLjU4NCwwLDIuMTU1LTAuMzUyLDIuMjQzLTAuNTYxYzAuMDg1LTAuMjAyLDAuNjEyLTIuMTY0LTYuMzMyLTkuMzg3bDAuMDAyLTAuMTgzICAgYzAsMC0yLjQ3Ny0zLjA3LDEuNTMzLTMuMDdjMC4wMSwwLDAuMDE5LDAsMC4wMjksMGMxLjI4NSwwLDIuNjA4LDAuMjE1LDMuOTgsMC4xODRjNC43NzEtMC4xMTcsOS4zMTYtMC40MjUsMTMuNTA2LTEuMDk2ICAgbDAuNDc0LTAuMDI4bDAuNjY4LDAuMTU4YzkuNjUxLDQuOTQ4LDE2LjczOCw3LjcxNiwxOS43MzgsNy43MTZ2MC4wMDZjMCwwLDAuMTY0LDAuMDExLDAuMjMsMC4wMDQgICBjLTAuMTg5LTAuNzIzLTIuMjMtMi44LTcuMjMtOS4wNzl2MC4wMjFjMCwwLTEuNTEyLTEuNjU4LDAuNzk3LTIuNjUzYzAuMDYzLTAuMDI2LDAuMDA4LDAuMDIzLDAuMDYtMC4wMDEgICBjOC42MzktMy41MDksMTMuNTAxLTguMjA0LDE1LjQxMS0xMS43NzVjMS4xNDUtMi4xMjksMC4yMDYtMi43ODQtMC42NTktMi45NzZjLTAuMzE3LTAuMDM4LTAuNjM0LTAuMDYyLTAuOTEyLTAuMDYyICAgYy0wLjIwNSwwLTAuMzc5LDAuMDEtMC41MjgsMC4wMjdsLTMuMTQzLDEuMjE0QzgzLjczMiw1My45MjYsNzMuMjE4LDU1LjUsNjEuMjIsNTUuNWMtMC4wMDIsMC0wLjAwNSwwLTAuMDA1LDAgICBjLTE1LjEyOCwwLTI5LjEwMS0yLjQzMi0zNi4wODMtMy43NzFsLTAuMTczLTAuMTExbC0wLjE2LTAuMTI2Yy01Ljg1OC0yLjY4MS01LjEzNy0xMC4yMDItNS4xMDMtMTAuNTE5bDAuMDYtMC4zICAgYzAuODk1LTIuODM4LDIuNDY3LTYuMzUyLDUuMjEzLTkuNzE5Yy0xLjgwOC0xLjM2OS00LjU5LTQuMTg4LTQuNDMtOC40OTRjMC4wNDYtMS4yNDQtMC40ODYtMi41MDgtMS40OTgtMy41NTkgICBjLTEuNDk4LTEuNTU1LTMuNzg1LTIuNDQ2LTYuMjc0LTIuNDQ2Yy0xLjc3LDAtMy41NTMsMC40NDItNS4yOTMsMS4zMTRjLTQuMDYxLDIuMDM1LTQuODU1LDQuNzM2LTUuNjkyLDcuNTk2ICAgYy0wLjEzNiwwLjQ2OC0wLjI4NCwwLjkzOS0wLjQzOCwxLjQxYy0wLjAwNiwwLjAxOS0wLjAyMiwwLjAzNS0wLjAyOCwwLjA1NkMwLjgzMywyOC40MjMsMS42OTEsMjguMzksMi4wOCwyOC4zMDh6IE0xMC40OTMsMTkuOTA4ICAgYzEuOTU2LDAsMy41NDgsMS41OTEsMy41NDgsMy41NDdjMCwxLjk1Ny0xLjU5MiwzLjU0OC0zLjU0OCwzLjU0OGMtMS45NTcsMC0zLjU0OC0xLjU5Mi0zLjU0OC0zLjU0OCAgIEM2Ljk0NCwyMS40OTksOC41MzYsMTkuOTA4LDEwLjQ5MywxOS45MDh6Ij48L3BhdGg+PC9nPjwvc3ZnPg==';
const dataSvg =
'data:image/svg+xml;utf8,';
+const sourceWithHeaders = {
+ uri: placeholder,
+ headers: {
+ 'x-token': '0012345'
+ }
+};
+const sourceWithHeadersAndRedirect = {
+ uri: source,
+ headers: {
+ 'x-token': '0012345'
+ }
+};
function Divider() {
return ;
@@ -114,6 +126,17 @@ export default function ImagePage() {
/>
+
+
+
+ With Headers
+
+
+
+ Headers & Redirect
+
+
+
);
}
diff --git a/packages/react-native-web/src/exports/Image/index.js b/packages/react-native-web/src/exports/Image/index.js
index d68898dea..e8fe78c94 100644
--- a/packages/react-native-web/src/exports/Image/index.js
+++ b/packages/react-native-web/src/exports/Image/index.js
@@ -8,6 +8,7 @@
* @flow
*/
+import type { ImageSource, LoadRequest } from '../../modules/ImageLoader';
import type { ImageProps } from './types';
import * as React from 'react';
@@ -146,6 +147,23 @@ function resolveAssetUri(source): ?string {
return uri;
}
+function raiseOnErrorEvent(uri, { onError, onLoadEnd }) {
+ if (onError) {
+ onError({
+ nativeEvent: {
+ error: `Failed to load resource ${uri} (404)`
+ }
+ });
+ }
+ if (onLoadEnd) onLoadEnd();
+}
+
+function hasSourceDiff(a: ImageSource, b: ImageSource) {
+ return (
+ a.uri !== b.uri || JSON.stringify(a.headers) !== JSON.stringify(b.headers)
+ );
+}
+
interface ImageStatics {
getSize: (
uri: string,
@@ -158,10 +176,12 @@ interface ImageStatics {
) => Promise<{| [uri: string]: 'disk/memory' |}>;
}
-const Image: React.AbstractComponent<
+type ImageComponent = React.AbstractComponent<
ImageProps,
React.ElementRef
-> = React.forwardRef((props, ref) => {
+>;
+
+const BaseImage: ImageComponent = React.forwardRef((props, ref) => {
const {
accessibilityLabel,
blurRadius,
@@ -279,16 +299,7 @@ const Image: React.AbstractComponent<
},
function error() {
updateState(ERRORED);
- if (onError) {
- onError({
- nativeEvent: {
- error: `Failed to load resource ${uri} (404)`
- }
- });
- }
- if (onLoadEnd) {
- onLoadEnd();
- }
+ raiseOnErrorEvent(uri, { onError, onLoadEnd });
}
);
}
@@ -332,14 +343,76 @@ const Image: React.AbstractComponent<
);
});
-Image.displayName = 'Image';
+BaseImage.displayName = 'Image';
+
+/**
+ * This component handles specifically loading an image source with headers
+ * default source is never loaded using headers
+ */
+const ImageWithHeaders: ImageComponent = React.forwardRef((props, ref) => {
+ // $FlowIgnore: This component would only be rendered when `source` matches `ImageSource`
+ const nextSource: ImageSource = props.source;
+ const [blobUri, setBlobUri] = React.useState('');
+ const request = React.useRef({
+ cancel: () => {},
+ source: { uri: '', headers: {} },
+ promise: Promise.resolve('')
+ });
+
+ const { onError, onLoadStart, onLoadEnd } = props;
+
+ React.useEffect(() => {
+ if (!hasSourceDiff(nextSource, request.current.source)) {
+ return;
+ }
+
+ // When source changes we want to clean up any old/running requests
+ request.current.cancel();
+
+ if (onLoadStart) {
+ onLoadStart();
+ }
+
+ // Store a ref for the current load request so we know what's the last loaded source,
+ // and so we can cancel it if a different source is passed through props
+ request.current = ImageLoader.loadWithHeaders(nextSource);
+
+ request.current.promise
+ .then((uri) => setBlobUri(uri))
+ .catch(() =>
+ raiseOnErrorEvent(request.current.source.uri, { onError, onLoadEnd })
+ );
+ }, [nextSource, onLoadStart, onError, onLoadEnd]);
+
+ // Cancel any request on unmount
+ React.useEffect(() => request.current.cancel, []);
+
+ const propsToPass = {
+ ...props,
+
+ // `onLoadStart` is called from the current component
+ // We skip passing it down to prevent BaseImage raising it a 2nd time
+ onLoadStart: undefined,
+
+ // Until the current component resolves the request (using headers)
+ // we skip forwarding the source so the base component doesn't attempt
+ // to load the original source
+ source: blobUri ? { ...nextSource, uri: blobUri } : undefined
+ };
+
+ return ;
+});
// $FlowIgnore: This is the correct type, but casting makes it unhappy since the variables aren't defined yet
-const ImageWithStatics = (Image: React.AbstractComponent<
- ImageProps,
- React.ElementRef
-> &
- ImageStatics);
+const ImageWithStatics: ImageComponent & ImageStatics = React.forwardRef(
+ (props, ref) => {
+ if (props.source && props.source.headers) {
+ return ;
+ }
+
+ return ;
+ }
+);
ImageWithStatics.getSize = function (uri, success, failure) {
ImageLoader.getSize(uri, success, failure);
diff --git a/packages/react-native-web/src/exports/Image/types.js b/packages/react-native-web/src/exports/Image/types.js
index 55ad3cb9f..ab9fee2e6 100644
--- a/packages/react-native-web/src/exports/Image/types.js
+++ b/packages/react-native-web/src/exports/Image/types.js
@@ -102,8 +102,8 @@ export type ImageStyle = {
tintColor?: ColorValue
};
-export type ImageProps = {
- ...ViewProps,
+export type ImageProps = {|
+ ...$Exact,
blurRadius?: number,
defaultSource?: Source,
draggable?: boolean,
@@ -116,4 +116,4 @@ export type ImageProps = {
resizeMode?: ResizeMode,
source?: Source,
style?: GenericStyleProp
-};
+|};
diff --git a/packages/react-native-web/src/exports/ImageBackground/index.js b/packages/react-native-web/src/exports/ImageBackground/index.js
index 561dd33d1..a86111839 100644
--- a/packages/react-native-web/src/exports/ImageBackground/index.js
+++ b/packages/react-native-web/src/exports/ImageBackground/index.js
@@ -16,12 +16,12 @@ import Image from '../Image';
import StyleSheet from '../StyleSheet';
import View from '../View';
-type ImageBackgroundProps = {
+type ImageBackgroundProps = {|
...ImageProps,
imageRef?: any,
imageStyle?: $PropertyType,
style?: $PropertyType
-};
+|};
const emptyObject = {};
diff --git a/packages/react-native-web/src/modules/ImageLoader/index.js b/packages/react-native-web/src/modules/ImageLoader/index.js
index 892db9929..0d7ceda8f 100644
--- a/packages/react-native-web/src/modules/ImageLoader/index.js
+++ b/packages/react-native-web/src/modules/ImageLoader/index.js
@@ -122,9 +122,18 @@ const ImageLoader = {
id += 1;
const image = new window.Image();
image.onerror = onError;
- image.onload = (e) => {
+ image.onload = (nativeEvent) => {
// avoid blocking the main thread
- const onDecode = () => onLoad({ nativeEvent: e });
+ const onDecode = () => {
+ // Append `source` to match RN's ImageLoadEvent interface
+ nativeEvent.source = {
+ uri: image.src,
+ width: image.naturalWidth,
+ height: image.naturalHeight
+ };
+
+ onLoad({ nativeEvent });
+ };
if (typeof image.decode === 'function') {
// Safari currently throws exceptions when decoding svgs.
// We want to catch that error and allow the load handler
@@ -136,8 +145,41 @@ const ImageLoader = {
};
image.src = uri;
requests[`${id}`] = image;
+
return id;
},
+ loadWithHeaders(source: ImageSource): LoadRequest {
+ let uri: string;
+ const abortController = new AbortController();
+ const request = new Request(source.uri, {
+ headers: source.headers,
+ signal: abortController.signal
+ });
+ request.headers.append('accept', 'image/*');
+
+ const promise = fetch(request)
+ .then((response) => response.blob())
+ .then((blob) => {
+ uri = URL.createObjectURL(blob);
+ return uri;
+ })
+ .catch((error) => {
+ if (error.name === 'AbortError') {
+ return '';
+ }
+
+ throw error;
+ });
+
+ return {
+ promise,
+ source,
+ cancel: () => {
+ abortController.abort();
+ URL.revokeObjectURL(uri);
+ }
+ };
+ },
prefetch(uri: string): Promise {
return new Promise((resolve, reject) => {
ImageLoader.load(
@@ -164,4 +206,15 @@ const ImageLoader = {
}
};
+export type LoadRequest = {|
+ cancel: Function,
+ source: ImageSource,
+ promise: Promise
+|};
+
+export type ImageSource = {
+ uri: string,
+ headers: { [key: string]: string }
+};
+
export default ImageLoader;