@oxyhq/app-preset 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # @oxyhq/app-preset
2
+
3
+ The **Oxy distro of Expo** — the shared configuration every Oxy app (Mention,
4
+ Homiio, Allo, accounts, Commons, …) used to copy-paste, centralized into one
5
+ zero-build package. Apps replace four config-plugin entries, a hand-tuned Metro
6
+ config, a Babel config, an ESLint config, a Tailwind CSS header, and three
7
+ tsconfigs with a single dependency, and pick up ecosystem changes with a version
8
+ bump instead of re-editing every repo.
9
+
10
+ There is no build step: this package ships plain CommonJS config plus static CSS
11
+ and JSON.
12
+
13
+ ## What it centralizes
14
+
15
+ | Piece | Import | Replaces |
16
+ | --- | --- | --- |
17
+ | Config plugin | `['@oxyhq/app-preset', {}]` | `withSharedUserId` + iOS keychain entitlement + `expo-build-properties` + `@oxyhq/services/plugins/withSharedIdentityReader` |
18
+ | Metro | `@oxyhq/app-preset/metro` | monorepo watch folders, block list, symlink + package-exports resolution, web-font/wasm asset exts, release minifier, NativeWind wrapper |
19
+ | Babel | `@oxyhq/app-preset/babel` | `babel-preset-expo` + `module-resolver` + `react-native-worklets/plugin` |
20
+ | ESLint | `@oxyhq/app-preset/eslint` | `eslint-config-expo/flat` + `dist/*` ignore |
21
+ | CSS base | `@oxyhq/app-preset/base.css` | Tailwind v4 + NativeWind + Bloom design-token imports + SDK `@source` globs |
22
+ | tsconfig | `@oxyhq/app-preset/tsconfig/{base,frontend,backend}.json` | the shared strict/composite TypeScript bases |
23
+
24
+ ## Usage
25
+
26
+ ### 1. Config plugin (`app.config.js` / `app.json`)
27
+
28
+ ```js
29
+ plugins: [
30
+ // …app-specific plugins…
31
+ ['@oxyhq/app-preset', {}],
32
+ ]
33
+ ```
34
+
35
+ This adds `android:sharedUserId="so.oxy.shared"`, the iOS
36
+ `keychain-access-groups` entitlement (`$(AppIdentifierPrefix)group.so.oxy.shared`),
37
+ the Oxy `expo-build-properties` defaults (iOS `deploymentTarget 16.4`; Android
38
+ `compileSdk 36` / `targetSdk 35` / ProGuard + resource shrinking), and the
39
+ `@oxyhq/services` shared-identity reader plugin (Android signature permission +
40
+ `<queries>` for silent "Sign in with Oxy").
41
+
42
+ Each piece is individually disableable and overridable:
43
+
44
+ ```js
45
+ ['@oxyhq/app-preset', {
46
+ sharedUserId: 'so.oxy.shared', // false → skip android:sharedUserId
47
+ keychainGroup: 'group.so.oxy.shared', // false → skip iOS keychain entitlement
48
+ ios: { deploymentTarget: '17.0' }, // deep-merges over defaults; false → skip iOS build props
49
+ android: { targetSdkVersion: 34 }, // deep-merges over defaults; false → skip Android build props
50
+ sharedIdentityReader: true, // false → skip @oxyhq/services reader plugin
51
+ }]
52
+ ```
53
+
54
+ ### 2. Metro (`metro.config.js`)
55
+
56
+ ```js
57
+ const { createOxyMetroConfig } = require('@oxyhq/app-preset/metro');
58
+
59
+ module.exports = createOxyMetroConfig(__dirname, {
60
+ sharedTypesPackage: '@myapp/shared-types', // optional
61
+ cssInput: './global.css', // optional, this is the default
62
+ });
63
+ ```
64
+
65
+ ### 3. Babel (`babel.config.js`)
66
+
67
+ ```js
68
+ module.exports = require('@oxyhq/app-preset/babel');
69
+ ```
70
+
71
+ ### 4. ESLint (`eslint.config.js`)
72
+
73
+ ```js
74
+ const oxyConfig = require('@oxyhq/app-preset/eslint');
75
+ module.exports = [...oxyConfig];
76
+ ```
77
+
78
+ ### 5. CSS (`global.css`)
79
+
80
+ ```css
81
+ @import "@oxyhq/app-preset/base.css";
82
+
83
+ /* App-specific globs (later rules win on overlap): */
84
+ @source "./app/**/*.{js,jsx,ts,tsx}";
85
+ @source "./components/**/*.{js,jsx,ts,tsx}";
86
+ ```
87
+
88
+ ### 6. TypeScript (`tsconfig.json`)
89
+
90
+ ```jsonc
91
+ // frontend
92
+ { "extends": "@oxyhq/app-preset/tsconfig/frontend.json", "compilerOptions": { "paths": { "@/*": ["./*"] } }, "include": ["**/*.ts", "**/*.tsx"] }
93
+ // backend
94
+ { "extends": "@oxyhq/app-preset/tsconfig/backend.json", "compilerOptions": { "rootDir": "./", "outDir": "dist" }, "include": ["**/*.ts"] }
95
+ ```
96
+
97
+ ## Peer dependencies
98
+
99
+ All peers are **optional** except `expo` — a piece's peer is only needed when you
100
+ use that piece (e.g. `expo-build-properties` only when build properties are
101
+ enabled, `eslint-config-expo` only for the ESLint config). The config plugin and
102
+ factories throw a clear, actionable error if a required peer is missing.
103
+
104
+ ## Compatibility
105
+
106
+ Requires **Expo SDK 56+**; validated against **Expo SDK 57 / React Native 0.86**
107
+ (the current Oxy ecosystem target) via `test-app-expo`.
package/app.plugin.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./plugin/withOxyAppPreset');
package/babel/index.js ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @oxyhq/app-preset — Babel config factory.
3
+ *
4
+ * The standard Babel config every Oxy Expo app shares: babel-preset-expo with
5
+ * `unstable_transformImportMeta`, the `@ → ./` module-resolver alias, and the
6
+ * `react-native-worklets/plugin` LAST (Reanimated 4 re-exports it — it must be
7
+ * the final plugin).
8
+ *
9
+ * Usage — a one-line babel.config.js:
10
+ *
11
+ * module.exports = require('@oxyhq/app-preset/babel');
12
+ *
13
+ * The peer packages (`babel-preset-expo`, `babel-plugin-module-resolver`,
14
+ * `react-native-worklets`) resolve from the consuming app's node_modules, where
15
+ * Expo already installs them.
16
+ *
17
+ * @param {import('@babel/core').ConfigAPI} api
18
+ * @returns {import('@babel/core').TransformOptions}
19
+ */
20
+ module.exports = function oxyBabelPreset(api) {
21
+ api.cache(true);
22
+ return {
23
+ presets: [
24
+ ['babel-preset-expo', { unstable_transformImportMeta: true }],
25
+ ],
26
+ plugins: [
27
+ // The module-resolver must come first for correct module resolution.
28
+ [
29
+ 'module-resolver',
30
+ {
31
+ root: ['./'],
32
+ alias: { '@': './' },
33
+ extensions: ['.ts', '.tsx', '.js', '.jsx', '.json', '.svg'],
34
+ },
35
+ ],
36
+ // Must be LAST.
37
+ 'react-native-worklets/plugin',
38
+ ],
39
+ };
40
+ };
package/css/base.css ADDED
@@ -0,0 +1,32 @@
1
+ /* @oxyhq/app-preset — Tailwind v4 + NativeWind base stylesheet.
2
+ *
3
+ * The shared CSS header every Oxy Expo app used to copy-paste: the Tailwind v4
4
+ * layer imports, NativeWind's theme, and Bloom's canonical design tokens
5
+ * (RADIUS scale, spacing, typography, color aliases — the single source of
6
+ * truth generated from `bloomThemeCss()`), plus the `@source` globs that scan
7
+ * the Oxy SDK packages for class names.
8
+ *
9
+ * Import it at the TOP of the app's global.css, then add the app-specific
10
+ * `@source` globs and `@theme` overrides below it (later rules win on overlap):
11
+ *
12
+ * @import "@oxyhq/app-preset/base.css";
13
+ *
14
+ * @source "./app/**\/*.{js,jsx,ts,tsx}";
15
+ * @source "./components/**\/*.{js,jsx,ts,tsx}";
16
+ *
17
+ * The SDK `@source` globs below are RELATIVE TO THIS FILE's location. Published,
18
+ * this file lives at `node_modules/@oxyhq/app-preset/css/base.css`, so
19
+ * `../../services/lib` resolves to `node_modules/@oxyhq/services/lib` and
20
+ * `../../bloom/lib` to `node_modules/@oxyhq/bloom/lib` — @oxyhq/app-preset,
21
+ * @oxyhq/services and @oxyhq/bloom are siblings under `@oxyhq/`. The same two
22
+ * levels up land on `packages/` in the source monorepo, so the globs resolve in
23
+ * both layouts without each app guessing its own node_modules path. */
24
+
25
+ @import "tailwindcss/theme.css" layer(theme);
26
+ @import "tailwindcss/preflight.css" layer(base);
27
+ @import "tailwindcss/utilities.css";
28
+ @import "nativewind/theme";
29
+ @import "@oxyhq/bloom/design-tokens/theme.css";
30
+
31
+ @source "../../services/lib/**/*.{js,jsx}";
32
+ @source "../../bloom/lib/**/*.{js,jsx}";
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @oxyhq/app-preset — flat ESLint config.
3
+ *
4
+ * `eslint-config-expo/flat` plus the shared `dist/*` ignore. Consumers spread
5
+ * it and append their own rules:
6
+ *
7
+ * const oxyConfig = require('@oxyhq/app-preset/eslint');
8
+ * module.exports = [...oxyConfig];
9
+ *
10
+ * `eslint` and `eslint-config-expo` resolve from the consuming app's
11
+ * node_modules (both are optional peers of the preset).
12
+ *
13
+ * @type {import('eslint').Linter.Config[]}
14
+ */
15
+ const { defineConfig } = require('eslint/config');
16
+ const expoConfig = require('eslint-config-expo/flat');
17
+
18
+ module.exports = defineConfig([
19
+ expoConfig,
20
+ {
21
+ ignores: ['dist/*'],
22
+ },
23
+ ]);
package/metro/index.js ADDED
@@ -0,0 +1,148 @@
1
+ /**
2
+ * @oxyhq/app-preset — Metro config factory.
3
+ *
4
+ * Lifts the shared Metro configuration every Oxy Expo app used to copy-paste:
5
+ * monorepo watch folders, the block list, symlink + package-exports resolution
6
+ * (required by @oxyhq/bloom's subpath exports), the web-font/wasm asset
7
+ * extensions, the release minifier tuning, and the NativeWind wrapper.
8
+ *
9
+ * Usage — a three-line metro.config.js:
10
+ *
11
+ * const { createOxyMetroConfig } = require('@oxyhq/app-preset/metro');
12
+ * module.exports = createOxyMetroConfig(__dirname, {
13
+ * sharedTypesPackage: '@myapp/shared-types',
14
+ * });
15
+ *
16
+ * @param {string} projectRoot Absolute path to the app package (pass `__dirname`).
17
+ * @param {object} [options]
18
+ * @param {string} [options.sharedTypesPackage] Bare name of the app's
19
+ * `packages/shared-types` workspace package. When set, it is aliased to the
20
+ * monorepo `packages/shared-types` dir and its `src` is added to the block
21
+ * list (so Metro never bundles the un-built TypeScript source).
22
+ * @param {string} [options.cssInput='./global.css'] NativeWind CSS entry point.
23
+ * @param {(string|RegExp)[]} [options.extraBlockList=[]] Extra Metro
24
+ * `blockList` patterns appended to the Oxy defaults.
25
+ * @returns {import('metro-config').MetroConfig}
26
+ */
27
+ const { getDefaultConfig } = require('expo/metro-config');
28
+ const { withNativeWind } = require('nativewind/metro');
29
+ const path = require('path');
30
+
31
+ function blockPath(dir) {
32
+ const resolved = path.resolve(dir);
33
+ return new RegExp(`${resolved.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/.*`);
34
+ }
35
+
36
+ function createOxyMetroConfig(projectRoot, options = {}) {
37
+ const {
38
+ sharedTypesPackage,
39
+ cssInput = './global.css',
40
+ extraBlockList = [],
41
+ } = options;
42
+
43
+ const monorepoRoot = path.resolve(projectRoot, '../..');
44
+ const config = getDefaultConfig(projectRoot);
45
+
46
+ config.projectRoot = projectRoot;
47
+
48
+ // Include the monorepo root so Metro resolves hoisted deps in root node_modules/.
49
+ config.watchFolders = [monorepoRoot];
50
+
51
+ const blockList = [
52
+ blockPath(path.join(monorepoRoot, 'packages/backend')),
53
+ blockPath(path.join(monorepoRoot, 'docs')),
54
+ /\.expo\/.*/,
55
+ /\.expo-shared\/.*/,
56
+ /\.metro\/.*/,
57
+ /\.cache\/.*/,
58
+ /node_modules\/\.cache\/.*/,
59
+ /\.tsbuildinfo$/,
60
+ /.*\.expo\/types\/.*/,
61
+ /__tests__\/.*/,
62
+ /\.test\.(js|ts|tsx|jsx)$/,
63
+ /\.spec\.(js|ts|tsx|jsx)$/,
64
+ /\.md$/,
65
+ /README/,
66
+ ...extraBlockList,
67
+ ];
68
+
69
+ const extraNodeModules = {};
70
+ if (sharedTypesPackage) {
71
+ const sharedTypesDir = path.join(monorepoRoot, 'packages/shared-types');
72
+ blockList.push(blockPath(path.join(sharedTypesDir, 'src')));
73
+ extraNodeModules[sharedTypesPackage] = sharedTypesDir;
74
+ }
75
+
76
+ config.resolver = {
77
+ ...config.resolver,
78
+ blockList,
79
+ extraNodeModules: {
80
+ ...config.resolver.extraNodeModules,
81
+ ...extraNodeModules,
82
+ },
83
+ // Resolve from the app's node_modules first, then the monorepo root (hoisted deps).
84
+ nodeModulesPaths: [
85
+ path.join(projectRoot, 'node_modules'),
86
+ path.join(monorepoRoot, 'node_modules'),
87
+ ],
88
+ // Enable symlinks for workspace resolution.
89
+ unstable_enableSymlinks: true,
90
+ // Enable package.json "exports" resolution (required by @oxyhq/bloom subpath exports).
91
+ unstable_enablePackageExports: true,
92
+ sourceExts: [...config.resolver.sourceExts, 'ts', 'tsx'],
93
+ // Bloom imports `.woff2`/`.woff` fonts directly from JS on web; Metro does not
94
+ // include them in default assetExts, and svg is handled by the transformer.
95
+ assetExts: [
96
+ ...config.resolver.assetExts.filter((ext) => ext !== 'svg'),
97
+ 'wasm',
98
+ 'woff2',
99
+ 'woff',
100
+ ],
101
+ };
102
+
103
+ config.transformer = {
104
+ ...config.transformer,
105
+ minifierConfig: {
106
+ ...config.transformer?.minifierConfig,
107
+ keep_classnames: false,
108
+ keep_fnames: false,
109
+ mangle: {
110
+ keep_classnames: false,
111
+ keep_fnames: false,
112
+ },
113
+ output: {
114
+ ascii_only: true,
115
+ quote_style: 3,
116
+ wrap_iife: true,
117
+ },
118
+ sourceMap: {
119
+ includeSources: false,
120
+ },
121
+ toplevel: false,
122
+ compress: {
123
+ arguments: true,
124
+ dead_code: true,
125
+ drop_console: false,
126
+ drop_debugger: true,
127
+ ecma: 2020,
128
+ evaluate: true,
129
+ inline: 1,
130
+ passes: 3,
131
+ reduce_funcs: true,
132
+ reduce_vars: true,
133
+ unsafe: false,
134
+ unsafe_comps: false,
135
+ unsafe_math: false,
136
+ unsafe_methods: false,
137
+ },
138
+ },
139
+ };
140
+
141
+ return withNativeWind(config, {
142
+ input: cssInput,
143
+ inlineRem: 16,
144
+ inlineVariables: false,
145
+ });
146
+ }
147
+
148
+ module.exports = { createOxyMetroConfig };
package/package.json ADDED
@@ -0,0 +1,105 @@
1
+ {
2
+ "name": "@oxyhq/app-preset",
3
+ "version": "0.1.0",
4
+ "description": "The Oxy distro of Expo — centralized Expo config plugin, Metro/Babel/ESLint factories, Tailwind base CSS and TypeScript bases shared by every Oxy app (Mention, Homiio, Allo, accounts, …). A zero-build package: apps replace copy-pasted config with a single dependency bump.",
5
+ "main": "./app.plugin.js",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "exports": {
10
+ ".": "./app.plugin.js",
11
+ "./app.plugin.js": "./app.plugin.js",
12
+ "./plugin/withOxyAppPreset": "./plugin/withOxyAppPreset.js",
13
+ "./plugin/withSharedUserId": "./plugin/withSharedUserId.js",
14
+ "./plugin/withOxyKeychain": "./plugin/withOxyKeychain.js",
15
+ "./plugin/withOxyBuildProperties": "./plugin/withOxyBuildProperties.js",
16
+ "./metro": "./metro/index.js",
17
+ "./babel": "./babel/index.js",
18
+ "./eslint": "./eslint/index.js",
19
+ "./base.css": "./css/base.css",
20
+ "./css/base.css": "./css/base.css",
21
+ "./tsconfig/base.json": "./tsconfig/base.json",
22
+ "./tsconfig/frontend.json": "./tsconfig/frontend.json",
23
+ "./tsconfig/backend.json": "./tsconfig/backend.json",
24
+ "./package.json": "./package.json"
25
+ },
26
+ "files": [
27
+ "app.plugin.js",
28
+ "plugin",
29
+ "metro",
30
+ "babel",
31
+ "eslint",
32
+ "css",
33
+ "tsconfig",
34
+ "README.md"
35
+ ],
36
+ "keywords": [
37
+ "oxyhq",
38
+ "expo",
39
+ "react-native",
40
+ "config-plugin",
41
+ "metro",
42
+ "babel",
43
+ "nativewind",
44
+ "preset"
45
+ ],
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "https://github.com/oxyhq/sdk",
49
+ "directory": "packages/app-preset"
50
+ },
51
+ "author": "OxyHQ",
52
+ "license": "MIT",
53
+ "homepage": "https://oxy.so",
54
+ "engines": {
55
+ "node": ">=18.0.0"
56
+ },
57
+ "scripts": {
58
+ "test": "echo \"@oxyhq/app-preset is a zero-build config package — no unit tests\" && exit 0"
59
+ },
60
+ "peerDependencies": {
61
+ "@oxyhq/bloom": "*",
62
+ "@oxyhq/services": "*",
63
+ "babel-plugin-module-resolver": "*",
64
+ "babel-preset-expo": "*",
65
+ "eslint": ">=9.0.0",
66
+ "eslint-config-expo": "*",
67
+ "expo": ">=56.0.0",
68
+ "expo-build-properties": "*",
69
+ "nativewind": ">=5.0.0-preview.0",
70
+ "react-native-worklets": "*",
71
+ "tailwindcss": ">=4.0.0"
72
+ },
73
+ "peerDependenciesMeta": {
74
+ "@oxyhq/bloom": {
75
+ "optional": true
76
+ },
77
+ "@oxyhq/services": {
78
+ "optional": true
79
+ },
80
+ "babel-plugin-module-resolver": {
81
+ "optional": true
82
+ },
83
+ "babel-preset-expo": {
84
+ "optional": true
85
+ },
86
+ "eslint": {
87
+ "optional": true
88
+ },
89
+ "eslint-config-expo": {
90
+ "optional": true
91
+ },
92
+ "expo-build-properties": {
93
+ "optional": true
94
+ },
95
+ "nativewind": {
96
+ "optional": true
97
+ },
98
+ "react-native-worklets": {
99
+ "optional": true
100
+ },
101
+ "tailwindcss": {
102
+ "optional": true
103
+ }
104
+ }
105
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Expo Config Plugin: withOxyAppPreset
3
+ *
4
+ * The single config-plugin entry every Oxy app adds in place of the four
5
+ * copy-pasted plugin entries (`withSharedUserId`, keychain entitlement,
6
+ * `expo-build-properties`, `@oxyhq/services/plugins/withSharedIdentityReader`).
7
+ *
8
+ * In app.config.js / app.json:
9
+ *
10
+ * plugins: [
11
+ * // …app-specific plugins…
12
+ * ['@oxyhq/app-preset', {}],
13
+ * ]
14
+ *
15
+ * Each piece is individually disableable by passing its option as `false`:
16
+ *
17
+ * ['@oxyhq/app-preset', {
18
+ * sharedUserId: 'so.oxy.shared', // false → skip android:sharedUserId
19
+ * keychainGroup: 'group.so.oxy.shared', // false → skip iOS keychain entitlement
20
+ * ios: { deploymentTarget: '17.0' }, // false → skip iOS build properties
21
+ * android: { targetSdkVersion: 34 }, // false → skip Android build properties
22
+ * sharedIdentityReader: true, // false → skip @oxyhq/services reader plugin
23
+ * }]
24
+ *
25
+ * @param {import('expo/config').ExpoConfig} config
26
+ * @param {object} [options]
27
+ * @param {string|false} [options.sharedUserId='so.oxy.shared']
28
+ * @param {string|false} [options.keychainGroup='group.so.oxy.shared']
29
+ * @param {object|false} [options.ios]
30
+ * @param {object|false} [options.android]
31
+ * @param {boolean} [options.sharedIdentityReader=true]
32
+ */
33
+ const withSharedUserId = require('./withSharedUserId');
34
+ const withOxyKeychain = require('./withOxyKeychain');
35
+ const withOxyBuildProperties = require('./withOxyBuildProperties');
36
+
37
+ module.exports = function withOxyAppPreset(config, options = {}) {
38
+ const {
39
+ sharedUserId = 'so.oxy.shared',
40
+ keychainGroup = 'group.so.oxy.shared',
41
+ ios = {},
42
+ android = {},
43
+ sharedIdentityReader = true,
44
+ } = options;
45
+
46
+ let next = config;
47
+
48
+ if (sharedUserId !== false) {
49
+ next = withSharedUserId(next, sharedUserId);
50
+ }
51
+
52
+ if (keychainGroup !== false) {
53
+ next = withOxyKeychain(next, keychainGroup);
54
+ }
55
+
56
+ if (ios !== false || android !== false) {
57
+ next = withOxyBuildProperties(next, { ios, android });
58
+ }
59
+
60
+ if (sharedIdentityReader !== false) {
61
+ let withSharedIdentityReader;
62
+ try {
63
+ withSharedIdentityReader = require('@oxyhq/services/plugins/withSharedIdentityReader');
64
+ } catch (error) {
65
+ throw new Error(
66
+ "[@oxyhq/app-preset] sharedIdentityReader is enabled but the peer dependency '@oxyhq/services' "
67
+ + 'is not installed. Install it, or pass `{ sharedIdentityReader: false }` to the preset.',
68
+ );
69
+ }
70
+ next = withSharedIdentityReader(next);
71
+ }
72
+
73
+ return next;
74
+ };
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Expo Config Plugin: withOxyBuildProperties
3
+ *
4
+ * Wraps `expo-build-properties` with the native build defaults every Oxy app
5
+ * ships with — iOS deployment target and the Android SDK / minification knobs —
6
+ * so apps stop copy-pasting the same `expo-build-properties` block. Caller
7
+ * overrides deep-merge over these defaults.
8
+ *
9
+ * `expo-build-properties` is required lazily (it is an optional peer): a missing
10
+ * install throws a clear, actionable error only when this plugin actually runs
11
+ * with build properties enabled.
12
+ *
13
+ * @param {import('expo/config').ExpoConfig} config
14
+ * @param {object} [options]
15
+ * @param {object|false} [options.ios] iOS overrides, or `false` to skip iOS.
16
+ * @param {object|false} [options.android] Android overrides, or `false` to skip Android.
17
+ */
18
+ const DEFAULTS = {
19
+ ios: {
20
+ deploymentTarget: '16.4',
21
+ },
22
+ android: {
23
+ compileSdkVersion: 36,
24
+ targetSdkVersion: 35,
25
+ buildToolsVersion: '36.0.0',
26
+ enableProguardInReleaseBuilds: true,
27
+ enableShrinkResourcesInReleaseBuilds: true,
28
+ useLegacyPackaging: false,
29
+ },
30
+ };
31
+
32
+ function isPlainObject(value) {
33
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
34
+ }
35
+
36
+ function deepMerge(base, override) {
37
+ if (!isPlainObject(override)) {
38
+ return base;
39
+ }
40
+ const result = { ...base };
41
+ for (const [key, value] of Object.entries(override)) {
42
+ result[key] = isPlainObject(value) && isPlainObject(result[key])
43
+ ? deepMerge(result[key], value)
44
+ : value;
45
+ }
46
+ return result;
47
+ }
48
+
49
+ function resolveBuildPropertiesPlugin() {
50
+ let mod;
51
+ try {
52
+ mod = require('expo-build-properties');
53
+ } catch (error) {
54
+ throw new Error(
55
+ "[@oxyhq/app-preset] withOxyBuildProperties requires the peer dependency 'expo-build-properties'. "
56
+ + "Install it with `npx expo install expo-build-properties`, or disable it by passing "
57
+ + '`{ ios: false, android: false }` to the preset.',
58
+ );
59
+ }
60
+ const plugin = typeof mod === 'function' ? mod : mod.withBuildProperties || mod.default;
61
+ if (typeof plugin !== 'function') {
62
+ throw new Error(
63
+ "[@oxyhq/app-preset] Could not resolve the 'expo-build-properties' config plugin from the installed package.",
64
+ );
65
+ }
66
+ return plugin;
67
+ }
68
+
69
+ module.exports = function withOxyBuildProperties(config, options = {}) {
70
+ const { ios = {}, android = {} } = options;
71
+
72
+ if (ios === false && android === false) {
73
+ return config;
74
+ }
75
+
76
+ const props = {};
77
+ if (ios !== false) {
78
+ props.ios = deepMerge(DEFAULTS.ios, ios);
79
+ }
80
+ if (android !== false) {
81
+ props.android = deepMerge(DEFAULTS.android, android);
82
+ }
83
+
84
+ const withBuildProperties = resolveBuildPropertiesPlugin();
85
+ return withBuildProperties(config, props);
86
+ };
87
+
88
+ module.exports.DEFAULTS = DEFAULTS;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Expo Config Plugin: withOxyKeychain
3
+ *
4
+ * Adds an iOS `keychain-access-groups` entitlement so Oxy apps signed with the
5
+ * same Team ID can share Keychain items — the iOS half of "sign in once, use
6
+ * everywhere" (the Android half is withSharedUserId + the shared-identity
7
+ * native module). The group is prefixed with `$(AppIdentifierPrefix)` so Xcode
8
+ * expands it to the Team ID at build time.
9
+ *
10
+ * Merge-not-overwrite: if the app (or another plugin, e.g. expo-build-properties)
11
+ * already declared keychain-access-groups, the Oxy group is appended without
12
+ * dropping the existing entries.
13
+ *
14
+ * @param {import('expo/config').ExpoConfig} config
15
+ * @param {string} [keychainGroup='group.so.oxy.shared'] The keychain group,
16
+ * without the `$(AppIdentifierPrefix)` prefix.
17
+ */
18
+ const { withEntitlementsPlist } = require('expo/config-plugins');
19
+
20
+ module.exports = function withOxyKeychain(config, keychainGroup = 'group.so.oxy.shared') {
21
+ const entry = `$(AppIdentifierPrefix)${keychainGroup}`;
22
+
23
+ return withEntitlementsPlist(config, (config) => {
24
+ const key = 'keychain-access-groups';
25
+ const existing = config.modResults[key];
26
+ const groups = Array.isArray(existing) ? [...existing] : [];
27
+
28
+ if (!groups.includes(entry)) {
29
+ groups.push(entry);
30
+ }
31
+
32
+ config.modResults[key] = groups;
33
+ return config;
34
+ });
35
+ };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Expo Config Plugin: withSharedUserId
3
+ *
4
+ * Adds android:sharedUserId to AndroidManifest.xml to enable cross-app data
5
+ * sharing between Oxy apps (Mention, Homiio, accounts, Commons, …).
6
+ *
7
+ * This allows:
8
+ * - Shared cryptographic identity storage
9
+ * - Cross-app authentication (sign in once, use everywhere)
10
+ * - Shared session tokens
11
+ *
12
+ * IMPORTANT:
13
+ * - All Oxy apps MUST use the same sharedUserId: "so.oxy.shared"
14
+ * - Apps MUST be signed with the same certificate
15
+ * - Cannot change sharedUserId after publishing (requires reinstall)
16
+ *
17
+ * @see https://developer.android.com/guide/topics/manifest/manifest-element#uid
18
+ *
19
+ * @param {import('expo/config').ExpoConfig} config
20
+ * @param {string} [sharedUserId='so.oxy.shared'] The android:sharedUserId value.
21
+ */
22
+ const { withAndroidManifest } = require('expo/config-plugins');
23
+
24
+ module.exports = function withSharedUserId(config, sharedUserId = 'so.oxy.shared') {
25
+ return withAndroidManifest(config, async (config) => {
26
+ const androidManifest = config.modResults.manifest;
27
+
28
+ // Add sharedUserId to the manifest root element
29
+ androidManifest.$ = {
30
+ ...androidManifest.$,
31
+ 'android:sharedUserId': sharedUserId,
32
+ };
33
+
34
+ return config;
35
+ });
36
+ };
@@ -0,0 +1,11 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "//": "@oxyhq/app-preset — Node/Bun backend TypeScript config. Compiled with tsc to dist/ (CommonJS) and THAT emitted output is what the Docker/ECS image runs — module format and emit layout are a runtime contract. Overrides the Oxy base only where this becomes an emitting CommonJS Node service. Consumers set their own `rootDir`/`outDir`/`include` (and `paths` to resolve workspace deps from source during type-check).",
4
+ "extends": "./base.json",
5
+ "compilerOptions": {
6
+ "module": "commonjs",
7
+ "moduleResolution": "node",
8
+ "noEmit": false,
9
+ "declaration": true
10
+ }
11
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "//": "@oxyhq/app-preset — shared, runtime-neutral TypeScript base. The strict/safety defaults every Oxy package inherits. This base is TYPE-CHECK ONLY (noEmit): the frontend (tsconfig/frontend.json) and emitting Node packages (tsconfig/backend.json) override module/target/emit for their runtime. Does NOT extend expo/tsconfig.base — that sets customConditions:[\"react-native\"], only valid with bundler/nodenext resolution, which would break the CJS/node packages. The frontend config layers the Expo base on top where bundler resolution is in effect.",
4
+ "compilerOptions": {
5
+ "target": "ES2020",
6
+ "lib": ["ES2020"],
7
+ "module": "ES2020",
8
+ "moduleResolution": "bundler",
9
+
10
+ "strict": true,
11
+ "esModuleInterop": true,
12
+ "allowSyntheticDefaultImports": true,
13
+ "skipLibCheck": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "resolveJsonModule": true,
16
+
17
+ "composite": true,
18
+ "declaration": true,
19
+ "declarationMap": true,
20
+ "sourceMap": true,
21
+
22
+ "noEmit": true
23
+ },
24
+ "exclude": [
25
+ "node_modules",
26
+ "**/node_modules",
27
+ "**/dist",
28
+ "**/build",
29
+ "**/.expo"
30
+ ]
31
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "//": "@oxyhq/app-preset — Expo / React Native frontend TypeScript config. Not compiled by tsc (Metro/Expo bundles the source); tsc is only for type-checking. Extends BOTH the Oxy base (strict/composite defaults) AND expo/tsconfig.base (React-Native JSX, customConditions:[\"react-native\"], bundler resolution). Later entries win, so the Expo base overrides the Oxy base where they differ; the explicit overrides below win over both. Consumers extend this and add their own `paths` (e.g. \"@/*\") + `include`.",
4
+ "extends": ["./base.json", "expo/tsconfig.base"],
5
+ "compilerOptions": {
6
+ "jsx": "react-native",
7
+ "strict": true,
8
+ "composite": true,
9
+ "noEmit": false
10
+ }
11
+ }