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 streamplaceReactNativeWebRTC,
201 [
202 "expo-video",
203 {
204 supportsBackgroundPlayback: true,
205 supportsPictureInPicture: true,
206 },
207 ],
208 ["expo-sqlite", { useSQLCipher: true }],
209 "expo-file-system",
210 [
211 "expo-font",
212 {
213 fonts: [
214 "./assets/fonts/AtkinsonHyperlegibleNext-Regular.ttf",
215 "./assets/fonts/AtkinsonHyperlegibleNext-Light.ttf",
216 "./assets/fonts/AtkinsonHyperlegibleNext-ExtraLight.ttf",
217 "./assets/fonts/AtkinsonHyperlegibleNext-Medium.ttf",
218 "./assets/fonts/AtkinsonHyperlegibleNext-SemiBold.ttf",
219 "./assets/fonts/AtkinsonHyperlegibleNext-Bold.ttf",
220 "./assets/fonts/AtkinsonHyperlegibleNext-ExtraBold.ttf",
221
222 "./assets/fonts/AtkinsonHyperlegibleMono-Regular.ttf",
223 "./assets/fonts/AtkinsonHyperlegibleMono-Medium.ttf",
224 "./assets/fonts/AtkinsonHyperlegibleMono-SemiBold.ttf",
225 "./assets/fonts/AtkinsonHyperlegibleMono-Bold.ttf",
226 ],
227 },
228 ],
229 [
230 "expo-build-properties",
231 {
232 ios: {
233 useFrameworks: "static",
234 },
235 // uncomment to test OTA updates to http://localhost:8080
236 // android: {
237 // usesCleartextTraffic: true,
238 // },
239 },
240 ],
241 [
242 "expo-asset",
243 {
244 assets: ["assets"],
245 },
246 ],
247 [withConsistentVersionNumber, { version: pkg.version }],
248 [
249 "react-native-edge-to-edge",
250 {
251 android: {
252 parentTheme: "Default",
253 enforceNavigationBarContrast: false,
254 },
255 },
256 ],
257 ...(isProd
258 ? [
259 "@react-native-firebase/app",
260 "@react-native-firebase/messaging",
261 [withNotificationsIOS, {}],
262 ]
263 : ["expo-dev-launcher", withoutNotificationsIOS]),
264 ...(enableSentry
265 ? [
266 [
267 "@sentry/react-native/expo",
268 {
269 url: "https://sentry.io/",
270 project: process.env["SP_SENTRY_APP"] || "app",
271 organization: process.env["SP_SENTRY_ORG"] || "streamplace",
272 },
273 ],
274 ]
275 : []),
276 ],
277 experiments: {
278 typedRoutes: true,
279 },
280 updates: isProd
281 ? {
282 url: `https://stream.place/api/manifest`,
283 enabled: true,
284 checkAutomatically: "ON_LOAD",
285 fallbackToCacheTimeout: 30000,
286 codeSigningCertificate: "./code-signing/certs/certificate.pem",
287 codeSigningMetadata: {
288 keyid: "main",
289 alg: "rsa-v1_5-sha256",
290 },
291 }
292 : {},
293 },
294 };
295}