diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1c790619..c463233b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,6 +28,8 @@ jobs: uses: cycjimmy/semantic-release-action@v4 with: working_directory: ./mobile + extra_plugins: | + semantic-release-expo env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.releaserc b/.releaserc index 0a563eee..11ca9aea 100644 --- a/.releaserc +++ b/.releaserc @@ -1,3 +1,10 @@ { - "branches": ["main"] + "branches": ["main"], + "plugins": [ + "semantic-release-expo", + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + "@semantic-release/npm", + "@semantic-release/github" + ], } diff --git a/mobile/.gitignore b/mobile/.gitignore index 1c40e829..f2ab8ed2 100644 --- a/mobile/.gitignore +++ b/mobile/.gitignore @@ -41,3 +41,6 @@ ios/ # Firebase config files google-services.json GoogleService-Info.plist + +# Google Cloud Platform +dsocial-fed6b-firebase-adminsdk-7sjg7-b2b5c052c3.json diff --git a/mobile/Makefile b/mobile/Makefile index 899f0c8c..b1d84a09 100644 --- a/mobile/Makefile +++ b/mobile/Makefile @@ -85,15 +85,7 @@ release.android: node_modules .PHONY: elease.android help: - @echo "Usage: make [target]" - @echo "" - @echo "Targets:" - @echo " ios - Build the iOS app" - @echo " ios.release - Build the iOS app in release mode" - @echo " android - Build the Android app" - @echo " start - Start the metro server" - @echo " clean - Clean the project" - @echo " clean_install - Clean the project and install dependencies" - @echo " release - Build the app for production" - @echo " help - Show this help message" + @echo "Available make commands:" + @cat Makefile | grep '^[a-z]' | grep -v '=' | cut -d: -f1 | sort | sed 's/^/ /' .PHONY: help + diff --git a/mobile/app.config.js b/mobile/app.config.js new file mode 100644 index 00000000..980af0a4 --- /dev/null +++ b/mobile/app.config.js @@ -0,0 +1,19 @@ +export default ({ config }) => { + if (process.env.MY_ENVIRONMENT === "production") { + return { + ...config, + ios: { + ...config.ios, + googleServicesFile: process.env.GOOGLESERVICES_INFO_PLIST || "./GoogleService-Info.plist", + }, + android: { + ...config.android, + googleServicesFile: process.env.GOOGLE_SERVICES_JSON || "./google-services.json", + }, + }; + } else { + return { + ...config, + }; + } +}; diff --git a/mobile/app.json b/mobile/app.json index 41d278e0..7c95ce15 100644 --- a/mobile/app.json +++ b/mobile/app.json @@ -3,7 +3,7 @@ "name": "dsocial", "slug": "dsocial", "platforms": ["ios", "android"], - "version": "1.0.5", + "version": "1.0.0", "orientation": "portrait", "scheme": "tech.berty.dsocial", "icon": "./assets/images/icon.png", @@ -28,7 +28,7 @@ } } }, - "googleServicesFile": "./GoogleService-Info.plist" + "buildNumber": "9" }, "android": { "adaptiveIcon": { @@ -36,8 +36,7 @@ "backgroundColor": "#ffffff" }, "package": "tech.berty.dsocial.android", - "versionCode": 8, - "googleServicesFile": "./google-services.json" + "versionCode": "9" }, "web": { "favicon": "./assets/images/favicon.png" diff --git a/mobile/app/_layout.tsx b/mobile/app/_layout.tsx index eb53bf1d..fbfdac87 100644 --- a/mobile/app/_layout.tsx +++ b/mobile/app/_layout.tsx @@ -1,10 +1,3 @@ -// order matters here -import "react-native-polyfill-globals/auto"; - -// Polyfill async.Iterator. For some reason, the Babel presets and plugins are not doing the trick. -// Code from here: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-3.html#caveats -(Symbol as any).asyncIterator = Symbol.asyncIterator || Symbol.for("Symbol.asyncIterator"); - import { Stack } from "expo-router"; import { DefaultTheme, ThemeProvider } from "@react-navigation/native"; diff --git a/mobile/eas.json b/mobile/eas.json index 5d2b1c39..13676c85 100644 --- a/mobile/eas.json +++ b/mobile/eas.json @@ -7,12 +7,30 @@ "developmentClient": true, "distribution": "internal" }, + "ios-simulator": { + "extends": "development", + "ios": { + "simulator": true + } + }, "preview": { - "distribution": "internal" + "distribution": "internal", + "env": { + "MY_ENVIRONMENT": "production" + } }, - "production": {} + "production": { + "env": { + "MY_ENVIRONMENT": "production" + } + } }, "submit": { - "production": {} + "production": { + "android": { + "serviceAccountKeyPath": "./dsocial-fed6b-firebase-adminsdk-7sjg7-b2b5c052c3.json", + "track": "internal" + } + } } } diff --git a/mobile/package-lock.json b/mobile/package-lock.json index 2cf61e60..bbccbf63 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -24,6 +24,7 @@ "expo-application": "~5.9.1", "expo-clipboard": "~6.0.3", "expo-constants": "~16.0.2", + "expo-dev-client": "~4.0.18", "expo-device": "~6.0.2", "expo-linear-gradient": "~13.0.2", "expo-linking": "~6.3.1", @@ -7137,6 +7138,93 @@ "expo": "*" } }, + "node_modules/expo-dev-client": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-4.0.18.tgz", + "integrity": "sha512-FxqBLHcTvUvIeqgaDGAjEfalWCWn9xmfvVm0Bpb50tkwxFrDcg4t13p/tvYw8sLEm+87HSee/Lx04OrZcC3oiQ==", + "dependencies": { + "expo-dev-launcher": "4.0.20", + "expo-dev-menu": "5.0.15", + "expo-dev-menu-interface": "1.8.3", + "expo-manifests": "~0.14.0", + "expo-updates-interface": "~0.16.2" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-dev-launcher": { + "version": "4.0.20", + "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-4.0.20.tgz", + "integrity": "sha512-BvEoBsSU2H3NHEa9qsydfv2dQFaNSMSW9g+dHXY8Zz3FpfR5FFbjxOpn/ck46GB52Zh3tLWggXKhoUXc8fEARA==", + "dependencies": { + "ajv": "8.11.0", + "expo-dev-menu": "5.0.15", + "expo-manifests": "~0.14.0", + "resolve-from": "^5.0.0", + "semver": "^7.6.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-dev-launcher/node_modules/ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/expo-dev-launcher/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/expo-dev-menu": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-5.0.15.tgz", + "integrity": "sha512-a5aADQXOH/uw2NDy4fbgVl9wkAcZIfkrz8yzwQz0X6Yvf0a68zafqxSvvYkq+MmUTrFsuiST49s+mk4uRqHJMw==", + "dependencies": { + "expo-dev-menu-interface": "1.8.3", + "semver": "^7.5.4" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-dev-menu-interface": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/expo-dev-menu-interface/-/expo-dev-menu-interface-1.8.3.tgz", + "integrity": "sha512-QM0LRozeFT5Ek0N7XpV93M+HMdEKRLEOXn0aW5M3uoUlnqC1+PLtF3HMy3k3hMKTTE/kJ1y1Z7akH07T0lunCQ==", + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-dev-menu/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/expo-device": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/expo-device/-/expo-device-6.0.2.tgz", @@ -7189,6 +7277,11 @@ "expo": "*" } }, + "node_modules/expo-json-utils": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/expo-json-utils/-/expo-json-utils-0.13.1.tgz", + "integrity": "sha512-mlfaSArGVb+oJmUcR22jEONlgPp0wj4iNIHfQ2je9Q8WTOqMc0Ws9tUciz3JdJnhffdHqo/k8fpvf0IRmN5HPA==" + }, "node_modules/expo-keep-awake": { "version": "13.0.2", "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-13.0.2.tgz", @@ -7214,6 +7307,18 @@ "invariant": "^2.2.4" } }, + "node_modules/expo-manifests": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-0.14.3.tgz", + "integrity": "sha512-L3b5/qocBPiQjbW0cpOHfnqdKZbTJS7sA3mgeDJT+mWga/xYsdpma1EfNmsuvrOzjLGjStr1k1fceM9Bl49aqQ==", + "dependencies": { + "@expo/config": "~9.0.0", + "expo-json-utils": "~0.13.0" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-modules-autolinking": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-1.11.1.tgz", @@ -7308,6 +7413,14 @@ "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-1.12.1.tgz", "integrity": "sha512-/t3xdbS8KB0prj5KG5w7z+wZPFlPtkgs95BsmrP/E7Q0xHXTcDcQ6Cu2FkFuRM+PKTb17cJDnLkawyS5vDLxMA==" }, + "node_modules/expo-updates-interface": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/expo-updates-interface/-/expo-updates-interface-0.16.2.tgz", + "integrity": "sha512-929XBU70q5ELxkKADj1xL0UIm3HvhYhNAOZv5DSk7rrKvLo7QDdPyl+JVnwZm9LrkNbH4wuE2rLoKu1KMgZ+9A==", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/fast-base64-decode": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz", diff --git a/mobile/package.json b/mobile/package.json index 1866e28b..78c1c615 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -51,7 +51,10 @@ "react-redux": "^9.1.0", "styled-components": "^6.1.8", "text-encoding": "^0.7.0", - "web-streams-polyfill": "^3.3.2" + "web-streams-polyfill": "^3.3.2", + "expo-application": "~5.9.1", + "expo-linear-gradient": "~13.0.2", + "expo-dev-client": "~4.0.18" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/mobile/src/grpc/transport_web.ts b/mobile/src/grpc/transport_web.ts index 204f6cf4..13b39e0e 100644 --- a/mobile/src/grpc/transport_web.ts +++ b/mobile/src/grpc/transport_web.ts @@ -1,6 +1,10 @@ -import type { AnyMessage, MethodInfo, PartialMessage, ServiceType } from '@bufbuild/protobuf'; +import { polyfill as polyfillReadableStream } from "react-native-polyfill-globals/src/readable-stream"; +polyfillReadableStream(); +import { fetch as fetchPolyfill, Headers as HeadersPolyfill } from "react-native-fetch-api"; -import type { StreamResponse, Transport, UnaryRequest, UnaryResponse } from '@connectrpc/connect'; +import type { AnyMessage, MethodInfo, PartialMessage, ServiceType } from "@bufbuild/protobuf"; +import type { ContextValues, StreamResponse, Transport, UnaryRequest, UnaryResponse } from "@connectrpc/connect"; +import { createContextValues } from "@connectrpc/connect"; import { createClientMethodSerializers, createEnvelopeReadableStream, @@ -8,20 +12,37 @@ import { encodeEnvelope, runStreamingCall, runUnaryCall, -} from '@connectrpc/connect/protocol'; -import { endStreamFlag, endStreamFromJson, requestHeader, validateResponse } from '@connectrpc/connect/protocol-connect'; +} from "@connectrpc/connect/protocol"; +import { + headerTimeout, + headerContentType, + contentTypeUnaryProto, + contentTypeUnaryJson, + contentTypeStreamJson, + headerUserAgent, + headerProtocolVersion, + protocolVersion, + contentTypeStreamProto, + endStreamFlag, + endStreamFromJson, + validateResponse, +} from "@connectrpc/connect/protocol-connect"; import { requestHeader as webRequestHeader, trailerFlag, trailerParse, validateResponse as webValidateResponse, validateTrailer, -} from '@connectrpc/connect/protocol-grpc-web'; -import { GrpcWebTransportOptions } from '@connectrpc/connect-web'; -import { Message, MethodKind } from '@bufbuild/protobuf'; +} from "@connectrpc/connect/protocol-grpc-web"; +import { GrpcWebTransportOptions } from "@connectrpc/connect-web"; +import { Message, MethodKind } from "@bufbuild/protobuf"; + +// Polyfill async.Iterator. For some reason, the Babel presets and plugins are not doing the trick. +// Code from here: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-3.html#caveats +(Symbol as any).asyncIterator = Symbol.asyncIterator || Symbol.for("Symbol.asyncIterator"); class AbortError extends Error { - name = 'AbortError'; + name = "AbortError"; } interface FetchXHRResponse { @@ -35,7 +56,7 @@ function parseHeaders(allHeaders: string): Headers { .trim() .split(/[\r\n]+/) .reduce((memo, header) => { - const [key, value] = header.split(': '); + const [key, value] = header.split(": "); memo.append(key, value); return memo; }, new Headers()); @@ -71,8 +92,14 @@ export function createXHRGrpcWebTransport(options: GrpcWebTransportOptions): Tra timeoutMs: number | undefined, header: Headers, message: PartialMessage, + contextValues?: ContextValues ): Promise> { - const { serialize, parse } = createClientMethodSerializers(method, useBinaryFormat, options.jsonOptions, options.binaryOptions); + const { serialize, parse } = createClientMethodSerializers( + method, + useBinaryFormat, + options.jsonOptions, + options.binaryOptions + ); return await runUnaryCall({ signal, @@ -83,10 +110,11 @@ export function createXHRGrpcWebTransport(options: GrpcWebTransportOptions): Tra method, url: createMethodUrl(options.baseUrl, service, method), init: { - method: 'POST', - mode: 'cors', + method: "POST", + mode: "cors", }, - header: webRequestHeader(useBinaryFormat, timeoutMs, header), + header: webRequestHeader(useBinaryFormat, timeoutMs, header, false), + contextValues: contextValues ?? createContextValues(), message, }, next: async (req: UnaryRequest): Promise> => { @@ -94,19 +122,19 @@ export function createXHRGrpcWebTransport(options: GrpcWebTransportOptions): Tra return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); - xhr.open(req.init.method ?? 'POST', req.url); + xhr.open(req.init.method ?? "POST", req.url); function onAbort() { xhr.abort(); } - req.signal.addEventListener('abort', onAbort); + req.signal.addEventListener("abort", onAbort); - xhr.addEventListener('abort', () => { - reject(new AbortError('Request aborted')); + xhr.addEventListener("abort", () => { + reject(new AbortError("Request aborted")); }); - xhr.addEventListener('load', () => { + xhr.addEventListener("load", () => { resolve({ status: xhr.status, headers: parseHeaders(xhr.getAllResponseHeaders()), @@ -114,15 +142,15 @@ export function createXHRGrpcWebTransport(options: GrpcWebTransportOptions): Tra }); }); - xhr.addEventListener('error', () => { - reject(new Error('Network Error')); + xhr.addEventListener("error", () => { + reject(new Error("Network Error")); }); - xhr.addEventListener('loadend', () => { - req.signal.removeEventListener('abort', onAbort); + xhr.addEventListener("loadend", () => { + req.signal.removeEventListener("abort", onAbort); }); - xhr.responseType = 'arraybuffer'; + xhr.responseType = "arraybuffer"; req.header.forEach((value: string, key: string) => xhr.setRequestHeader(key, value)); @@ -141,7 +169,7 @@ export function createXHRGrpcWebTransport(options: GrpcWebTransportOptions): Tra chunks.forEach(({ flags, data }) => { if (flags === trailerFlag) { if (trailer !== undefined) { - throw 'extra trailer'; + throw "extra trailer"; } // Unary responses require exactly one response message, but in @@ -152,20 +180,20 @@ export function createXHRGrpcWebTransport(options: GrpcWebTransportOptions): Tra } if (message !== undefined) { - throw 'extra message'; + throw "extra message"; } message = parse(data); }); if (trailer === undefined) { - throw 'missing trailer'; + throw "missing trailer"; } - validateTrailer(trailer); + validateTrailer(trailer, response.headers); if (message === undefined) { - throw 'missing message'; + throw "missing message"; } return >{ @@ -185,10 +213,16 @@ export function createXHRGrpcWebTransport(options: GrpcWebTransportOptions): Tra timeoutMs: number | undefined, header: HeadersInit | undefined, input: AsyncIterable>, + contextValues?: ContextValues ): Promise> { - const { serialize, parse } = createClientMethodSerializers(method, useBinaryFormat, options.jsonOptions, options.binaryOptions); - - async function* parseResponseBody(body: ReadableStream, trailerTarget: Headers) { + const { serialize, parse } = createClientMethodSerializers( + method, + useBinaryFormat, + options.jsonOptions, + options.binaryOptions + ); + + async function* parseResponseBody(body: ReadableStream, trailerTarget: HeadersPolyfill) { const reader = createEnvelopeReadableStream(body).getReader(); let endStreamReceived = false; @@ -215,23 +249,53 @@ export function createXHRGrpcWebTransport(options: GrpcWebTransportOptions): Tra } if (!endStreamReceived) { - throw 'missing EndStreamResponse'; + throw "missing EndStreamResponse"; } } async function createRequestBody(input: AsyncIterable): Promise { if (method.kind != MethodKind.ServerStreaming) { - throw 'The fetch API does not support streaming request bodies'; + throw "The fetch API does not support streaming request bodies"; } const r = await input[Symbol.asyncIterator]().next(); if (r.done == true) { - throw 'missing request message'; + throw "missing request message"; } return encodeEnvelope(0, serialize(r.value)); } + function requestHeader( + methodKind: MethodKind, + useBinaryFormat: boolean, + timeoutMs: number | undefined, + userProvidedHeaders: HeadersInit | undefined, + setUserAgent: boolean + ): HeadersPolyfill { + const result = new HeadersPolyfill( + userProvidedHeaders !== null && userProvidedHeaders !== void 0 ? userProvidedHeaders : {} + ); + if (timeoutMs !== undefined) { + result.set(headerTimeout, `${timeoutMs}`); + } + result.set( + headerContentType, + methodKind == MethodKind.Unary + ? useBinaryFormat + ? contentTypeUnaryProto + : contentTypeUnaryJson + : useBinaryFormat + ? contentTypeStreamProto + : contentTypeStreamJson + ); + result.set(headerProtocolVersion, protocolVersion); + if (setUserAgent) { + result.set(headerUserAgent, "connect-es/1.4.0"); + } + return result; + } + return await runStreamingCall({ interceptors: options.interceptors, timeoutMs, @@ -242,16 +306,16 @@ export function createXHRGrpcWebTransport(options: GrpcWebTransportOptions): Tra method, url: createMethodUrl(options.baseUrl, service, method), init: { - method: 'POST', - credentials: options.credentials ?? 'same-origin', - mode: 'cors', + method: "POST", + credentials: options.credentials ?? "same-origin", + mode: "cors", }, - header: requestHeader(method.kind, useBinaryFormat, timeoutMs, header), + header: requestHeader(method.kind, useBinaryFormat, timeoutMs, header, false), + contextValues: contextValues ?? createContextValues(), message: input, }, next: async (req) => { - const fetch = options.fetch ?? globalThis.fetch; - const fRes = await fetch(req.url, { + const fRes = await fetchPolyfill(req.url, { ...req.init, headers: req.header, signal: req.signal, @@ -261,10 +325,10 @@ export function createXHRGrpcWebTransport(options: GrpcWebTransportOptions): Tra validateResponse(method.kind, fRes.status, fRes.headers); if (fRes.body === null) { - throw 'missing response body'; + throw "missing response body"; } - const trailer = new Headers(); + const trailer = new HeadersPolyfill(); // We have to implement the `*[Symbol.asyncIterator]()` of the object we give to the StreamResponse.message field. // It seems that react-native lacks the feature.