diff --git a/.changeset/fix-fetch-image-blob-arraybuffer.md b/.changeset/fix-fetch-image-blob-arraybuffer.md new file mode 100644 index 000000000..7d4659396 --- /dev/null +++ b/.changeset/fix-fetch-image-blob-arraybuffer.md @@ -0,0 +1,12 @@ +--- +'@reown/appkit-core-react-native': patch +--- + +fix(core): load wallet & network images on React Native + +`FetchUtil.fetchImage` built its data URL with `response.blob()` + `FileReader`, +but on React Native `fetch(...).blob()` throws "Creating blobs from 'ArrayBuffer' +and 'ArrayBufferView' are not supported" for binary responses — so wallet and +network images silently failed to load (placeholders only). Read the bytes via +`response.arrayBuffer()` and base64-encode them into the data URL instead, using +a dependency-free encoder (RN guarantees neither `Buffer` nor `btoa`). diff --git a/packages/core/src/__tests__/utils/FetchUtil.test.ts b/packages/core/src/__tests__/utils/FetchUtil.test.ts index 0e864fb4d..49e08b8da 100644 --- a/packages/core/src/__tests__/utils/FetchUtil.test.ts +++ b/packages/core/src/__tests__/utils/FetchUtil.test.ts @@ -104,4 +104,63 @@ describe('FetchUtil', () => { expect(url).toBe('https://another.com/test?foo=bar&bar=baz&clientId=test-client-id'); }); }); + + describe('fetchImage', () => { + const originalFetch = global.fetch; + + afterEach(() => { + global.fetch = originalFetch; + }); + + it('should build a base64 data URL from the response ArrayBuffer', async () => { + // "foobar" -> base64 "Zm9vYmFy" + const bytes = new Uint8Array([0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72]); + global.fetch = jest.fn().mockResolvedValue({ + arrayBuffer: async () => bytes.buffer, + headers: { get: () => 'image/png' } + }) as unknown as typeof fetch; + + const fetchUtil = new FetchUtil({ baseUrl }); + const result = await fetchUtil.fetchImage('/getWalletImage/1'); + + expect(result).toBe('data:image/png;base64,Zm9vYmFy'); + }); + + it('should default the content type to image/png when the header is missing', async () => { + global.fetch = jest.fn().mockResolvedValue({ + arrayBuffer: async () => new Uint8Array([0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72]).buffer, + headers: { get: () => null } + }) as unknown as typeof fetch; + + const fetchUtil = new FetchUtil({ baseUrl }); + const result = await fetchUtil.fetchImage('/getWalletImage/1'); + + expect(result).toBe('data:image/png;base64,Zm9vYmFy'); + }); + + it('should not call response.blob() (RN cannot build a Blob from an ArrayBuffer)', async () => { + const blob = jest.fn(); + global.fetch = jest.fn().mockResolvedValue({ + arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer, + headers: { get: () => 'image/webp' }, + blob + }) as unknown as typeof fetch; + + const fetchUtil = new FetchUtil({ baseUrl }); + await fetchUtil.fetchImage('/getWalletImage/1'); + + expect(blob).not.toHaveBeenCalled(); + }); + + it('should return undefined when the request throws', async () => { + global.fetch = jest + .fn() + .mockRejectedValue(new Error('network error')) as unknown as typeof fetch; + + const fetchUtil = new FetchUtil({ baseUrl }); + const result = await fetchUtil.fetchImage('/getWalletImage/1'); + + expect(result).toBeUndefined(); + }); + }); }); diff --git a/packages/core/src/utils/FetchUtil.ts b/packages/core/src/utils/FetchUtil.ts index f0b3451c2..1a69f95d1 100644 --- a/packages/core/src/utils/FetchUtil.ts +++ b/packages/core/src/utils/FetchUtil.ts @@ -80,18 +80,43 @@ export class FetchUtil { try { const url = this.createUrl({ path, params }).toString(); const response = await fetch(url, { headers }); - const blob = await response.blob(); - const reader = new FileReader(); - reader.readAsDataURL(blob); - return new Promise(resolve => { - reader.onloadend = () => resolve(reader.result as string); - }); + // React Native's `fetch(...).blob()` throws "Creating blobs from + // 'ArrayBuffer' and 'ArrayBufferView' are not supported" for binary + // responses, so Blob + FileReader can't be used to build the data URL + // (wallet/network images would silently fail to load). Read the bytes as + // an ArrayBuffer and base64-encode them into a data URL instead. + const arrayBuffer = await response.arrayBuffer(); + const contentType = response.headers.get('content-type') ?? 'image/png'; + + return `data:${contentType};base64,${FetchUtil._arrayBufferToBase64(arrayBuffer)}`; } catch { return undefined; } } + // Dependency-free base64 encoder (RN has no global `Buffer`/`btoa` guarantee). + /* eslint-disable no-bitwise */ + private static _arrayBufferToBase64(buffer: ArrayBuffer): string { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + const bytes = new Uint8Array(buffer); + let base64 = ''; + + for (let i = 0; i < bytes.length; i += 3) { + const byte0 = bytes[i] as number; + const byte1 = i + 1 < bytes.length ? (bytes[i + 1] as number) : 0; + const byte2 = i + 2 < bytes.length ? (bytes[i + 2] as number) : 0; + + base64 += chars[byte0 >> 2]; + base64 += chars[((byte0 & 0x03) << 4) | (byte1 >> 4)]; + base64 += i + 1 < bytes.length ? chars[((byte1 & 0x0f) << 2) | (byte2 >> 6)] : '='; + base64 += i + 2 < bytes.length ? chars[byte2 & 0x3f] : '='; + } + + return base64; + } + /* eslint-enable no-bitwise */ + public createUrl({ path, params }: RequestArguments) { let fullUrl: string;