├── android ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── expo │ │ └── modules │ │ └── dynamicappicon │ │ ├── ExpoDynamicAppIconView.kt │ │ ├── ExpoDynamicAppIconPackage.kt │ │ ├── ExpoDynamicAppIconModule.kt │ │ └── ExpoDynamicAppIconReactActivityLifecycleListener.kt └── build.gradle ├── app.plugin.js ├── src ├── types.ts ├── ExpoDynamicAppIconModule.ts ├── index.web.ts └── index.ts ├── example ├── assets │ ├── icon.png │ ├── favicon.png │ ├── splash.png │ ├── adaptive-icon.png │ ├── ios_icon_default_dark.png │ ├── ios_icon_default_light.png │ ├── android_icon_default_dark.png │ └── android_icon_default_light.png ├── tsconfig.json ├── .gitignore ├── index.js ├── webpack.config.js ├── babel.config.js ├── package.json ├── metro.config.js ├── App.tsx └── app.json ├── .eslintrc.js ├── .npmignore ├── expo-module.config.json ├── plugin ├── tsconfig.json └── src │ └── withDynamicIcon.ts ├── tsconfig.json ├── .gitignore ├── ios ├── ExpoDynamicAppIcon.podspec └── ExpoDynamicAppIconModule.swift ├── package.json └── README.md /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app.plugin.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./plugin/build/withDynamicIcon"); 2 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface DynamicAppIconRegistry { 2 | IconName: string; 3 | } 4 | -------------------------------------------------------------------------------- /example/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozzius/expo-dynamic-app-icon/HEAD/example/assets/icon.png -------------------------------------------------------------------------------- /example/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozzius/expo-dynamic-app-icon/HEAD/example/assets/favicon.png -------------------------------------------------------------------------------- /example/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozzius/expo-dynamic-app-icon/HEAD/example/assets/splash.png -------------------------------------------------------------------------------- /example/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozzius/expo-dynamic-app-icon/HEAD/example/assets/adaptive-icon.png -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['universe/native', 'universe/web'], 4 | ignorePatterns: ['build'], 5 | }; 6 | -------------------------------------------------------------------------------- /example/assets/ios_icon_default_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozzius/expo-dynamic-app-icon/HEAD/example/assets/ios_icon_default_dark.png -------------------------------------------------------------------------------- /example/assets/ios_icon_default_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozzius/expo-dynamic-app-icon/HEAD/example/assets/ios_icon_default_light.png -------------------------------------------------------------------------------- /example/assets/android_icon_default_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozzius/expo-dynamic-app-icon/HEAD/example/assets/android_icon_default_dark.png -------------------------------------------------------------------------------- /example/assets/android_icon_default_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozzius/expo-dynamic-app-icon/HEAD/example/assets/android_icon_default_light.png -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Exclude all top-level hidden directories by convention 2 | /.*/ 3 | 4 | __mocks__ 5 | __tests__ 6 | 7 | /babel.config.js 8 | /android/src/androidTest/ 9 | /android/src/test/ 10 | /android/build/ 11 | /example/ 12 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | 16 | ios/ 17 | android/ 18 | -------------------------------------------------------------------------------- /expo-module.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "platforms": ["ios", "android", "web"], 3 | "ios": { 4 | "modules": ["ExpoDynamicAppIconModule"] 5 | }, 6 | "android": { 7 | "modules": ["expo.modules.dynamicappicon.ExpoDynamicAppIconModule"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo-module-scripts/tsconfig.plugin", 3 | "compilerOptions": { 4 | "outDir": "build", 5 | "rootDir": "src" 6 | }, 7 | "include": ["./src"], 8 | "exclude": ["**/__mocks__/*", "**/__tests__/*"] 9 | } 10 | -------------------------------------------------------------------------------- /src/ExpoDynamicAppIconModule.ts: -------------------------------------------------------------------------------- 1 | import { requireNativeModule } from 'expo'; 2 | 3 | // It loads the native module object from the JSI or falls back to 4 | // the bridge module (from NativeModulesProxy) if the remote debugger is on. 5 | export default requireNativeModule('ExpoDynamicAppIcon'); 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | // @generated by expo-module-scripts 2 | { 3 | "extends": "expo-module-scripts/tsconfig.base", 4 | "compilerOptions": { 5 | "outDir": "./build" 6 | }, 7 | "include": ["./src"], 8 | "exclude": ["**/__mocks__/*", "**/__tests__/*", "**/__rsc_tests__/*"] 9 | } 10 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import { registerRootComponent } from 'expo'; 2 | 3 | import App from './App'; 4 | 5 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App); 6 | // It also ensures that whether you load the app in Expo Go or in a native build, 7 | // the environment is set up appropriately 8 | registerRootComponent(App); 9 | -------------------------------------------------------------------------------- /android/src/main/java/expo/modules/dynamicappicon/ExpoDynamicAppIconView.kt: -------------------------------------------------------------------------------- 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 ExpoDynamicAppIconView(context: Context, appContext: AppContext) : 8 | ExpoView(context, appContext) 9 | -------------------------------------------------------------------------------- /src/index.web.ts: -------------------------------------------------------------------------------- 1 | import { DynamicAppIconRegistry } from "./types"; 2 | 3 | export type IconName = DynamicAppIconRegistry["IconName"]; 4 | 5 | export function setAppIcon( 6 | name: IconName | null 7 | ): IconName | "DEFAULT" | false { 8 | console.error("setAppIcon is not supported on web"); 9 | return false; 10 | } 11 | 12 | export function getAppIcon(): IconName | "DEFAULT" { 13 | console.error("getAppIcon is not supported on web"); 14 | return ""; 15 | } 16 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import ExpoDynamicAppIconModule from "./ExpoDynamicAppIconModule"; 2 | import { DynamicAppIconRegistry } from "./types"; 3 | 4 | export type IconName = DynamicAppIconRegistry["IconName"]; 5 | 6 | export function setAppIcon( 7 | name: IconName | null 8 | ): IconName | "DEFAULT" | false { 9 | return ExpoDynamicAppIconModule.setAppIcon(name); 10 | } 11 | 12 | export function getAppIcon(): IconName | "DEFAULT" { 13 | return ExpoDynamicAppIconModule.getAppIcon(); 14 | } 15 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const createConfigAsync = require('@expo/webpack-config'); 2 | const path = require('path'); 3 | 4 | module.exports = async (env, argv) => { 5 | const config = await createConfigAsync( 6 | { 7 | ...env, 8 | babel: { 9 | dangerouslyAddModulePathsToTranspile: ['expo-dynamic-app-icon'], 10 | }, 11 | }, 12 | argv 13 | ); 14 | config.resolve.modules = [ 15 | path.resolve(__dirname, './node_modules'), 16 | path.resolve(__dirname, '../node_modules'), 17 | ]; 18 | 19 | return config; 20 | }; 21 | -------------------------------------------------------------------------------- /android/src/main/java/expo/modules/dynamicappicon/ExpoDynamicAppIconPackage.kt: -------------------------------------------------------------------------------- 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 ExpoDynamicAppIconPackage : Package { 8 | override fun createReactActivityLifecycleListeners( 9 | activityContext: Context 10 | ): List { 11 | return listOf(ExpoDynamicAppIconReactActivityLifecycleListener()) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | module.exports = function (api) { 3 | api.cache(true); 4 | return { 5 | presets: ['babel-preset-expo'], 6 | plugins: [ 7 | [ 8 | 'module-resolver', 9 | { 10 | extensions: ['.tsx', '.ts', '.js', '.json'], 11 | alias: { 12 | // For development, we want to alias the library to the source 13 | 'expo-dynamic-app-icon': path.join(__dirname, '..', 'src', 'index.ts'), 14 | }, 15 | }, 16 | ], 17 | ], 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "expo-dynamic-app-icon-example", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "start": "expo start --dev-client", 6 | "prebuild": "expo prebuild", 7 | "android": "expo run:android", 8 | "ios": "expo run:ios", 9 | "web": "expo start --web" 10 | }, 11 | "dependencies": { 12 | "@mozzius/expo-dynamic-app-icon": "link:./../", 13 | "babel-plugin-module-resolver": "^5.0.2", 14 | "expo": "^54.0.2", 15 | "expo-splash-screen": "~31.0.9", 16 | "expo-status-bar": "~3.0.8", 17 | "react": "19.1.0", 18 | "react-dom": "19.1.0", 19 | "react-native": "0.81.4", 20 | "react-native-web": "^0.21.0" 21 | }, 22 | "devDependencies": { 23 | "@babel/core": "^7.26.0", 24 | "@types/react": "~19.1.10", 25 | "typescript": "~5.9.2" 26 | }, 27 | "private": true 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # VSCode 6 | .vscode/ 7 | jsconfig.json 8 | 9 | # Xcode 10 | # 11 | build/ 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata 21 | *.xccheckout 22 | *.moved-aside 23 | DerivedData 24 | *.hmap 25 | *.ipa 26 | *.xcuserstate 27 | project.xcworkspace 28 | 29 | # Android/IJ 30 | # 31 | .classpath 32 | .cxx 33 | .gradle 34 | .idea 35 | .project 36 | .settings 37 | local.properties 38 | android.iml 39 | 40 | # Cocoapods 41 | # 42 | example/ios/Pods 43 | 44 | # Ruby 45 | example/vendor/ 46 | 47 | # node.js 48 | # 49 | node_modules/ 50 | npm-debug.log 51 | yarn-debug.log 52 | yarn-error.log 53 | 54 | # BUCK 55 | buck-out/ 56 | \.buckd/ 57 | android/app/libs 58 | android/keystores/debug.keystore 59 | 60 | # Expo 61 | .expo/* 62 | 63 | *.tsbuildinfo 64 | -------------------------------------------------------------------------------- /ios/ExpoDynamicAppIcon.podspec: -------------------------------------------------------------------------------- 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 = 'ExpoDynamicAppIcon' 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 28 | -------------------------------------------------------------------------------- /example/metro.config.js: -------------------------------------------------------------------------------- 1 | // Learn more https://docs.expo.io/guides/customizing-metro 2 | const { getDefaultConfig } = require('expo/metro-config'); 3 | const path = require('path'); 4 | 5 | const config = getDefaultConfig(__dirname); 6 | 7 | // npm v7+ will install ../node_modules/react-native because of peerDependencies. 8 | // To prevent the incompatible react-native bewtween ./node_modules/react-native and ../node_modules/react-native, 9 | // excludes the one from the parent folder when bundling. 10 | config.resolver.blockList = [ 11 | ...Array.from(config.resolver.blockList ?? []), 12 | new RegExp(path.resolve('..', 'node_modules', 'react-native')), 13 | ]; 14 | 15 | config.resolver.nodeModulesPaths = [ 16 | path.resolve(__dirname, './node_modules'), 17 | path.resolve(__dirname, '../node_modules'), 18 | ]; 19 | 20 | config.watchFolders = [path.resolve(__dirname, '..')]; 21 | 22 | config.transformer.getTransformOptions = async () => ({ 23 | transform: { 24 | experimentalImportSupport: false, 25 | inlineRequires: true, 26 | }, 27 | }); 28 | 29 | module.exports = config; -------------------------------------------------------------------------------- /ios/ExpoDynamicAppIconModule.swift: -------------------------------------------------------------------------------- 1 | import ExpoModulesCore 2 | 3 | public class ExpoDynamicAppIconModule: Module { 4 | public func definition() -> ModuleDefinition { 5 | 6 | Name("ExpoDynamicAppIcon") 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 | } 35 | -------------------------------------------------------------------------------- /example/App.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Text, View } from "react-native"; 2 | 3 | import { 4 | getAppIcon, 5 | IconName, 6 | setAppIcon, 7 | } from "@mozzius/expo-dynamic-app-icon"; 8 | import { useState } from "react"; 9 | 10 | export default function App() { 11 | const [iconName, setIconName] = useState(); 12 | 13 | return ( 14 | 22 | 23 |