expo-app-icon 1.0.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.
Files changed (40) hide show
  1. package/README.md +62 -0
  2. package/android/build.gradle +43 -0
  3. package/android/src/main/AndroidManifest.xml +2 -0
  4. package/android/src/main/java/expo/modules/dynamicappicon/ExpoAppIconChangerModule.kt +81 -0
  5. package/android/src/main/java/expo/modules/dynamicappicon/ExpoAppIconChangerPackage.kt +13 -0
  6. package/android/src/main/java/expo/modules/dynamicappicon/ExpoAppIconChangerReactActivityLifecycleListener.kt +224 -0
  7. package/android/src/main/java/expo/modules/dynamicappicon/ExpoAppIconChangerView.kt +8 -0
  8. package/app.plugin.js +1 -0
  9. package/build/ExpoAppIconChangerModule.d.ts +3 -0
  10. package/build/ExpoAppIconChangerModule.d.ts.map +1 -0
  11. package/build/ExpoAppIconChangerModule.js +5 -0
  12. package/build/ExpoAppIconChangerModule.js.map +1 -0
  13. package/build/index.d.ts +21 -0
  14. package/build/index.d.ts.map +1 -0
  15. package/build/index.js +21 -0
  16. package/build/index.js.map +1 -0
  17. package/build/index.web.d.ts +12 -0
  18. package/build/index.web.d.ts.map +1 -0
  19. package/build/index.web.js +16 -0
  20. package/build/index.web.js.map +1 -0
  21. package/build/types.d.ts +4 -0
  22. package/build/types.d.ts.map +1 -0
  23. package/build/types.js +2 -0
  24. package/build/types.js.map +1 -0
  25. package/expo-module.config.json +9 -0
  26. package/ios/ExpoAppIconChanger.podspec +27 -0
  27. package/ios/ExpoAppIconChangerModule.swift +34 -0
  28. package/package.json +59 -0
  29. package/plugin/build/android-resources.d.ts +89 -0
  30. package/plugin/build/android-resources.js +151 -0
  31. package/plugin/build/android.d.ts +18 -0
  32. package/plugin/build/android.js +203 -0
  33. package/plugin/build/apple.d.ts +14 -0
  34. package/plugin/build/apple.js +143 -0
  35. package/plugin/build/icons.d.ts +23 -0
  36. package/plugin/build/icons.js +62 -0
  37. package/plugin/build/index.d.ts +8 -0
  38. package/plugin/build/index.js +46 -0
  39. package/plugin/build/types.d.ts +62 -0
  40. package/plugin/build/types.js +2 -0
