Skip to content

Commit 3fbf01d

Browse files
committed
feat: add expo support
1 parent 6ce8c9d commit 3fbf01d

File tree

11 files changed

+2227
-56
lines changed

11 files changed

+2227
-56
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,4 @@ android/.kotlin/
9494
android/.kotlinc/
9595

9696
tsconfig.tsbuildinfo
97+
expoConfig/build

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,27 @@ React-native wrapper for android & IOS google maps sdk
2222
yarn add react-native-google-maps-plus react-native-nitro-modules
2323
```
2424

25+
### Expo Projects
26+
27+
Add your keys to the `app.json`.
28+
The config plugin automatically injects them into your native Android and iOS builds during `expo prebuild`.
29+
30+
```json
31+
{
32+
"expo": {
33+
"plugins": [
34+
[
35+
"react-native-google-maps-plus",
36+
{
37+
"googleMapsAndroidApiKey": "YOUR_ANDROID_MAPS_API_KEY",
38+
"googleMapsIosApiKey": "YOUR_IOS_MAPS_API_KEY"
39+
}
40+
]
41+
]
42+
}
43+
}
44+
```
45+
2546
# Dependencies
2647

2748
This package builds on native libraries for SVG rendering and Google Maps integration:

app.plugin.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('./expoConfig/build');

eslint.config.mjs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ export default defineConfig([
2424
},
2525
},
2626
{
27-
ignores: ['node_modules/', 'lib/', '.yarn', 'eslint.config.mjs'],
27+
ignores: [
28+
'node_modules/',
29+
'lib/',
30+
'expoConfig/build',
31+
'.yarn',
32+
'eslint.config.mjs',
33+
],
2834
},
2935
]);
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { withAndroidManifest } from '@expo/config-plugins';
2+
import type { ConfigPlugin } from '@expo/config-plugins';
3+
import {
4+
addMetaDataItemToMainApplication,
5+
getMainApplicationOrThrow,
6+
removeMetaDataItemFromMainApplication,
7+
} from '@expo/config-plugins/build/android/Manifest';
8+
import type { RNGoogleMapsPlusExpoPluginProps } from '../types';
9+
10+
const withMapsAndroid: ConfigPlugin<RNGoogleMapsPlusExpoPluginProps> = (
11+
config,
12+
props
13+
) => {
14+
return withAndroidManifest(config, (conf) => {
15+
const manifest = conf.modResults;
16+
const mainApplication = getMainApplicationOrThrow(manifest);
17+
18+
const apiKey =
19+
props.googleMapsAndroidApiKey ??
20+
process.env.GOOGLE_MAPS_API_KEY_ANDROID ??
21+
null;
22+
23+
if (apiKey) {
24+
addMetaDataItemToMainApplication(
25+
mainApplication,
26+
'com.google.android.geo.API_KEY',
27+
apiKey
28+
);
29+
} else {
30+
removeMetaDataItemFromMainApplication(
31+
mainApplication,
32+
'com.google.android.geo.API_KEY'
33+
);
34+
console.warn(
35+
'[react-native-google-maps-plus] No Android API key provided. Removed existing entry.'
36+
);
37+
}
38+
39+
return conf;
40+
});
41+
};
42+
43+
export default withMapsAndroid;

expoConfig/src/index.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { ConfigPlugin } from '@expo/config-plugins';
2+
import type { RNGoogleMapsPlusExpoPluginProps } from './types';
3+
import fs from 'fs';
4+
import path from 'path';
5+
6+
const pkg = JSON.parse(
7+
fs.readFileSync(path.resolve(__dirname, '../../package.json'), 'utf8')
8+
);
9+
10+
import { createRunOncePlugin } from '@expo/config-plugins';
11+
import withIosGoogleMapsPlus from './ios/withIosGoogleMapsPlus';
12+
import withAndroidGoogleMapsPlus from './android/withAndroidGoogleMapsPlus';
13+
14+
const withGoogleMapsPlus: ConfigPlugin<RNGoogleMapsPlusExpoPluginProps> = (
15+
config,
16+
props
17+
) => {
18+
config = withAndroidGoogleMapsPlus(config, props);
19+
config = withIosGoogleMapsPlus(config, props);
20+
return config;
21+
};
22+
23+
module.exports = createRunOncePlugin(withGoogleMapsPlus, pkg.name, pkg.version);
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import {
2+
withInfoPlist,
3+
withPodfile,
4+
withAppDelegate,
5+
type ConfigPlugin,
6+
} from '@expo/config-plugins';
7+
import { mergeContents } from '@expo/config-plugins/build/utils/generateCode';
8+
import type { RNGoogleMapsPlusExpoPluginProps } from '../types';
9+
10+
const withIosGoogleMapsPlus: ConfigPlugin<RNGoogleMapsPlusExpoPluginProps> = (
11+
config,
12+
props
13+
) => {
14+
config = withInfoPlist(config, (conf) => {
15+
const apiKey =
16+
props.googleMapsIosApiKey ?? process.env.GOOGLE_MAPS_API_KEY_IOS ?? null;
17+
18+
if (!apiKey) {
19+
console.warn(
20+
'[react-native-google-maps-plus] No iOS API key provided. Google Maps may fail to initialize.'
21+
);
22+
}
23+
if (apiKey) {
24+
conf.modResults.MAPS_API_KEY = apiKey;
25+
}
26+
return conf;
27+
});
28+
29+
config = withPodfile(config, (conf) => {
30+
let src = conf.modResults.contents;
31+
if (!src.includes('use_modular_headers!')) {
32+
src = mergeContents({
33+
tag: 'react-native-google-maps-modular-headers',
34+
src,
35+
newSrc: 'use_modular_headers!',
36+
anchor: /use_frameworks!|platform\s+:ios.*/,
37+
offset: 1,
38+
comment: '#',
39+
}).contents;
40+
}
41+
42+
const patchSnippet = `
43+
# Force iOS 16+ to avoid deployment target warnings
44+
installer.pods_project.targets.each do |target|
45+
target.build_configurations.each do |config|
46+
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '16.0'
47+
end
48+
end
49+
50+
# --- SVGKit Patch ---
51+
require 'fileutils'
52+
svgkit_path = File.join(installer.sandbox.pod_dir('SVGKit'), 'Source')
53+
54+
# --- Patch Node.h imports ---
55+
Dir.glob(File.join(svgkit_path, '**', '*.{h,m}')).each do |file|
56+
FileUtils.chmod("u+w", file)
57+
text = File.read(file)
58+
new_contents = text.gsub('#import "Node.h"', '#import "SVGKit/Node.h"')
59+
File.open(file, 'w') { |f| f.write(new_contents) }
60+
end
61+
62+
# --- Patch CSSValue.h imports ---
63+
Dir.glob(File.join(svgkit_path, '**', '*.{h,m}')).each do |file|
64+
FileUtils.chmod("u+w", file)
65+
text = File.read(file)
66+
new_contents = text.gsub('#import "CSSValue.h"', '#import "SVGKit/CSSValue.h"')
67+
File.open(file, 'w') { |f| f.write(new_contents) }
68+
end
69+
70+
# --- Patch SVGLength.h imports ---
71+
Dir.glob(File.join(svgkit_path, '**', '*.{h,m}')).each do |file|
72+
FileUtils.chmod("u+w", file)
73+
text = File.read(file)
74+
new_contents = text.gsub('#import "SVGLength.h"', '#import "SVGKit/SVGLength.h"')
75+
File.open(file, 'w') { |f| f.write(new_contents) }
76+
end
77+
`;
78+
79+
if (src.includes('post_install do |installer|')) {
80+
src = src.replace(
81+
/post_install do \|installer\|([\s\S]*?)end/,
82+
(match, inner) => {
83+
if (inner.includes('SVGKit Patch')) return match; // idempotent
84+
return `post_install do |installer|${inner}\n${patchSnippet}\nend`;
85+
}
86+
);
87+
} else {
88+
src += `\npost_install do |installer|\n${patchSnippet}\nend\n`;
89+
}
90+
91+
conf.modResults.contents = src;
92+
return conf;
93+
});
94+
95+
config = withAppDelegate(config, (conf) => {
96+
const { language } = conf.modResults;
97+
if (language !== 'swift') {
98+
console.warn(
99+
'[react-native-google-maps-plus] AppDelegate is not Swift; skipping GMSServices injection.'
100+
);
101+
return conf;
102+
}
103+
104+
let src = conf.modResults.contents;
105+
106+
if (!src.includes('import GoogleMaps')) {
107+
src = mergeContents({
108+
tag: 'react-native-google-maps-import',
109+
src,
110+
newSrc: 'import GoogleMaps',
111+
anchor: /^import React/m,
112+
offset: 1,
113+
comment: '//',
114+
}).contents;
115+
}
116+
117+
const initSnippet = `
118+
if let apiKey = Bundle.main.object(forInfoDictionaryKey: "MAPS_API_KEY") as? String {
119+
GMSServices.provideAPIKey(apiKey)
120+
}`;
121+
122+
try {
123+
src = mergeContents({
124+
tag: 'react-native-google-maps-init',
125+
src,
126+
newSrc: initSnippet,
127+
anchor:
128+
/return\s+super\.application\s*\(\s*application\s*,\s*didFinishLaunchingWithOptions:\s*launchOptions\s*\)/,
129+
offset: -1,
130+
comment: '//',
131+
}).contents;
132+
} catch (error: any) {
133+
if (error.code === 'ERR_NO_MATCH') {
134+
src = mergeContents({
135+
tag: 'react-native-google-maps-init',
136+
src,
137+
newSrc: initSnippet,
138+
anchor: /didFinishLaunchingWithOptions[^{]*\{/,
139+
offset: 1,
140+
comment: '//',
141+
}).contents;
142+
} else {
143+
throw error;
144+
}
145+
}
146+
147+
conf.modResults.contents = src;
148+
return conf;
149+
});
150+
151+
return config;
152+
};
153+
154+
export default withIosGoogleMapsPlus;

expoConfig/src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export type RNGoogleMapsPlusExpoPluginProps = {
2+
googleMapsAndroidApiKey: string;
3+
googleMapsIosApiKey: string;
4+
};

expoConfig/tsconfig.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"compilerOptions": {
3+
"rootDir": "src",
4+
"outDir": "build",
5+
"skipLibCheck": true,
6+
"esModuleInterop": true,
7+
"resolveJsonModule": true,
8+
"allowSyntheticDefaultImports": true
9+
},
10+
"include": ["src"]
11+
}

package.json

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"test": "jest",
2222
"git:clean": "git clean -dfX",
2323
"release": "semantic-release",
24-
"build": "yarn nitrogen && bob build",
24+
"build": "yarn nitrogen && bob build && yarn build:plugin",
25+
"build:plugin": "tsc -p expoConfig/tsconfig.json",
2526
"nitrogen": "nitrogen --logLevel=\"debug\" && node scripts/nitrogen-patch.js",
2627
"prepare": "bob build"
2728
},
@@ -33,11 +34,13 @@
3334
"google-maps-sdk",
3435
"android",
3536
"ios",
36-
"maps"
37+
"maps",
38+
"expo"
3739
],
3840
"files": [
3941
"src",
4042
"lib",
43+
"expoConfig/build",
4144
"!**/__tests__",
4245
"!**/__fixtures__",
4346
"!**/__mocks__",
@@ -108,10 +111,16 @@
108111
"semantic-release": "25.0.0-beta.6"
109112
},
110113
"peerDependencies": {
114+
"expo": "*",
111115
"react": "*",
112116
"react-native": "*",
113117
"react-native-nitro-modules": "*"
114118
},
119+
"peerDependenciesMeta": {
120+
"expo": {
121+
"optional": true
122+
}
123+
},
115124
"eslintConfig": {
116125
"root": true,
117126
"extends": [

0 commit comments

Comments
 (0)