Live video on the AT Protocol
1import {
2 ConfigPlugin,
3 withAndroidManifest,
4 withEntitlementsPlist,
5 withXcodeProject,
6} from "expo/config-plugins";
7import streamplaceReactNativeWebRTC from "../config-react-native-webrtc";
8export const withNotificationsIOS: ConfigPlugin = (config) => {
9 config = withEntitlementsPlist(config, (config) => {
10 config.modResults["aps-environment"] = "production";
11 return config;
12 });
13 return config;
14};
15
16export const withoutNotificationsIOS: ConfigPlugin = (config) => {
17 config = withEntitlementsPlist(config, (config) => {
18 delete config.modResults["aps-environment"];
19 return config;
20 });
21 return config;
22};
23
24const withAndroidProfileable = (config) => {
25 return withAndroidManifest(config, (config) => {
26 const androidManifest = config.modResults.manifest;
27 if (
28 !androidManifest.application ||
29 androidManifest.application.length === 0
30 ) {
31 throw new Error("No application found in AndroidManifest.xml");
32 }
33 const mainApplication = androidManifest.application[0];
34
35 (mainApplication as any).profileable = [
36 {
37 $: {
38 "android:shell": "true",
39 "android:enabled": "true",
40 },
41 },
42 ];
43
44 return config;
45 });
46};
47
48const withConsistentVersionNumber = (
49 config,
50 { version }: { version: string },
51) => {
52 // if (!config.ios) {
53 // config.ios = {};
54 // }
55 // if (!config.ios.infoPlist) {
56 // config.ios.infoPlist = {};
57 // }
58 config = withXcodeProject(config, (config) => {
59 for (let [k, v] of Object.entries(
60 config.modResults.hash.project.objects.XCBuildConfiguration,
61 )) {
62 const obj = v as any;
63 if (!obj.buildSettings) {
64 continue;
65 }
66 if (typeof obj.buildSettings.MARKETING_VERSION !== "undefined") {
67 obj.buildSettings.MARKETING_VERSION = version;
68 }
69 if (typeof obj.buildSettings.CURRENT_PROJECT_VERSION !== "undefined") {
70 obj.buildSettings.CURRENT_PROJECT_VERSION = version;
71 }
72 }
73 return config;
74 });
75 return config;
76};
77
78// turn a semver string into a always-increasing integer for google
79export const versionCode = (verStr: string) => {
80 const [major, minor, patch] = verStr.split(".").map((x) => parseInt(x));
81 return major * 1000 * 1000 + minor * 1000 + patch;
82};
83
84export default function () {
85 const isProd =
86 process.env["SP_PRODUCTION_RELEASE"] === "true" || !!process.env.CI;
87 const enableSentry = process.env["SP_ENABLE_SENTRY"] === "true";
88 const pkg = require("./package.json");
89 const name = isProd ? "Streamplace" : "Devplace";
90 let bundle = isProd ? "tv.aquareum" : "tv.aquareum.dev";
91 if (process.env["SP_BUNDLE_OVERRIDE"]) {
92 bundle = process.env["SP_BUNDLE_OVERRIDE"];
93 }
94 let appleTeamId = process.env["SP_APPLE_TEAM_ID"];
95 const scheme = process.env["SP_APP_SCHEME"] ?? bundle;
96 return {
97 expo: {
98 name: name,
99 slug: name,
100 version: pkg.version,
101 // Only rev this to the current version when native dependencies change!
102 runtimeVersion: pkg.runtimeVersion,
103 orientation: "default",
104 icon: "./assets/images/icon.png",
105 scheme: scheme,
106 userInterfaceStyle: "automatic",
107 splash: {
108 image: "./assets/images/splash.png",
109 resizeMode: "contain",
110 backgroundColor: "#ffffff",
111 },
112 assetBundlePatterns: ["**/*"],
113 ios: {
114 supportsTablet: true,
115 bundleIdentifier: bundle,
116 infoPlist: {
117 UIBackgroundModes: ["fetch", "remote-notification"],
118 LSMinimumSystemVersion: "12.0",
119 },
120 ...(appleTeamId
121 ? {
122 appleTeamId,
123 }
124 : {}),
125 ...(isProd
126 ? {
127 googleServicesFile: "./GoogleService-Info.plist",
128 entitlements: {
129 "aps-environment": "production",
130 },
131 associatedDomains: ["applinks:stream.place"],
132 }
133 : {}),
134 },
135 android: {
136 adaptiveIcon: {
137 foregroundImage: "./assets/images/adaptive-icon.png",
138 backgroundColor: "#ffffff",
139 },
140 package: bundle,
141 edgeToEdgeEnabled: true,
142 versionCode: versionCode(pkg.version),
143 intentFilters: [
144 {
145 action: "VIEW",
146 autoVerify: true,
147 data: [
148 {
149 scheme: "https",
150 host: "stream.place",
151 pathPattern: "/.*:.*",
152 },
153 ],
154 category: ["BROWSABLE", "DEFAULT"],
155 },
156 {
157 action: "VIEW",
158 autoVerify: true,
159 data: [
160 {
161 scheme: "https",
162 host: "stream.place",
163 pathPattern: "/.*\\\\..*",
164 },
165 ],
166 category: ["BROWSABLE", "DEFAULT"],
167 },
168 {
169 action: "VIEW",
170 autoVerify: true,
171 data: [
172 {
173 scheme: "https",
174 host: "stream.place",
175 path: "/",
176 },
177 ],
178 category: ["BROWSABLE", "DEFAULT"],
179 },
180 ],
181 ...(isProd
182 ? {
183 googleServicesFile: "./google-services.json",
184 permissions: [
185 "android.permission.SCHEDULE_EXACT_ALARM",
186 "android.permission.POST_NOTIFICATIONS",
187 ],
188 }
189 : {}),
190 },
191 web: {
192 bundler: "metro",
193 output: "single",
194 favicon: "./assets/images/favicon.png",
195 },
196 plugins: [
197 withAndroidProfileable,
198 "expo-video",
199 "expo-web-browser",
200 "expo-screen-orientation",
201 streamplaceReactNativeWebRTC,
202 [
203 "expo-video",
204 {
205 supportsBackgroundPlayback: true,
206 supportsPictureInPicture: true,
207 },
208 ],
209 ["expo-sqlite", { useSQLCipher: true }],
210 "expo-file-system",
211 [
212 "expo-font",
213 {
214 fonts: [
215 "./assets/fonts/AtkinsonHyperlegibleNext-Regular.ttf",
216 "./assets/fonts/AtkinsonHyperlegibleNext-Light.ttf",
217 "./assets/fonts/AtkinsonHyperlegibleNext-ExtraLight.ttf",
218 "./assets/fonts/AtkinsonHyperlegibleNext-Medium.ttf",
219 "./assets/fonts/AtkinsonHyperlegibleNext-SemiBold.ttf",
220 "./assets/fonts/AtkinsonHyperlegibleNext-Bold.ttf",
221 "./assets/fonts/AtkinsonHyperlegibleNext-ExtraBold.ttf",
222
223 "./assets/fonts/AtkinsonHyperlegibleMono-Regular.ttf",
224 "./assets/fonts/AtkinsonHyperlegibleMono-Medium.ttf",
225 "./assets/fonts/AtkinsonHyperlegibleMono-SemiBold.ttf",
226 "./assets/fonts/AtkinsonHyperlegibleMono-Bold.ttf",
227 ],
228 },
229 ],
230 [
231 "expo-build-properties",
232 {
233 ios: {
234 useFrameworks: "static",
235 },
236 // uncomment to test OTA updates to http://localhost:8080
237 // android: {
238 // usesCleartextTraffic: true,
239 // },
240 },
241 ],
242 [
243 "expo-asset",
244 {
245 assets: ["assets"],
246 },
247 ],
248 [withConsistentVersionNumber, { version: pkg.version }],
249 [
250 "react-native-edge-to-edge",
251 {
252 android: {
253 parentTheme: "Default",
254 enforceNavigationBarContrast: false,
255 },
256 },
257 ],
258 ...(isProd
259 ? [
260 "@react-native-firebase/app",
261 "@react-native-firebase/messaging",
262 [withNotificationsIOS, {}],
263 ]
264 : ["expo-dev-launcher", withoutNotificationsIOS]),
265 ...(enableSentry
266 ? [
267 [
268 "@sentry/react-native/expo",
269 {
270 url: "https://sentry.io/",
271 project: process.env["SP_SENTRY_APP"] || "app",
272 organization: process.env["SP_SENTRY_ORG"] || "streamplace",
273 },
274 ],
275 ]
276 : []),
277 ],
278 experiments: {
279 typedRoutes: true,
280 },
281 updates: isProd
282 ? {
283 url: `https://stream.place/api/manifest`,
284 enabled: true,
285 checkAutomatically: "ON_LOAD",
286 fallbackToCacheTimeout: 30000,
287 codeSigningCertificate: "./code-signing/certs/certificate.pem",
288 codeSigningMetadata: {
289 keyid: "main",
290 alg: "rsa-v1_5-sha256",
291 },
292 }
293 : {},
294 },
295 };
296}