package/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # expo-app-icon
2
+
3
+ Programmatically change your app's icon at runtime in Expo, with proper Android adaptive-icon support.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npx expo install expo-app-icon
9
+ ```
10
+
11
+ ## Configure
12
+
13
+ Add the plugin to your app config and declare your icons. Each icon needs an `ios` and/or `android` image path:
14
+
15
+ ```json
16
+ {
17
+ "expo": {
18
+ "plugins": [
19
+ [
20
+ "expo-app-icon",
21
+ {
22
+ "red": {
23
+ "ios": "./assets/icons/red.png",
24
+ "android": "./assets/icons/red.png"
25
+ },
26
+ "blue": {
27
+ "ios": "./assets/icons/blue.png",
28
+ "android": "./assets/icons/blue.png"
29
+ }
30
+ }
31
+ ]
32
+ ]
33
+ }
34
+ }
35
+ ```
36
+
37
+ Then create a new build (the plugin runs during prebuild):
38
+
39
+ ```sh
40
+ npx expo prebuild --clean
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ ```ts
46
+ import { getAppIcon, setAppIcon } from "expo-app-icon";
47
+
48
+ // Get the name of the current icon ("DEFAULT" when none is set)
49
+ const current = getAppIcon();
50
+
51
+ // Switch to one of your configured icons by key
52
+ setAppIcon("red");
53
+
54
+ // Reset back to the default icon
55
+ setAppIcon(null);
56
+ ```
57
+
58
+ ## Notes
59
+
60
+ - Icon keys are the keys you define in the plugin config.
61
+ - Android icons are generated as adaptive icons. Source images are scaled into the adaptive safe zone, so design your artwork edge-to-edge — it will be centered automatically.
62
+ - Changing the icon on Android briefly closes and reopens the app.
@@ -0,0 +1,43 @@
1
+ apply plugin: 'com.android.library'
2
+
3
+ group = 'expo.modules.dynamicappicon'
4
+ version = '0.6.0'
5
+
6
+ def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
7
+ apply from: expoModulesCorePlugin
8
+ applyKotlinExpoModulesCorePlugin()
9
+ useCoreDependencies()
10
+ useExpoPublishing()
11
+
12
+ // If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
13
+ // The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
14
+ // Most of the time, you may like to manage the Android SDK versions yourself.
15
+ def useManagedAndroidSdkVersions = false
16
+ if (useManagedAndroidSdkVersions) {
17
+ useDefaultAndroidSdkVersions()
18
+ } else {
19
+ buildscript {
20
+ // Simple helper that allows the root project to override versions declared by this library.
21
+ ext.safeExtGet = { prop, fallback ->
22
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
23
+ }
24
+ }
25
+ project.android {
26
+ compileSdkVersion safeExtGet("compileSdkVersion", 34)
27
+ defaultConfig {
28
+ minSdkVersion safeExtGet("minSdkVersion", 21)
29
+ targetSdkVersion safeExtGet("targetSdkVersion", 34)
30
+ }
31
+ }
32
+ }
33
+
34
+ android {
35
+ namespace "expo.modules.dynamicappicon"
36
+ defaultConfig {
37
+ versionCode 1
38
+ versionName "0.6.0"
39
+ }
40
+ lintOptions {
41
+ abortOnError false
42
+ }
43
+ }
@@ -0,0 +1,2 @@
1
+ <manifest>
2
+ </manifest>
@@ -0,0 +1,81 @@
1
+ package expo.modules.dynamicappicon
2
+
3
+ import android.content.Context
4
+ import expo.modules.kotlin.modules.Module
5
+ import expo.modules.kotlin.modules.ModuleDefinition
6
+
7
+ class ExpoAppIconChangerModule : Module() {
8
+
9
+ override fun definition() = ModuleDefinition {
10
+ Name("ExpoAppIconChanger")
11
+
12
+ Function("setAppIcon") { name: String? ->
13
+ try {
14
+ SharedObject.packageName = context.packageName
15
+ SharedObject.pm = pm
16
+ SharedObject.shouldChangeIcon = true
17
+
18
+ var result: String
19
+
20
+ // The default icon is served by a dedicated alias so the real
21
+ // MainActivity is never disabled (see the config plugin).
22
+ val defaultIcon = context.packageName + ".MainActivity" + "DEFAULT"
23
+
24
+ if (name == null) {
25
+ // Resetting to default icon if nothing passed
26
+ var currentIcon =
27
+ if (!SharedObject.icon.isEmpty()) SharedObject.icon
28
+ else defaultIcon
29
+
30
+ SharedObject.classesToKill.add(currentIcon)
31
+ SharedObject.icon = defaultIcon
32
+ result = "DEFAULT"
33
+ } else {
34
+ var newIcon = context.packageName + ".MainActivity" + name
35
+ var currentIcon =
36
+ if (!SharedObject.icon.isEmpty()) SharedObject.icon
37
+ else defaultIcon
38
+
39
+ if (currentIcon == newIcon) {
40
+ return@Function name
41
+ }
42
+
43
+ SharedObject.classesToKill.add(currentIcon)
44
+ SharedObject.icon = newIcon
45
+ result = name
46
+ }
47
+
48
+ // background the app to trigger icon change
49
+ try {
50
+ currentActivity.moveTaskToBack(true)
51
+ } catch (e: Exception) {
52
+ // do nothing
53
+ }
54
+
55
+ return@Function result
56
+ } catch (e: Exception) {
57
+ return@Function false
58
+ }
59
+ }
60
+
61
+ Function("getAppIcon") {
62
+ val componentClass: String = currentActivity.componentName.className
63
+ val currentIcon: String =
64
+ if (SharedObject.icon.isNotEmpty()) SharedObject.icon else componentClass
65
+ val parts = currentIcon.split("MainActivity")
66
+ val currentIconName = if (parts.size > 1) parts[1] else ""
67
+
68
+ return@Function if (currentIconName.isEmpty() || currentIconName == "DEFAULT") "DEFAULT"
69
+ else currentIconName
70
+ }
71
+ }
72
+
73
+ private val context: Context
74
+ get() = requireNotNull(appContext.reactContext) { "React Application Context is null" }
75
+
76
+ private val currentActivity
77
+ get() = requireNotNull(appContext.activityProvider?.currentActivity)
78
+
79
+ private val pm
80
+ get() = requireNotNull(currentActivity.packageManager)
81
+ }
@@ -0,0 +1,13 @@
1
+ package expo.modules.dynamicappicon
2
+
3
+ import android.content.Context
4
+ import expo.modules.core.interfaces.Package
5
+ import expo.modules.core.interfaces.ReactActivityLifecycleListener
6
+
7
+ class ExpoAppIconChangerPackage : Package {
8
+ override fun createReactActivityLifecycleListeners(
9
+ activityContext: Context
10
+ ): List<ReactActivityLifecycleListener> {
11
+ return listOf(ExpoAppIconChangerReactActivityLifecycleListener())
12
+ }
13
+ }
@@ -0,0 +1,224 @@
1
+ package expo.modules.dynamicappicon
2
+
3
+ import android.app.Activity
4
+ import android.content.ComponentName
5
+ import android.content.Context
6
+ import android.content.pm.PackageManager
7
+ import android.os.Handler
8
+ import android.os.Looper
9
+ import android.util.Log
10
+ import expo.modules.core.interfaces.ReactActivityLifecycleListener
11
+
12
+ object SharedObject {
13
+ var packageName: String = ""
14
+ var classesToKill = ArrayList<String>()
15
+ var icon: String = ""
16
+ var pm: PackageManager? = null
17
+ var shouldChangeIcon: Boolean = false
18
+ }
19
+
20
+ class ExpoAppIconChangerReactActivityLifecycleListener : ReactActivityLifecycleListener {
21
+ private var currentActivity: Activity? = null
22
+ private val handler = Handler(Looper.getMainLooper())
23
+
24
+ override fun onPause(activity: Activity) {
25
+ currentActivity = activity
26
+ // Apply icon change immediately when app goes to background
27
+ if (SharedObject.shouldChangeIcon) {
28
+ applyIconChange(activity)
29
+ // Force close the app after icon change to ensure clean restart
30
+ handler.postDelayed(
31
+ { forceCloseApp(activity) },
32
+ 500
33
+ ) // Small delay to ensure icon change completes
34
+ }
35
+ }
36
+
37
+ override fun onResume(activity: Activity) {
38
+ currentActivity = activity
39
+ // Repair any non-icon activities that were incorrectly disabled by older versions
40
+ repairIncorrectlyDisabledActivities(activity)
41
+ }
42
+
43
+ override fun onDestroy(activity: Activity) {
44
+ if (SharedObject.shouldChangeIcon) {
45
+ applyIconChange(activity)
46
+ }
47
+ if (currentActivity === activity) {
48
+ currentActivity = null
49
+ }
50
+ }
51
+
52
+ private fun forceCloseApp(activity: Activity) {
53
+ try {
54
+ // Force close the app process to ensure clean restart
55
+ activity.finishAffinity()
56
+ android.os.Process.killProcess(android.os.Process.myPid())
57
+ } catch (e: Exception) {
58
+ Log.e("IconChange", "Error force closing app", e)
59
+ }
60
+ }
61
+
62
+ private fun applyIconChange(activity: Activity) {
63
+ SharedObject.icon.takeIf { it.isNotEmpty() }?.let { icon ->
64
+ val pm = SharedObject.pm ?: return
65
+ val newComponent = ComponentName(SharedObject.packageName, icon)
66
+
67
+ if (!doesComponentExist(newComponent)) {
68
+ SharedObject.shouldChangeIcon = false
69
+ return
70
+ }
71
+
72
+ try {
73
+ // Get all launcher activities and disable all icon aliases except the new one
74
+ val packageInfo =
75
+ pm.getPackageInfo(
76
+ SharedObject.packageName,
77
+ PackageManager.GET_ACTIVITIES or
78
+ PackageManager.GET_DISABLED_COMPONENTS
79
+ )
80
+
81
+ // Only disable activities that are icon aliases (named MainActivity*).
82
+ // The bare MainActivity is deliberately excluded: it must stay
83
+ // enabled so tools that launch it by explicit component (e.g. the
84
+ // Expo dev client) keep working. Its launcher role is owned by the
85
+ // DEFAULT alias instead (see the config plugin).
86
+ val mainActivityPrefix = "${SharedObject.packageName}.MainActivity"
87
+ val bareMainActivity = "${SharedObject.packageName}.MainActivity"
88
+
89
+ packageInfo.activities?.forEach { activityInfo ->
90
+ val componentName = ComponentName(SharedObject.packageName, activityInfo.name)
91
+ val state = pm.getComponentEnabledSetting(componentName)
92
+
93
+ // Only manage the icon aliases, leave MainActivity and other activities alone
94
+ val isIconAlias = activityInfo.name.startsWith(mainActivityPrefix) &&
95
+ activityInfo.name != bareMainActivity
96
+
97
+ if (isIconAlias &&
98
+ activityInfo.name != icon &&
99
+ state != PackageManager.COMPONENT_ENABLED_STATE_DISABLED
100
+ ) {
101
+ pm.setComponentEnabledSetting(
102
+ componentName,
103
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
104
+ PackageManager.DONT_KILL_APP
105
+ )
106
+ Log.i("IconChange", "Disabled component: ${activityInfo.name}")
107
+ }
108
+ }
109
+
110
+ // Enable the new icon
111
+ pm.setComponentEnabledSetting(
112
+ newComponent,
113
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
114
+ PackageManager.DONT_KILL_APP
115
+ )
116
+ Log.i("IconChange", "Enabled new icon: $icon")
117
+ } catch (e: Exception) {
118
+ Log.e("IconChange", "Error during icon change", e)
119
+ } finally {
120
+ SharedObject.shouldChangeIcon = false
121
+ }
122
+
123
+ // Ensure at least one component is enabled
124
+ ensureAtLeastOneComponentEnabled(activity)
125
+ }
126
+ }
127
+
128
+ private fun ensureAtLeastOneComponentEnabled(context: Context) {
129
+ val pm = SharedObject.pm ?: return
130
+ val packageInfo =
131
+ pm.getPackageInfo(
132
+ SharedObject.packageName,
133
+ PackageManager.GET_ACTIVITIES or PackageManager.GET_DISABLED_COMPONENTS
134
+ )
135
+
136
+ val hasEnabledComponent =
137
+ packageInfo.activities?.any { activityInfo ->
138
+ val componentName = ComponentName(SharedObject.packageName, activityInfo.name)
139
+ pm.getComponentEnabledSetting(componentName) ==
140
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED
141
+ }
142
+ ?: false
143
+
144
+ if (!hasEnabledComponent) {
145
+ // Fall back to the DEFAULT alias (the launcher), not the bare
146
+ // MainActivity, which intentionally has no launcher filter.
147
+ val defaultAlias = "${SharedObject.packageName}.MainActivityDEFAULT"
148
+ val defaultComponent = ComponentName(SharedObject.packageName, defaultAlias)
149
+ try {
150
+ pm.setComponentEnabledSetting(
151
+ defaultComponent,
152
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
153
+ PackageManager.DONT_KILL_APP
154
+ )
155
+ Log.i("IconChange", "No active component found. Re-enabling $defaultAlias")
156
+ } catch (e: Exception) {
157
+ Log.e("IconChange", "Error enabling fallback DEFAULT alias", e)
158
+ }
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Repair activities that were incorrectly disabled by older versions of this library.
164
+ * Re-enables any non-icon-alias activities that are currently disabled.
165
+ */
166
+ private fun repairIncorrectlyDisabledActivities(activity: Activity) {
167
+ val pm = activity.packageManager
168
+ val packageName = activity.packageName
169
+
170
+ try {
171
+ val packageInfo =
172
+ pm.getPackageInfo(
173
+ packageName,
174
+ PackageManager.GET_ACTIVITIES or PackageManager.GET_DISABLED_COMPONENTS
175
+ )
176
+
177
+ val mainActivityPrefix = "${packageName}.MainActivity"
178
+ val bareMainActivity = "${packageName}.MainActivity"
179
+
180
+ packageInfo.activities?.forEach { activityInfo ->
181
+ // The bare MainActivity is not an icon alias and must stay enabled,
182
+ // so a previously-disabled MainActivity gets repaired here too.
183
+ val isIconAlias = activityInfo.name.startsWith(mainActivityPrefix) &&
184
+ activityInfo.name != bareMainActivity
185
+
186
+ // If it's NOT an icon alias, it should never have been disabled by us
187
+ if (!isIconAlias) {
188
+ val componentName = ComponentName(packageName, activityInfo.name)
189
+ val state = pm.getComponentEnabledSetting(componentName)
190
+
191
+ // Re-enable if it was explicitly disabled
192
+ if (state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED) {
193
+ pm.setComponentEnabledSetting(
194
+ componentName,
195
+ PackageManager.COMPONENT_ENABLED_STATE_DEFAULT,
196
+ PackageManager.DONT_KILL_APP
197
+ )
198
+ Log.i("IconChange", "Repaired incorrectly disabled activity: ${activityInfo.name}")
199
+ }
200
+ }
201
+ }
202
+ } catch (e: Exception) {
203
+ Log.e("IconChange", "Error repairing disabled activities", e)
204
+ }
205
+ }
206
+
207
+ /** Check if a component exists in the manifest (including disabled ones). */
208
+ private fun doesComponentExist(componentName: ComponentName): Boolean {
209
+ return try {
210
+ val packageInfo =
211
+ SharedObject.pm?.getPackageInfo(
212
+ SharedObject.packageName,
213
+ PackageManager.GET_ACTIVITIES or PackageManager.GET_DISABLED_COMPONENTS
214
+ )
215
+
216
+ val activityExists =
217
+ packageInfo?.activities?.any { it.name == componentName.className } == true
218
+
219
+ activityExists
220
+ } catch (e: Exception) {
221
+ false
222
+ }
223
+ }
224
+ }
@@ -0,0 +1,8 @@
1
+ package expo.modules.dynamicappicon
2
+
3
+ import android.content.Context
4
+ import expo.modules.kotlin.AppContext
5
+ import expo.modules.kotlin.views.ExpoView
6
+
7
+ class ExpoAppIconChangerView(context: Context, appContext: AppContext) :
8
+ ExpoView(context, appContext)
package/app.plugin.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require("./plugin/build");
@@ -0,0 +1,3 @@
1
+ declare const _default: any;
2
+ export default _default;
3
+ //# sourceMappingURL=ExpoAppIconChangerModule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoAppIconChangerModule.d.ts","sourceRoot":"","sources":["../src/ExpoAppIconChangerModule.ts"],"names":[],"mappings":";AAIA,wBAAyD"}
@@ -0,0 +1,5 @@
1
+ import { requireNativeModule } from 'expo';
2
+ // It loads the native module object from the JSI or falls back to
3
+ // the bridge module (from NativeModulesProxy) if the remote debugger is on.
4
+ export default requireNativeModule('ExpoAppIconChanger');
5
+ //# sourceMappingURL=ExpoAppIconChangerModule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoAppIconChangerModule.js","sourceRoot":"","sources":["../src/ExpoAppIconChangerModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAE3C,kEAAkE;AAClE,4EAA4E;AAC5E,eAAe,mBAAmB,CAAC,oBAAoB,CAAC,CAAC","sourcesContent":["import { requireNativeModule } from 'expo';\n\n// It loads the native module object from the JSI or falls back to\n// the bridge module (from NativeModulesProxy) if the remote debugger is on.\nexport default requireNativeModule('ExpoAppIconChanger');\n"]}
@@ -0,0 +1,21 @@
1
+ import type { DynamicAppIconRegistry } from "./types";
2
+ /**
3
+ * Union of the icon keys declared in the plugin config (widened to string when none).
4
+ */
5
+ export type IconName = DynamicAppIconRegistry["IconName"];
6
+ /**
7
+ * Value reported / accepted for the project's default launcher icon.
8
+ */
9
+ export declare const DEFAULT_ICON: "DEFAULT";
10
+ /**
11
+ * Switch the launcher icon at runtime.
12
+ *
13
+ * @param name A configured icon key, or `null` to reset to the default icon.
14
+ * @returns The applied icon name, `"DEFAULT"`, or `false` if unsupported.
15
+ */
16
+ export declare function setAppIcon(name: IconName | null): IconName | typeof DEFAULT_ICON | false;
17
+ /**
18
+ * Get the currently active icon name, or `"DEFAULT"` when none is set.
19
+ */
20
+ export declare function getAppIcon(): IconName | typeof DEFAULT_ICON;
21
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAGtD;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,sBAAsB,CAAC,UAAU,CAAC,CAAC;AAE1D;;GAEG;AACH,eAAO,MAAM,YAAY,EAAG,SAAkB,CAAC;AAE/C;;;;;GAKG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,QAAQ,GAAG,IAAI,GACpB,QAAQ,GAAG,OAAO,YAAY,GAAG,KAAK,CAExC;AAED;;GAEG;AACH,wBAAgB,UAAU,IAAI,QAAQ,GAAG,OAAO,YAAY,CAE3D"}
package/build/index.js ADDED
@@ -0,0 +1,21 @@
1
+ import NativeAppIcon from "./ExpoAppIconChangerModule";
2
+ /**
3
+ * Value reported / accepted for the project's default launcher icon.
4
+ */
5
+ export const DEFAULT_ICON = "DEFAULT";
6
+ /**
7
+ * Switch the launcher icon at runtime.
8
+ *
9
+ * @param name A configured icon key, or `null` to reset to the default icon.
10
+ * @returns The applied icon name, `"DEFAULT"`, or `false` if unsupported.
11
+ */
12
+ export function setAppIcon(name) {
13
+ return NativeAppIcon.setAppIcon(name);
14
+ }
15
+ /**
16
+ * Get the currently active icon name, or `"DEFAULT"` when none is set.
17
+ */
18
+ export function getAppIcon() {
19
+ return NativeAppIcon.getAppIcon();
20
+ }
21
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,aAAa,MAAM,4BAA4B,CAAC;AAOvD;;GAEG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,SAAkB,CAAC;AAE/C;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CACxB,IAAqB;IAErB,OAAO,aAAa,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;AACxC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,UAAU;IACxB,OAAO,aAAa,CAAC,UAAU,EAAE,CAAC;AACpC,CAAC","sourcesContent":["import type { DynamicAppIconRegistry } from \"./types\";\nimport NativeAppIcon from \"./ExpoAppIconChangerModule\";\n\n/**\n * Union of the icon keys declared in the plugin config (widened to string when none).\n */\nexport type IconName = DynamicAppIconRegistry[\"IconName\"];\n\n/**\n * Value reported / accepted for the project's default launcher icon.\n */\nexport const DEFAULT_ICON = \"DEFAULT\" as const;\n\n/**\n * Switch the launcher icon at runtime.\n *\n * @param name A configured icon key, or `null` to reset to the default icon.\n * @returns The applied icon name, `\"DEFAULT\"`, or `false` if unsupported.\n */\nexport function setAppIcon(\n name: IconName | null\n): IconName | typeof DEFAULT_ICON | false {\n return NativeAppIcon.setAppIcon(name);\n}\n\n/**\n * Get the currently active icon name, or `\"DEFAULT\"` when none is set.\n */\nexport function getAppIcon(): IconName | typeof DEFAULT_ICON {\n return NativeAppIcon.getAppIcon();\n}\n"]}
@@ -0,0 +1,12 @@
1
+ import type { DynamicAppIconRegistry } from "./types";
2
+ export type IconName = DynamicAppIconRegistry["IconName"];
3
+ export declare const DEFAULT_ICON: "DEFAULT";
4
+ /**
5
+ * Web has no launcher icon; this is a no-op that reports failure.
6
+ */
7
+ export declare function setAppIcon(_name: IconName | null): IconName | typeof DEFAULT_ICON | false;
8
+ /**
9
+ * Web has no launcher icon; always reports the default.
10
+ */
11
+ export declare function getAppIcon(): IconName | typeof DEFAULT_ICON;
12
+ //# sourceMappingURL=index.web.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.web.d.ts","sourceRoot":"","sources":["../src/index.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAEtD,MAAM,MAAM,QAAQ,GAAG,sBAAsB,CAAC,UAAU,CAAC,CAAC;AAE1D,eAAO,MAAM,YAAY,EAAG,SAAkB,CAAC;AAE/C;;GAEG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,QAAQ,GAAG,IAAI,GACrB,QAAQ,GAAG,OAAO,YAAY,GAAG,KAAK,CAGxC;AAED;;GAEG;AACH,wBAAgB,UAAU,IAAI,QAAQ,GAAG,OAAO,YAAY,CAG3D"}
@@ -0,0 +1,16 @@
1
+ export const DEFAULT_ICON = "DEFAULT";
2
+ /**
3
+ * Web has no launcher icon; this is a no-op that reports failure.
4
+ */
5
+ export function setAppIcon(_name) {
6
+ console.error("setAppIcon is not supported on web");
7
+ return false;
8
+ }
9
+ /**
10
+ * Web has no launcher icon; always reports the default.
11
+ */
12
+ export function getAppIcon() {
13
+ console.error("getAppIcon is not supported on web");
14
+ return DEFAULT_ICON;
15
+ }
16
+ //# sourceMappingURL=index.web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.web.js","sourceRoot":"","sources":["../src/index.web.ts"],"names":[],"mappings":"AAIA,MAAM,CAAC,MAAM,YAAY,GAAG,SAAkB,CAAC;AAE/C;;GAEG;AACH,MAAM,UAAU,UAAU,CACxB,KAAsB;IAEtB,OAAO,CAAC,KAAK,CAAC,oCAAoC,CAAC,CAAC;IACpD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,UAAU;IACxB,OAAO,CAAC,KAAK,CAAC,oCAAoC,CAAC,CAAC;IACpD,OAAO,YAAY,CAAC;AACtB,CAAC","sourcesContent":["import type { DynamicAppIconRegistry } from \"./types\";\n\nexport type IconName = DynamicAppIconRegistry[\"IconName\"];\n\nexport const DEFAULT_ICON = \"DEFAULT\" as const;\n\n/**\n * Web has no launcher icon; this is a no-op that reports failure.\n */\nexport function setAppIcon(\n _name: IconName | null\n): IconName | typeof DEFAULT_ICON | false {\n console.error(\"setAppIcon is not supported on web\");\n return false;\n}\n\n/**\n * Web has no launcher icon; always reports the default.\n */\nexport function getAppIcon(): IconName | typeof DEFAULT_ICON {\n console.error(\"getAppIcon is not supported on web\");\n return DEFAULT_ICON;\n}\n"]}
@@ -0,0 +1,4 @@
1
+ export interface DynamicAppIconRegistry {
2
+ IconName: string;
3
+ }
4
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;CAClB"}
package/build/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["export interface DynamicAppIconRegistry {\n IconName: string;\n}\n"]}
@@ -0,0 +1,9 @@
1
+ {
2
+ "platforms": ["ios", "android", "web"],
3
+ "ios": {
4
+ "modules": ["ExpoAppIconChangerModule"]
5
+ },
6
+ "android": {
7
+ "modules": ["expo.modules.dynamicappicon.ExpoAppIconChangerModule"]
8
+ }
9
+ }
@@ -0,0 +1,27 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = 'ExpoAppIconChanger'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.description = package['description']
10
+ s.license = package['license']
11
+ s.author = package['author']
12
+ s.homepage = package['homepage']
13
+ s.platform = :ios, '13.0'
14
+ s.swift_version = '5.4'
15
+ s.source = { git: 'https://github.com/outsung/expo-dynamic-app-icon' }
16
+ s.static_framework = true
17
+
18
+ s.dependency 'ExpoModulesCore'
19
+
20
+ # Swift/Objective-C compatibility
21
+ s.pod_target_xcconfig = {
22
+ 'DEFINES_MODULE' => 'YES',
23
+ 'SWIFT_COMPILATION_MODE' => 'wholemodule'
24
+ }
25
+
26
+ s.source_files = "**/*.{h,m,swift}"
27
+ end
@@ -0,0 +1,34 @@
1
+ import ExpoModulesCore
2
+
3
+ public class ExpoAppIconChangerModule: Module {
4
+ public func definition() -> ModuleDefinition {
5
+
6
+ Name("ExpoAppIconChanger")
7
+
8
+ Function("setAppIcon") { (name: String?) -> String in
9
+ self.setAppIcon(name)
10
+
11
+ // Return "DEFAULT" if name is nil or empty
12
+ return name ?? "DEFAULT"
13
+ }
14
+
15
+ Function("getAppIcon") { () -> String in
16
+ // Return the current alternate icon name or "DEFAULT" if none is set
17
+ return UIApplication.shared.alternateIconName ?? "DEFAULT"
18
+ }
19
+ }
20
+
21
+ private func setAppIcon(_ iconName: String?) {
22
+ if UIApplication.shared.responds(to: #selector(getter: UIApplication.supportsAlternateIcons)) && UIApplication.shared.supportsAlternateIcons {
23
+ let iconNameToUse = iconName?.isEmpty == false ? iconName : nil // If the icon name is nil or empty, reset to default
24
+
25
+ // Set the alternate icon or reset to the default icon
26
+ UIApplication.shared.setAlternateIconName(iconNameToUse, completionHandler: { error in
27
+ if let error = error {
28
+ // Handle error if necessary
29
+ print("Failed to set app icon: \(error.localizedDescription)")
30
+ }
31
+ })
32
+ }
33
+ }
34
+ }