react-native-icon-switcher 0.1.7 → 0.1.8

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 CHANGED
@@ -22,7 +22,7 @@ Switch the app icon at runtime on React Native and Expo from one config file, wi
22
22
  npx expo install react-native-icon-switcher
23
23
  ```
24
24
 
25
- Add the config plugin in `app.json` / `app.config.ts` (icon paths can live here or in `icon-switcher.config.ts` at the project root — see [Config](#config)):
25
+ Add the config plugin in `app.json` / `app.config.ts` and pass the icon config as its options (same shape as [Config](#config)):
26
26
 
27
27
  ```ts
28
28
  export default {
@@ -44,6 +44,20 @@ export default {
44
44
 
45
45
  Then run `npx expo prebuild` or use a development build. Expo Go is not supported.
46
46
 
47
+ The plugin only reads its inline options; it does not look for `icon-switcher.config.ts` on its own. To keep the config in its own file (for example to share it with the `sync` CLI), import it in `app.config.ts` with the explicit extension:
48
+
49
+ ```ts
50
+ import iconSwitcherConfig from './icon-switcher.config.ts';
51
+
52
+ export default {
53
+ expo: {
54
+ plugins: [['react-native-icon-switcher', iconSwitcherConfig]],
55
+ },
56
+ };
57
+ ```
58
+
59
+ Expo transpiles `app.config.ts` but not the files it imports, so this relies on Node's built-in TypeScript type stripping (Node 22.18 or later). On older Node versions, write the config as `icon-switcher.config.js` instead.
60
+
47
61
  ### Bare React Native
48
62
 
49
63
  ```sh
@@ -61,13 +75,27 @@ npx react-native-icon-switcher sync
61
75
 
62
76
  Use `npx react-native-icon-switcher sync --check` in CI to fail when generated native files or `icon-switcher.d.ts` are out of date.
63
77
 
78
+ ```sh
79
+ react-native-icon-switcher sync [--check] [--project-root <dir>] [--config <path>]
80
+ ```
81
+
82
+ | Flag | Default | Description |
83
+ | --- | --- | --- |
84
+ | `--check` | off | Write nothing; print each out-of-date path and exit `1` if any. |
85
+ | `--project-root <dir>` | current directory | App root containing `ios/` and `android/` (useful in monorepos). |
86
+ | `--config <path>` | first of `icon-switcher.config.{ts,js,mjs,cjs}` in the project root | Config file to load. TypeScript is loaded directly, with no compile step. |
87
+
88
+ Exit codes: `0` success (or no drift with `--check`), `1` drift found by `--check`, `2` invalid arguments, config errors, or an unexpected project layout. Without `--check` it prints `updated N files, unchanged M`.
89
+
64
90
  **No `expo` package is required** for bare apps: this library is a TurboModule with a standalone `sync` CLI. You only need `expo` if you opt into the Expo config plugin workflow.
65
91
 
66
92
  See `example/` (Expo) and `example-bare/` (bare CLI + `sync`) in this repo.
67
93
 
68
94
  ## Config
69
95
 
70
- Bare apps use `icon-switcher.config.ts` at the project root. Expo apps can pass the same shape as plugin options or point the plugin at a config file.
96
+ Bare apps use `icon-switcher.config.ts` at the project root. Expo apps pass the same shape as plugin options (or import that file, see [Expo](#expo)).
97
+
98
+ `defineIcons` has no runtime effect: it returns its argument unchanged and exists so TypeScript checks the object as you write it (misspelled keys, wrong `applyOn` values, a missing adaptive `background`) and offers autocomplete. Value rules are checked when `sync` or `expo prebuild` runs, and every problem is reported at once.
71
99
 
72
100
  ```ts
73
101
  import { defineIcons } from 'react-native-icon-switcher/config';
@@ -105,12 +133,31 @@ Per-icon `ios` / `android` fields accept a path or structured variants (`IosIcon
105
133
 
106
134
  Icons with no art for a platform are omitted from that platform's generated outputs.
107
135
 
136
+ **Validation:** `sync` and the Expo plugin fail if `icons` is empty, a name does not match `/^[a-z][a-z0-9_]*$/` or uses `default`, a colour is not `#RGB` / `#RRGGBB`, an image file does not exist, or Expo's default icon and `default` point at different files.
137
+
108
138
  **Android reserved names:** names may not be `round`, `foreground`, `background`, or `monochrome`, nor end in `_round`, `_foreground`, `_background`, or `_monochrome`.
109
139
 
140
+ **Android adaptive icons:** layers are 108×108 dp, but launchers show only the central 72×72 dp and then mask it (circle, squircle, …). Keep logos and text inside that centre, about the middle two thirds of the image. To fit artwork that fills its square, scale it into the centre of the `background` image on a matching colour and use a transparent `foreground`.
141
+
110
142
  **Legacy Android icons (API 24–25):** besides the adaptive layers and `mipmap-anydpi-v26` XML, `sync` and the plugin write `ic_launcher*.png` the way Android Studio's Image Asset tool does: the visible centre of an adaptive icon (72 dp of the 108 dp layers; a plain path uses the whole image) in a 38 dp rounded square or a 44 dp circle, with a soft drop shadow, on a transparent 48 dp canvas.
111
143
 
144
+ **Variants (experimental):** iOS `dark` / `tinted` and Android `monochrome` (themed icons, Android 13+) are generated when set, but have not yet been verified on devices.
145
+
146
+ ### What `sync` and the plugin generate
147
+
148
+ | | iOS | Android |
149
+ | --- | --- | --- |
150
+ | Images | `AppIcon-<name>.appiconset` in `Images.xcassets` (1024 px, flattened onto `ios.flattenOn`) | `mipmap-*/ic_launcher_<name>*` in five densities, plus `mipmap-anydpi-v26` adaptive XML |
151
+ | Registration | `ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES` and `ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS` build settings | One `activity-alias` per icon, `.IconSwitcher_<name>`; `MainActivity` loses its launcher filter |
152
+ | Runtime list | `IconSwitcherIcons` array in `Info.plist` | `com.iconswitcher.*` meta-data in `AndroidManifest.xml` |
153
+ | Types | `icon-switcher.d.ts` at the project root | same file |
154
+
155
+ Xcode generates `CFBundleIcons` from the build settings, so the library does not write it. Other content in these files is left as it is: comments, formatting, and the quoting CocoaPods applies to the pbxproj, so `sync --check` stays clean after `pod install`. In bare apps, commit the generated files. With Expo prebuild, commit `icon-switcher.d.ts`; native folders follow your usual prebuild policy.
156
+
112
157
  **Managed iOS icon sets:** when `sync` or the plugin rewrites an `.appiconset` it owns, it deletes files in that folder that the new `Contents.json` does not reference (`sync --check` reports them). Other icon sets are left alone.
113
158
 
159
+ **Generated images across machines:** image resampling rounds slightly differently on different CPUs (for example an Apple silicon Mac and x86 Linux CI). `sync` treats an existing PNG whose pixels differ from the regenerated one by at most 3 levels per channel as unchanged, so `sync --check` stays green and files are not rewritten.
160
+
114
161
  ## Usage
115
162
 
116
163
  ```ts
@@ -133,7 +180,7 @@ if (IconSwitcher.isSupported) {
133
180
 
134
181
  | Method | Returns | Notes |
135
182
  | --- | --- | --- |
136
- | `IconSwitcher.isSupported` | `boolean` | `false` when the TurboModule is missing (web, Expo Go, unlink) or the device brand is skipped. |
183
+ | `IconSwitcher.isSupported` | `boolean` | `false` when the TurboModule is missing (web, Expo Go, unlink), the device brand is skipped, or iOS reports `supportsAlternateIcons` as `false`. |
137
184
  | `IconSwitcher.list()` | `IconName[]` | Bundled icon names; `default` first. |
138
185
  | `IconSwitcher.getCurrent()` | `Promise<IconName>` | |
139
186
  | `IconSwitcher.set(name)` | `Promise<{ status: 'applied' \| 'pending' }>` | Android with `applyOn: 'background'` → `pending` until background or `applyPending()`. |
@@ -181,7 +228,7 @@ const unsubscribe = await syncIcon({
181
228
  const res = await fetch('https://api.example.com/icon');
182
229
  return (await res.json()).iconName as string;
183
230
  },
184
- storage: optionalAsyncKeyValue, // default: in-memory
231
+ storage: AsyncStorage, // optional; default: in-memory
185
232
  retryAfterMs: 24 * 60 * 60 * 1000, // skip re-apply after failure
186
233
  onError: (err) => console.warn(err),
187
234
  });
@@ -192,6 +239,10 @@ const unsubscribe = await syncIcon({
192
239
 
193
240
  On **iOS**, the helper runs when the app is `active` and subscribes to `AppState`. On **Android**, it runs once when called (`set` may still return `pending`).
194
241
 
242
+ Each run does nothing when `fetchChoice` returns `null`, `undefined`, `''`, or a name not in `list()`, or when the choice is already current. A `pending` result is not recorded as applied.
243
+
244
+ `storage` is any object with async `getItem(key): Promise<string | null>` and `setItem(key, value): Promise<void>` (`SyncIconStorage`), such as `@react-native-async-storage/async-storage`. The helper uses the keys `icon-switcher:lastApplied` and `icon-switcher:lastFailed`. With the default in-memory storage, the retry block lasts only as long as that `syncIcon` call, so a restart retries straight away.
245
+
195
246
  ## Testing with Jest
196
247
 
197
248
  Ship a CommonJS mock that never loads native code:
@@ -247,6 +298,7 @@ Summary: with default `applyOn: 'background'`, `set()` returns `pending` and the
247
298
  ## Requirements
248
299
 
249
300
  - **New Architecture (TurboModule) required** — React Native **≥ 0.82** (developed with **0.88**).
301
+ - **iOS 15.1+** (React Native's `min_ios_version_supported`) and **Android minSdk 24**. On Android the module adds `androidx.lifecycle:lifecycle-process` to detect when the app goes to the background.
250
302
  - **Expo SDK ≥ 52** when using the config plugin (optional peer; developed with **SDK 58**).
251
303
  - **Expo Go is not supported** — use a development build or bare native project.
252
304
 
@@ -48,6 +48,16 @@ function _isFailedBlocked(failed, choice, retryAfterMs) {
48
48
  return Date.now() - failed.at < retryAfterMs;
49
49
  }
50
50
 
51
+ // Shared by every syncIcon instance: callers restart syncIcon when their inputs change, and an
52
+ // older instance's run can still be waiting on the OS (iOS shows an alert per set call). Running
53
+ // one at a time lets the next run see the icon the previous one applied.
54
+ let _queue = Promise.resolve();
55
+ function _enqueue(task) {
56
+ const next = _queue.then(task, task);
57
+ _queue = next.catch(() => {});
58
+ return next;
59
+ }
60
+
51
61
  /**
52
62
  * Fetches the remote icon choice, validates it, and applies it via {@link IconSwitcher}.
53
63
  * iOS: runs while the app is `active`. Android: calls `set` immediately (native may defer).
@@ -64,13 +74,17 @@ export async function syncIcon(options) {
64
74
  }
65
75
  };
66
76
  let inFlight = false;
67
- const run = async () => {
68
- if (inFlight) {
69
- return;
70
- }
71
- inFlight = true;
77
+ let stopped = false;
78
+ const _apply = async () => {
72
79
  try {
80
+ // Unsubscribed while queued: the caller has moved on to a newer syncIcon.
81
+ if (stopped) {
82
+ return;
83
+ }
73
84
  const choice = await options.fetchChoice();
85
+ if (stopped) {
86
+ return;
87
+ }
74
88
  if (choice == null || choice === '') {
75
89
  return;
76
90
  }
@@ -103,6 +117,15 @@ export async function syncIcon(options) {
103
117
  }
104
118
  } catch (error) {
105
119
  reportError(error);
120
+ }
121
+ };
122
+ const run = async () => {
123
+ if (inFlight || stopped) {
124
+ return;
125
+ }
126
+ inFlight = true;
127
+ try {
128
+ await _enqueue(_apply);
106
129
  } finally {
107
130
  inFlight = false;
108
131
  }
@@ -119,9 +142,14 @@ export async function syncIcon(options) {
119
142
  }
120
143
  });
121
144
  runWhenActive();
122
- return () => subscription.remove();
145
+ return () => {
146
+ stopped = true;
147
+ subscription.remove();
148
+ };
123
149
  }
124
150
  await run();
125
- return () => {};
151
+ return () => {
152
+ stopped = true;
153
+ };
126
154
  }
127
155
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["AppState","Platform","IconSwitcher","STORAGE_KEY_APPLIED","STORAGE_KEY_FAILED","DEFAULT_RETRY_AFTER_MS","_createMemoryStorage","data","Map","getItem","key","get","setItem","value","set","_encodeFailed","name","JSON","stringify","at","Date","now","_decodeFailed","raw","length","parsed","parse","_isFailedBlocked","failed","choice","retryAfterMs","syncIcon","options","storage","reportError","error","onError","inFlight","run","fetchChoice","list","includes","isSupported","failedRaw","current","getCurrent","result","status","runWhenActive","currentState","OS","subscription","addEventListener","state","remove"],"sourceRoot":"../../../src","sources":["remote/index.ts"],"mappings":";;AAAA,SAASA,QAAQ,EAAEC,QAAQ,QAAQ,cAAc;AAEjD,OAAOC,YAAY,MAAM,aAAU;AAGnC,MAAMC,mBAAmB,GAAG,2BAA2B;AACvD,MAAMC,kBAAkB,GAAG,0BAA0B;AAErD,MAAMC,sBAAsB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI;AAelD,SAASC,oBAAoBA,CAAA,EAAoB;EAC/C,MAAMC,IAAI,GAAG,IAAIC,GAAG,CAAiB,CAAC;EACtC,OAAO;IACLC,OAAO,EAAE,MAAOC,GAAG,IAAKH,IAAI,CAACI,GAAG,CAACD,GAAG,CAAC,IAAI,IAAI;IAC7CE,OAAO,EAAE,MAAAA,CAAOF,GAAG,EAAEG,KAAK,KAAK;MAC7BN,IAAI,CAACO,GAAG,CAACJ,GAAG,EAAEG,KAAK,CAAC;IACtB;EACF,CAAC;AACH;AAEA,SAASE,aAAaA,CAACC,IAAY,EAAU;EAC3C,OAAOC,IAAI,CAACC,SAAS,CAAC;IAAEF,IAAI;IAAEG,EAAE,EAAEC,IAAI,CAACC,GAAG,CAAC;EAAE,CAAC,CAAC;AACjD;AAEA,SAASC,aAAaA,CACpBC,GAAkB,EACmB;EACrC,IAAI,CAACA,GAAG,IAAIA,GAAG,CAACC,MAAM,KAAK,CAAC,EAAE;IAC5B,OAAO,IAAI;EACb;EACA,IAAI;IACF,MAAMC,MAAM,GAAGR,IAAI,CAACS,KAAK,CAACH,GAAG,CAAmC;IAChE,IAAI,OAAOE,MAAM,CAACT,IAAI,KAAK,QAAQ,IAAI,OAAOS,MAAM,CAACN,EAAE,KAAK,QAAQ,EAAE;MACpE,OAAO;QAAEH,IAAI,EAAES,MAAM,CAACT,IAAI;QAAEG,EAAE,EAAEM,MAAM,CAACN;MAAG,CAAC;IAC7C;EACF,CAAC,CAAC,MAAM;IACN;IACA,OAAO;MAAEH,IAAI,EAAEO,GAAG;MAAEJ,EAAE,EAAE;IAAE,CAAC;EAC7B;EACA,OAAO,IAAI;AACb;AAEA,SAASQ,gBAAgBA,CACvBC,MAA2C,EAC3CC,MAAc,EACdC,YAAoB,EACX;EACT,IAAI,CAACF,MAAM,IAAIA,MAAM,CAACZ,IAAI,KAAKa,MAAM,EAAE;IACrC,OAAO,KAAK;EACd;EACA,OAAOT,IAAI,CAACC,GAAG,CAAC,CAAC,GAAGO,MAAM,CAACT,EAAE,GAAGW,YAAY;AAC9C;;AAEA;AACA;AACA;AACA;AACA,OAAO,eAAeC,QAAQA,CAACC,OAAwB,EAAuB;EAC5E,MAAMC,OAAO,GAAGD,OAAO,CAACC,OAAO,IAAI3B,oBAAoB,CAAC,CAAC;EACzD,MAAMwB,YAAY,GAAGE,OAAO,CAACF,YAAY,IAAIzB,sBAAsB;EACnE;EACA,MAAM6B,WAAW,GAAIC,KAAc,IAAW;IAC5C,IAAI;MACFH,OAAO,CAACI,OAAO,GAAGD,KAAK,CAAC;IAC1B,CAAC,CAAC,MAAM;MACN;IAAA;EAEJ,CAAC;EAED,IAAIE,QAAQ,GAAG,KAAK;EAEpB,MAAMC,GAAG,GAAG,MAAAA,CAAA,KAA2B;IACrC,IAAID,QAAQ,EAAE;MACZ;IACF;IACAA,QAAQ,GAAG,IAAI;IACf,IAAI;MACF,MAAMR,MAAM,GAAG,MAAMG,OAAO,CAACO,WAAW,CAAC,CAAC;MAC1C,IAAIV,MAAM,IAAI,IAAI,IAAIA,MAAM,KAAK,EAAE,EAAE;QACnC;MACF;MACA,IAAI,CAAC3B,YAAY,CAACsC,IAAI,CAAC,CAAC,CAACC,QAAQ,CAACZ,MAAkB,CAAC,EAAE;QACrD;MACF;MACA,IAAI,CAAC3B,YAAY,CAACwC,WAAW,EAAE;QAC7B;MACF;MAEA,MAAMC,SAAS,GAAG,MAAMV,OAAO,CAACxB,OAAO,CAACL,kBAAkB,CAAC;MAC3D,MAAMwB,MAAM,GAAGN,aAAa,CAACqB,SAAS,CAAC;MACvC,IAAIhB,gBAAgB,CAACC,MAAM,EAAEC,MAAM,EAAEC,YAAY,CAAC,EAAE;QAClD;MACF;MAEA,MAAMc,OAAO,GAAG,MAAM1C,YAAY,CAAC2C,UAAU,CAAC,CAAC;MAC/C,IAAIhB,MAAM,KAAKe,OAAO,EAAE;QACtB,MAAMX,OAAO,CAACrB,OAAO,CAACT,mBAAmB,EAAE0B,MAAM,CAAC;QAClD;MACF;MAEA,IAAI;QACF,MAAMiB,MAAM,GAAG,MAAM5C,YAAY,CAACY,GAAG,CAACe,MAAkB,CAAC;QACzD,IAAIiB,MAAM,CAACC,MAAM,KAAK,SAAS,EAAE;UAC/B;QACF;QACA,MAAMd,OAAO,CAACrB,OAAO,CAACT,mBAAmB,EAAE0B,MAAM,CAAC;QAClD,MAAMI,OAAO,CAACrB,OAAO,CAACR,kBAAkB,EAAE,EAAE,CAAC;MAC/C,CAAC,CAAC,OAAO+B,KAAK,EAAE;QACd,MAAMF,OAAO,CAACrB,OAAO,CAACR,kBAAkB,EAAEW,aAAa,CAACc,MAAM,CAAC,CAAC;QAChEK,WAAW,CAACC,KAAK,CAAC;MACpB;IACF,CAAC,CAAC,OAAOA,KAAK,EAAE;MACdD,WAAW,CAACC,KAAK,CAAC;IACpB,CAAC,SAAS;MACRE,QAAQ,GAAG,KAAK;IAClB;EACF,CAAC;EAED,MAAMW,aAAa,GAAGA,CAAA,KAAY;IAChC,IAAIhD,QAAQ,CAACiD,YAAY,KAAK,QAAQ,EAAE;MACtC,KAAKX,GAAG,CAAC,CAAC;IACZ;EACF,CAAC;EAED,IAAIrC,QAAQ,CAACiD,EAAE,KAAK,KAAK,EAAE;IACzB,MAAMC,YAAY,GAAGnD,QAAQ,CAACoD,gBAAgB,CAAC,QAAQ,EAAGC,KAAK,IAAK;MAClE,IAAIA,KAAK,KAAK,QAAQ,EAAE;QACtB,KAAKf,GAAG,CAAC,CAAC;MACZ;IACF,CAAC,CAAC;IACFU,aAAa,CAAC,CAAC;IACf,OAAO,MAAMG,YAAY,CAACG,MAAM,CAAC,CAAC;EACpC;EAEA,MAAMhB,GAAG,CAAC,CAAC;EACX,OAAO,MAAM,CAAC,CAAC;AACjB","ignoreList":[]}
1
+ {"version":3,"names":["AppState","Platform","IconSwitcher","STORAGE_KEY_APPLIED","STORAGE_KEY_FAILED","DEFAULT_RETRY_AFTER_MS","_createMemoryStorage","data","Map","getItem","key","get","setItem","value","set","_encodeFailed","name","JSON","stringify","at","Date","now","_decodeFailed","raw","length","parsed","parse","_isFailedBlocked","failed","choice","retryAfterMs","_queue","Promise","resolve","_enqueue","task","next","then","catch","syncIcon","options","storage","reportError","error","onError","inFlight","stopped","_apply","fetchChoice","list","includes","isSupported","failedRaw","current","getCurrent","result","status","run","runWhenActive","currentState","OS","subscription","addEventListener","state","remove"],"sourceRoot":"../../../src","sources":["remote/index.ts"],"mappings":"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","ignoreList":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/remote/index.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpD;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IACtD,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC;AA6CD;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,CA+E5E"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/remote/index.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpD;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IACtD,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC;AAwDD;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,CAkG5E"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-icon-switcher",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Switch the app icon at runtime on React Native and Expo from one config file, with the native wiring generated for you",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -64,6 +64,17 @@ function _isFailedBlocked(
64
64
  return Date.now() - failed.at < retryAfterMs;
65
65
  }
66
66
 
67
+ // Shared by every syncIcon instance: callers restart syncIcon when their inputs change, and an
68
+ // older instance's run can still be waiting on the OS (iOS shows an alert per set call). Running
69
+ // one at a time lets the next run see the icon the previous one applied.
70
+ let _queue: Promise<void> = Promise.resolve();
71
+
72
+ function _enqueue(task: () => Promise<void>): Promise<void> {
73
+ const next = _queue.then(task, task);
74
+ _queue = next.catch(() => {});
75
+ return next;
76
+ }
77
+
67
78
  /**
68
79
  * Fetches the remote icon choice, validates it, and applies it via {@link IconSwitcher}.
69
80
  * iOS: runs while the app is `active`. Android: calls `set` immediately (native may defer).
@@ -81,14 +92,18 @@ export async function syncIcon(options: SyncIconOptions): Promise<() => void> {
81
92
  };
82
93
 
83
94
  let inFlight = false;
95
+ let stopped = false;
84
96
 
85
- const run = async (): Promise<void> => {
86
- if (inFlight) {
87
- return;
88
- }
89
- inFlight = true;
97
+ const _apply = async (): Promise<void> => {
90
98
  try {
99
+ // Unsubscribed while queued: the caller has moved on to a newer syncIcon.
100
+ if (stopped) {
101
+ return;
102
+ }
91
103
  const choice = await options.fetchChoice();
104
+ if (stopped) {
105
+ return;
106
+ }
92
107
  if (choice == null || choice === '') {
93
108
  return;
94
109
  }
@@ -124,6 +139,16 @@ export async function syncIcon(options: SyncIconOptions): Promise<() => void> {
124
139
  }
125
140
  } catch (error) {
126
141
  reportError(error);
142
+ }
143
+ };
144
+
145
+ const run = async (): Promise<void> => {
146
+ if (inFlight || stopped) {
147
+ return;
148
+ }
149
+ inFlight = true;
150
+ try {
151
+ await _enqueue(_apply);
127
152
  } finally {
128
153
  inFlight = false;
129
154
  }
@@ -142,9 +167,14 @@ export async function syncIcon(options: SyncIconOptions): Promise<() => void> {
142
167
  }
143
168
  });
144
169
  runWhenActive();
145
- return () => subscription.remove();
170
+ return () => {
171
+ stopped = true;
172
+ subscription.remove();
173
+ };
146
174
  }
147
175
 
148
176
  await run();
149
- return () => {};
177
+ return () => {
178
+ stopped = true;
179
+ };
150
180
  }