react-native-store-version 1.4.2 → 2.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 (149) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +514 -43
  3. package/dist/checkVersion.d.ts +73 -0
  4. package/dist/checkVersion.d.ts.map +1 -0
  5. package/dist/checkVersion.js +193 -0
  6. package/dist/cli/check.js +38 -0
  7. package/dist/cli/checks/ids.js +111 -0
  8. package/dist/cli/checks/installed.js +51 -0
  9. package/dist/cli/checks/peers.js +31 -0
  10. package/dist/cli/checks/rollout.js +141 -0
  11. package/dist/cli/checks/store.js +71 -0
  12. package/dist/cli/checks/versions.js +83 -0
  13. package/dist/cli/index.js +43 -0
  14. package/dist/cli/main.js +182 -0
  15. package/dist/cli/output.js +77 -0
  16. package/dist/cli/packages.js +40 -0
  17. package/dist/cli/project/android.js +60 -0
  18. package/dist/cli/project/expo.js +194 -0
  19. package/dist/cli/project/files.js +42 -0
  20. package/dist/cli/project/gradle.js +145 -0
  21. package/dist/cli/project/index.js +10 -0
  22. package/dist/cli/project/ios.js +190 -0
  23. package/dist/cli/project/jsx.js +327 -0
  24. package/dist/cli/project/plist.js +166 -0
  25. package/dist/cli/project/sources.js +86 -0
  26. package/dist/cli/result.js +12 -0
  27. package/dist/compare.d.ts +31 -0
  28. package/dist/compare.d.ts.map +1 -0
  29. package/dist/compare.js +68 -0
  30. package/dist/detect.d.ts +68 -0
  31. package/dist/detect.d.ts.map +1 -0
  32. package/dist/detect.js +103 -0
  33. package/dist/errors.d.ts +33 -0
  34. package/dist/errors.d.ts.map +1 -0
  35. package/dist/errors.js +23 -0
  36. package/dist/fetchers/android.d.ts +53 -0
  37. package/dist/fetchers/android.d.ts.map +1 -0
  38. package/dist/fetchers/android.js +91 -0
  39. package/dist/fetchers/custom.d.ts +29 -0
  40. package/dist/fetchers/custom.d.ts.map +1 -0
  41. package/dist/fetchers/custom.js +41 -0
  42. package/dist/fetchers/ios.d.ts +61 -0
  43. package/dist/fetchers/ios.d.ts.map +1 -0
  44. package/dist/fetchers/ios.js +79 -0
  45. package/dist/index.d.ts +15 -14
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +9 -58
  48. package/dist/openStore.d.ts +29 -0
  49. package/dist/openStore.d.ts.map +1 -0
  50. package/dist/openStore.js +46 -0
  51. package/dist/policy.d.ts +75 -0
  52. package/dist/policy.d.ts.map +1 -0
  53. package/dist/policy.js +128 -0
  54. package/dist/react/DefaultForceModal.d.ts +26 -0
  55. package/dist/react/DefaultForceModal.d.ts.map +1 -0
  56. package/dist/react/DefaultForceModal.js +58 -0
  57. package/dist/react/DefaultWarningSheet.d.ts +26 -0
  58. package/dist/react/DefaultWarningSheet.d.ts.map +1 -0
  59. package/dist/react/DefaultWarningSheet.js +66 -0
  60. package/dist/react/StoreVersionModal.d.ts +52 -0
  61. package/dist/react/StoreVersionModal.d.ts.map +1 -0
  62. package/dist/react/StoreVersionModal.js +250 -0
  63. package/dist/react/StoreVersionProvider.d.ts +51 -0
  64. package/dist/react/StoreVersionProvider.d.ts.map +1 -0
  65. package/dist/react/StoreVersionProvider.js +69 -0
  66. package/dist/react/defaultUi.d.ts +72 -0
  67. package/dist/react/defaultUi.d.ts.map +1 -0
  68. package/dist/react/defaultUi.js +118 -0
  69. package/dist/react/engine.d.ts +146 -0
  70. package/dist/react/engine.d.ts.map +1 -0
  71. package/dist/react/engine.js +541 -0
  72. package/dist/react/texts.d.ts +43 -0
  73. package/dist/react/texts.d.ts.map +1 -0
  74. package/dist/react/texts.js +57 -0
  75. package/dist/react/useIsomorphicLayoutEffect.d.ts +3 -0
  76. package/dist/react/useIsomorphicLayoutEffect.d.ts.map +1 -0
  77. package/dist/react/useIsomorphicLayoutEffect.js +6 -0
  78. package/dist/react/useStoreVersion.d.ts +121 -0
  79. package/dist/react/useStoreVersion.d.ts.map +1 -0
  80. package/dist/react/useStoreVersion.js +23 -0
  81. package/dist/request.d.ts +78 -0
  82. package/dist/request.d.ts.map +1 -0
  83. package/dist/request.js +227 -0
  84. package/dist/resolve.d.ts +98 -0
  85. package/dist/resolve.d.ts.map +1 -0
  86. package/dist/resolve.js +218 -0
  87. package/dist/texts/ja.d.ts +8 -0
  88. package/dist/texts/ja.d.ts.map +1 -0
  89. package/dist/texts/ja.js +18 -0
  90. package/dist/vendor/compare-versions.d.ts +52 -0
  91. package/dist/vendor/compare-versions.d.ts.map +1 -0
  92. package/dist/vendor/compare-versions.js +111 -0
  93. package/package.json +83 -44
  94. package/src/checkVersion.ts +273 -0
  95. package/src/cli/check.ts +59 -0
  96. package/src/cli/checks/ids.ts +163 -0
  97. package/src/cli/checks/installed.ts +75 -0
  98. package/src/cli/checks/peers.ts +34 -0
  99. package/src/cli/checks/rollout.ts +183 -0
  100. package/src/cli/checks/store.ts +105 -0
  101. package/src/cli/checks/versions.ts +108 -0
  102. package/src/cli/index.ts +51 -0
  103. package/src/cli/main.ts +229 -0
  104. package/src/cli/output.ts +122 -0
  105. package/src/cli/packages.ts +43 -0
  106. package/src/cli/project/android.ts +96 -0
  107. package/src/cli/project/expo.ts +232 -0
  108. package/src/cli/project/files.ts +56 -0
  109. package/src/cli/project/gradle.ts +181 -0
  110. package/src/cli/project/index.ts +18 -0
  111. package/src/cli/project/ios.ts +284 -0
  112. package/src/cli/project/jsx.ts +354 -0
  113. package/src/cli/project/plist.ts +179 -0
  114. package/src/cli/project/sources.ts +118 -0
  115. package/src/cli/result.ts +34 -0
  116. package/src/compare.ts +101 -0
  117. package/src/detect.ts +178 -0
  118. package/src/errors.ts +63 -0
  119. package/src/fetchers/android.ts +136 -0
  120. package/src/fetchers/custom.ts +84 -0
  121. package/src/fetchers/ios.ts +160 -0
  122. package/src/index.ts +37 -0
  123. package/src/openStore.ts +74 -0
  124. package/src/policy.ts +223 -0
  125. package/src/react/DefaultForceModal.tsx +100 -0
  126. package/src/react/DefaultWarningSheet.tsx +115 -0
  127. package/src/react/StoreVersionModal.tsx +365 -0
  128. package/src/react/StoreVersionProvider.tsx +149 -0
  129. package/src/react/defaultUi.tsx +205 -0
  130. package/src/react/engine.ts +768 -0
  131. package/src/react/texts.ts +119 -0
  132. package/src/react/useIsomorphicLayoutEffect.ts +8 -0
  133. package/src/react/useStoreVersion.ts +153 -0
  134. package/src/request.ts +328 -0
  135. package/src/resolve.ts +388 -0
  136. package/src/texts/ja.ts +26 -0
  137. package/src/vendor/compare-versions.ts +128 -0
  138. package/texts/ja.d.ts +2 -0
  139. package/texts/ja.js +3 -0
  140. package/.eslintrc.js +0 -51
  141. package/.github/FUNDING.yml +0 -3
  142. package/.github/workflows/pr.yml +0 -48
  143. package/.github/workflows/release.yml +0 -259
  144. package/.prettierrc.js +0 -6
  145. package/babel.config.js +0 -3
  146. package/dist/android.d.ts +0 -1
  147. package/dist/android.js +0 -17
  148. package/dist/ios.d.ts +0 -1
  149. package/dist/ios.js +0 -18
@@ -0,0 +1,541 @@
1
+ // Provider の判定の状態遷移と副作用(取得・AppState・onResult / onError)。React を import しない(React の state へは
2
+ // setModel で写すだけ。配線は StoreVersionProvider.tsx)。判定そのものは resolve に任せ、ここでは
3
+ // 「いつ取り直すか」「どの結果をどの状態にするか」「どのエラーを onError に渡すか」だけを決める
4
+ import { AppState } from 'react-native';
5
+ import { detect } from '../detect.js';
6
+ import { StoreVersionError } from '../errors.js';
7
+ import { openStore as openStorePage } from '../openStore.js';
8
+ import { evaluatePolicy, validatePolicy, } from '../policy.js';
9
+ import { isAbortError } from '../request.js';
10
+ import { attachedErrorsOf, resolve, } from '../resolve.js';
11
+ /** 判定全体の既定のタイムアウト(Android の HTML が約 1.25MB あるため) */
12
+ export const DEFAULT_TIMEOUT_MS = 10000;
13
+ const LOG_PREFIX = '[react-native-store-version]';
14
+ const isDev = () => typeof __DEV__ !== 'undefined' && __DEV__ === true;
15
+ /** locale(BCP 47。`ja-JP` / `zh-Hant-TW` / `en_US`)の地域を小文字の 2 文字で返す。無ければ null */
16
+ const regionOf = (locale) => {
17
+ if (typeof locale !== 'string') {
18
+ return null;
19
+ }
20
+ const subtags = locale.split(/[-_]/);
21
+ // 先頭は言語。2 文字の英字の subtag は地域だけ(extlang は 3 文字、script は 4 文字、variant は 4 文字以上)。
22
+ // 1 文字の subtag(`-u-` などの拡張)から後は見ない。数字 3 桁の地域(`419` など)は国ではないので使わない
23
+ for (let i = 1; i < subtags.length; i += 1) {
24
+ const subtag = subtags[i];
25
+ if (subtag.length === 1) {
26
+ break;
27
+ }
28
+ if (/^[A-Za-z]{2}$/.test(subtag)) {
29
+ return subtag.toLowerCase();
30
+ }
31
+ }
32
+ return null;
33
+ };
34
+ /**
35
+ * `country` の既定。端末ロケールの地域を JS だけで取る(`Intl.DateTimeFormat().resolvedOptions().locale`)。
36
+ * 取れなければ `us`。例外は投げない(パッケージの入口からは export しない)
37
+ */
38
+ export const defaultCountry = () => {
39
+ try {
40
+ return regionOf(Intl.DateTimeFormat().resolvedOptions().locale) ?? 'us';
41
+ }
42
+ catch {
43
+ return 'us';
44
+ }
45
+ };
46
+ export const IDLE = {
47
+ status: 'idle',
48
+ local: null,
49
+ error: null,
50
+ snapshot: null,
51
+ evaluation: null,
52
+ };
53
+ /** 直近の判定の最新 / 最低バージョンに policy を当て直す。値で指定した policy は差分によらず検証する(resolve と同じ) */
54
+ const reevaluate = (local, snapshot, policy) => {
55
+ try {
56
+ validatePolicy(policy);
57
+ const { result, detail, action, diff } = evaluatePolicy({
58
+ local,
59
+ remote: snapshot.latest ?? '',
60
+ minimum: snapshot.minimum,
61
+ policy,
62
+ });
63
+ return { evaluation: { result, detail, action, diff } };
64
+ }
65
+ catch (error) {
66
+ // 取得の結果は resolve で一度判定できているので、ここで投げうるのは policy の POLICY_FAILED だけ
67
+ return { error: error };
68
+ }
69
+ };
70
+ /**
71
+ * Provider の engine を作る。判断はすべてここの変数(最新の値)で行い、React の state は `setModel` で写すだけにする。
72
+ * props はいつも `getProps()` で最新のものを読む(変わっても取り直さない。例外は `enabled` の切り替え)
73
+ */
74
+ export const createEngine = (getProps, setModel, setDismissedRemote) => {
75
+ let model = IDLE;
76
+ // マウント中か。false の間は setState も onResult / onError も呼ばない
77
+ let mounted = false;
78
+ // 直近に始めた判定
79
+ let run = null;
80
+ // いま見せている結果(ready と、再チェック中に見せ続けている前回の ready)を出した判定。
81
+ // 再チェックの間も、この判定の iOS の URL の lookup は止めずに残し、終わったら見せている結果の storeUrl に反映する
82
+ let shownRun = null;
83
+ let target = null;
84
+ let storeUrlResult = null;
85
+ // snapshot の evaluation を計算した policy(policy の変更を見分ける)
86
+ let evaluatedPolicy;
87
+ // 判定の途中でバックグラウンドになって取り消した。active に戻ったら取り直す
88
+ let abortedByBackground = false;
89
+ // バックグラウンドの間に取得を始めようとした(マウント・recheck・enabled の切り替え)。active になったら始める
90
+ let pendingStart = false;
91
+ // 直近の active の後に background になったか(background → active の復帰だけを見分ける。inactive → active は数えない)
92
+ let wentBackground = false;
93
+ // 直近に見た enabled(false → true / true → false の切り替えを見分ける)
94
+ let lastEnabled = true;
95
+ let subscription = null;
96
+ const publish = (next) => {
97
+ model = next;
98
+ if (mounted) {
99
+ setModel(next);
100
+ }
101
+ };
102
+ const warnThrown = (name, error) => {
103
+ if (isDev()) {
104
+ console.warn(`${LOG_PREFIX} ${name} threw`, error);
105
+ }
106
+ };
107
+ // 開発者のコールバックが throw しても、Promise を返して reject しても、unhandled rejection にしない(アプリを止めない)。
108
+ // 返した Promise は待たない
109
+ const call = (name, fn, arg) => {
110
+ try {
111
+ const returned = fn(arg);
112
+ if (typeof returned === 'object' &&
113
+ returned !== null &&
114
+ typeof returned.then === 'function') {
115
+ returned.then(undefined, (error) => warnThrown(name, error));
116
+ }
117
+ }
118
+ catch (error) {
119
+ warnThrown(name, error);
120
+ }
121
+ };
122
+ const report = (error) => {
123
+ if (!mounted) {
124
+ return;
125
+ }
126
+ const { onError } = getProps();
127
+ if (onError) {
128
+ call('onError', onError, error);
129
+ }
130
+ else if (isDev()) {
131
+ console.warn(`${LOG_PREFIX} ${error.code}: ${error.message}`, error);
132
+ }
133
+ };
134
+ const notifyResult = () => {
135
+ const { onResult } = getProps();
136
+ const { evaluation, snapshot, local } = model;
137
+ if (!mounted || !onResult || !evaluation || !snapshot || local === null) {
138
+ return;
139
+ }
140
+ call('onResult', onResult, {
141
+ result: evaluation.result,
142
+ detail: evaluation.detail,
143
+ action: evaluation.action,
144
+ diff: evaluation.diff,
145
+ local,
146
+ remote: snapshot.latest,
147
+ });
148
+ };
149
+ const isCurrent = (r) => mounted && run === r && !r.controller.signal.aborted;
150
+ /** 判定をしない / できない状態にする(前回の結果も消す) */
151
+ const finishWithoutCheck = (next) => {
152
+ target = null;
153
+ storeUrlResult = null;
154
+ publish(next);
155
+ };
156
+ /** 見せている判定の結果(ready と、それを見せ続けている再チェック中)。無ければ null */
157
+ const shownReady = () => (model.status === 'ready' || model.status === 'checking') &&
158
+ model.evaluation !== null &&
159
+ model.snapshot !== null
160
+ ? model
161
+ : null;
162
+ /** 前回の判定の、まだ終わっていない iOS の URL の lookup を止める(取り直さない。onError にも渡さない) */
163
+ const stopLookup = (r) => {
164
+ if (r?.settled && r.lookupPending) {
165
+ r.controller.abort();
166
+ }
167
+ };
168
+ /**
169
+ * 再チェックが失敗したら、見せ続けていた前回の ready に戻す(force を再チェックの失敗で消さない)。
170
+ * エラーは onError に渡すだけで hook の `error` には入れない(status が ready のまま error だけが入る状態を作らない)。
171
+ * 再チェック中に policy が変わっていたら、前回の取得結果に当て直す
172
+ */
173
+ const restore = () => {
174
+ publish({ ...model, status: 'ready', error: null });
175
+ engine.applyPolicy(getProps().policy);
176
+ };
177
+ /** 判定の結果を受け取ったとき */
178
+ const onResolved = (current, local, policy, resolved) => {
179
+ if (!isCurrent(current)) {
180
+ return;
181
+ }
182
+ current.settled = true;
183
+ const snapshot = {
184
+ latest: resolved.latest,
185
+ minimum: resolved.minimum,
186
+ storeUrl: resolved.storeUrl,
187
+ releaseNotes: resolved.releaseNotes,
188
+ };
189
+ // 判定中に policy が変わっていたら、最新の policy で当て直してから出す(onResult を 2 回呼ばないように)
190
+ const latestPolicy = getProps().policy;
191
+ const outcome = latestPolicy === policy
192
+ ? { evaluation: resolved }
193
+ : reevaluate(local, snapshot, latestPolicy);
194
+ evaluatedPolicy = latestPolicy;
195
+ storeUrlResult = resolved.storeUrlResult;
196
+ // 新しい判定の結果を出すので、前回の判定の URL の lookup はもう要らない
197
+ if (shownRun !== current) {
198
+ stopLookup(shownRun);
199
+ }
200
+ shownRun = current;
201
+ if ('error' in outcome) {
202
+ publish({
203
+ status: 'error',
204
+ local,
205
+ error: outcome.error,
206
+ snapshot,
207
+ evaluation: null,
208
+ });
209
+ }
210
+ else {
211
+ const { result, detail, action, diff } = outcome.evaluation;
212
+ publish({
213
+ status: 'ready',
214
+ local,
215
+ error: null,
216
+ snapshot,
217
+ evaluation: { result, detail, action, diff },
218
+ });
219
+ }
220
+ // 判定を止めなかったエラー(MINIMUM_VERSION_FAILED と、最新が比較できず最低バージョンで force にしたときの INVALID_VERSION)
221
+ for (const error of resolved.errors) {
222
+ report(error);
223
+ }
224
+ if ('error' in outcome) {
225
+ report(outcome.error);
226
+ }
227
+ else {
228
+ notifyResult();
229
+ }
230
+ watchStoreUrl(current, resolved.storeUrlResult);
231
+ };
232
+ /**
233
+ * iOS で getLatestVersion を使うときは、URL の lookup が終わった時点で storeUrl を更新する。
234
+ * 起動時の URL の lookup の失敗(trackViewUrl が無いことも)は STORE_URL_UNAVAILABLE で onError へ(storeUrlResult は reject しない)。
235
+ * 再チェックの間(この判定の結果を見せ続けている間)に終わったときも、見せている結果に反映する
236
+ */
237
+ const watchStoreUrl = (current, result) => {
238
+ current.lookupPending = true;
239
+ void result.then((found) => {
240
+ current.lookupPending = false;
241
+ if (!mounted ||
242
+ shownRun !== current ||
243
+ current.controller.signal.aborted) {
244
+ return;
245
+ }
246
+ if (found.error) {
247
+ report(found.error);
248
+ return;
249
+ }
250
+ if (model.snapshot && model.snapshot.storeUrl !== found.storeUrl) {
251
+ publish({
252
+ ...model,
253
+ snapshot: { ...model.snapshot, storeUrl: found.storeUrl },
254
+ });
255
+ }
256
+ });
257
+ };
258
+ /** 判定が失敗したとき(取り消しは何もしない) */
259
+ const onRejected = (current, local, error) => {
260
+ // 取り消した判定(アンマウント・バックグラウンド・取り直し)は何も出さない
261
+ if (!isCurrent(current) || isAbortError(error)) {
262
+ return;
263
+ }
264
+ current.settled = true;
265
+ if (shownReady()) {
266
+ restore();
267
+ }
268
+ else {
269
+ publish({
270
+ ...IDLE,
271
+ status: 'error',
272
+ local,
273
+ error: error,
274
+ });
275
+ }
276
+ report(error);
277
+ // 判定を止めたエラーに添えられた、ほかのエラー(その時点で分かっている最低バージョンのエラー)
278
+ for (const attached of attachedErrorsOf(error)) {
279
+ report(attached);
280
+ }
281
+ };
282
+ /** 取得を始める(detect の後。バックグラウンドの間は active まで待つ) */
283
+ const fetchFor = (current, detected) => {
284
+ // RN のタイマーはバックグラウンドで止まるので、バックグラウンドの間は取得を始めず、active になったら始める
285
+ if (AppState.currentState === 'background') {
286
+ pendingStart = true;
287
+ return;
288
+ }
289
+ const props = getProps();
290
+ const { platform, version: local, appId } = detected;
291
+ const country = props.country ?? defaultCountry();
292
+ target = { platform, appId, country };
293
+ current.settled = false;
294
+ // 再チェックの間は前回の ready の値を見せ続ける(初回は前回の結果が無い)。
295
+ // 前回の URL の取得結果も、新しい判定が終わるまで openStore に使う
296
+ const previous = shownReady();
297
+ if (previous) {
298
+ publish({ ...previous, status: 'checking', error: null });
299
+ }
300
+ else {
301
+ storeUrlResult = null;
302
+ publish({ ...IDLE, status: 'checking', local });
303
+ }
304
+ const policy = props.policy;
305
+ // reject は onRejected で受ける(unhandled rejection にしない)
306
+ void resolve({
307
+ platform,
308
+ version: local,
309
+ appId,
310
+ country,
311
+ policy,
312
+ getLatestVersion: props.getLatestVersion,
313
+ getMinimumVersion: props.getMinimumVersion,
314
+ timeoutMs: props.timeoutMs ?? DEFAULT_TIMEOUT_MS,
315
+ signal: current.controller.signal,
316
+ }).then((resolved) => onResolved(current, local, policy, resolved), (error) => onRejected(current, local, error));
317
+ };
318
+ const start = () => {
319
+ if (!mounted) {
320
+ return;
321
+ }
322
+ pendingStart = false;
323
+ abortedByBackground = false;
324
+ // 判定中なら取り消す(onError には渡さない)。同じストアへの取得は request.ts が次の判定に引き継ぐ。
325
+ // 判定が終わって URL の lookup だけが残っている判定は止めない(前回の結果の storeUrl に反映するため)
326
+ if (run && !run.settled) {
327
+ run.controller.abort();
328
+ }
329
+ const current = {
330
+ controller: new AbortController(),
331
+ settled: true,
332
+ lookupPending: false,
333
+ };
334
+ run = current;
335
+ // 取得しない(skipped)・できない(LOCAL_VERSION_UNAVAILABLE)は、バックグラウンドでもすぐ決める
336
+ const props = getProps();
337
+ if (props.enabled === false) {
338
+ finishWithoutCheck({ ...IDLE, status: 'skipped' });
339
+ return;
340
+ }
341
+ let detected;
342
+ try {
343
+ detected = detect({
344
+ version: props.version,
345
+ bundleId: props.bundleId,
346
+ packageName: props.packageName,
347
+ });
348
+ }
349
+ catch (error) {
350
+ // LOCAL_VERSION_UNAVAILABLE
351
+ if (shownReady()) {
352
+ restore();
353
+ }
354
+ else {
355
+ finishWithoutCheck({
356
+ ...IDLE,
357
+ status: 'error',
358
+ error: error,
359
+ });
360
+ }
361
+ // 報告は microtask に回し、そのときまだこの判定が最新のときだけ渡す(StrictMode のマウント・アンマウント・マウントで
362
+ // 1 回目の判定の報告を捨て、2 回報告しないように)
363
+ void Promise.resolve().then(() => {
364
+ if (isCurrent(current)) {
365
+ report(error);
366
+ }
367
+ });
368
+ return;
369
+ }
370
+ if (detected.skipped !== null) {
371
+ finishWithoutCheck({ ...IDLE, status: 'skipped' });
372
+ return;
373
+ }
374
+ fetchFor(current, detected);
375
+ };
376
+ const onAppStateChange = (next) => {
377
+ if (next === 'background') {
378
+ wentBackground = true;
379
+ // RN のタイマーはバックグラウンドで止まり、復帰時に期限切れで発火する(TIMEOUT になり、この起動で force を出す機会を失う)ので、
380
+ // 終わっていない判定は取り消して、復帰時に取り直す(recheckOnForeground に関係なく)
381
+ if (run && !run.settled && !run.controller.signal.aborted) {
382
+ run.controller.abort();
383
+ abortedByBackground = true;
384
+ }
385
+ // 判定は終わったが iOS の URL の lookup がまだなら、それだけを止める(取り直さない。storeUrlResult はエラーで終わり、
386
+ // openStore はその場で lookup し直す)
387
+ stopLookup(run);
388
+ stopLookup(shownRun);
389
+ return;
390
+ }
391
+ if (next === 'active') {
392
+ const returned = wentBackground;
393
+ wentBackground = false;
394
+ // 待っていた取得の開始・取り消した判定の取り直し・recheckOnForeground が重なっても、始めるのは 1 回
395
+ if (pendingStart ||
396
+ (returned &&
397
+ (abortedByBackground || getProps().recheckOnForeground === true))) {
398
+ start();
399
+ }
400
+ }
401
+ };
402
+ const engine = {
403
+ mount() {
404
+ mounted = true;
405
+ wentBackground = false;
406
+ pendingStart = false;
407
+ lastEnabled = getProps().enabled !== false;
408
+ subscription = AppState.addEventListener('change', onAppStateChange);
409
+ // バックグラウンドのままマウントされたら、取得は active になるまで始めない(status は idle のまま)
410
+ start();
411
+ },
412
+ unmount() {
413
+ // 取り消した後は onResult / onError / setState を呼ばない(resolve の abort は何も添えずに AbortError で終わる)
414
+ mounted = false;
415
+ pendingStart = false;
416
+ subscription?.remove();
417
+ subscription = null;
418
+ run?.controller.abort();
419
+ shownRun?.controller.abort();
420
+ run = null;
421
+ shownRun = null;
422
+ },
423
+ start,
424
+ setEnabled(enabled) {
425
+ if (!mounted || enabled === lastEnabled) {
426
+ return;
427
+ }
428
+ lastEnabled = enabled;
429
+ if (enabled) {
430
+ // false → true: 判定を始める
431
+ start();
432
+ return;
433
+ }
434
+ // true → false: 判定を取り消して skipped にする(onError は呼ばない)
435
+ run?.controller.abort();
436
+ shownRun?.controller.abort();
437
+ run = null;
438
+ shownRun = null;
439
+ pendingStart = false;
440
+ abortedByBackground = false;
441
+ finishWithoutCheck({ ...IDLE, status: 'skipped' });
442
+ },
443
+ applyPolicy(policy) {
444
+ const { snapshot, local } = model;
445
+ // 再チェック中は当て直さない(終わったときに最新の policy で当て直す。失敗したら前回の結果に当て直す)
446
+ if (!mounted ||
447
+ model.status === 'checking' ||
448
+ !snapshot ||
449
+ local === null ||
450
+ policy === evaluatedPolicy) {
451
+ return;
452
+ }
453
+ evaluatedPolicy = policy;
454
+ const outcome = reevaluate(local, snapshot, policy);
455
+ if ('error' in outcome) {
456
+ // 不正な policy が続く(インラインで書いた関数が描画のたびに throw するなど)間は、onError を繰り返さない
457
+ if (model.status === 'ready') {
458
+ publish({
459
+ ...model,
460
+ status: 'error',
461
+ error: outcome.error,
462
+ evaluation: null,
463
+ });
464
+ report(outcome.error);
465
+ }
466
+ return;
467
+ }
468
+ // action が変わらなければ何もしない(インラインで書いた policy で、描画のたびに onResult を呼ばないように)
469
+ if (model.status === 'ready' &&
470
+ model.evaluation?.action === outcome.evaluation.action) {
471
+ return;
472
+ }
473
+ publish({
474
+ ...model,
475
+ status: 'ready',
476
+ error: null,
477
+ evaluation: outcome.evaluation,
478
+ });
479
+ notifyResult();
480
+ },
481
+ async openStore() {
482
+ const opening = target;
483
+ if (opening === null) {
484
+ // 判定しない環境(skipped)や、アプリのバージョン / ID が取れなかったときは、開く先が分からない
485
+ report(new StoreVersionError('STORE_URL_UNAVAILABLE', `cannot open the store: the app was not detected (status: ${model.status})`));
486
+ return false;
487
+ }
488
+ try {
489
+ await openStorePage({
490
+ ...opening,
491
+ storeUrl: storeUrlResult,
492
+ timeoutMs: getProps().timeoutMs ?? DEFAULT_TIMEOUT_MS,
493
+ });
494
+ return true;
495
+ }
496
+ catch (error) {
497
+ // STORE_URL_UNAVAILABLE(iOS の lookup の失敗 / Linking.openURL の失敗)。onError に渡すだけで reject しない
498
+ // (`onPress={openStore}` のまま使っても unhandled rejection にならないように)。status / error は変えない
499
+ report(error);
500
+ return false;
501
+ }
502
+ },
503
+ dismiss() {
504
+ // 再チェック中も、前回の ready の warning なら閉じられる
505
+ const shown = shownReady();
506
+ if (mounted &&
507
+ shown?.evaluation?.action === 'warning' &&
508
+ shown.snapshot?.latest != null) {
509
+ setDismissedRemote(shown.snapshot.latest);
510
+ }
511
+ },
512
+ };
513
+ return engine;
514
+ };
515
+ /**
516
+ * engine の状態から hook の戻り値を作る。判定の結果(result / action / remote / storeUrl など)は ready のとき
517
+ * (再チェック中は前回の ready の値)だけ見せる
518
+ */
519
+ export const stateOf = (model, dismissedRemote, engine) => {
520
+ const evaluation = model.evaluation;
521
+ const snapshot = evaluation ? model.snapshot : null;
522
+ const action = evaluation?.action ?? null;
523
+ const remote = snapshot?.latest ?? null;
524
+ return {
525
+ status: model.status,
526
+ result: evaluation?.result ?? null,
527
+ detail: evaluation?.detail ?? null,
528
+ action,
529
+ diff: evaluation?.diff ?? null,
530
+ // 「あとで」は remote のバージョン単位。remote が変われば解除、force なら常に出す
531
+ dismissed: action === 'warning' && remote !== null && remote === dismissedRemote,
532
+ local: model.local,
533
+ remote,
534
+ storeUrl: snapshot?.storeUrl ?? null,
535
+ releaseNotes: snapshot?.releaseNotes,
536
+ openStore: engine.openStore,
537
+ dismiss: engine.dismiss,
538
+ recheck: engine.start,
539
+ error: model.error,
540
+ };
541
+ };
@@ -0,0 +1,43 @@
1
+ /** Force(強制アップデート)の文言。`{remote}`(最新バージョン)/ `{local}`(現在のバージョン)は表示時に置き換える */
2
+ export type StoreVersionForceTexts = {
3
+ title: string;
4
+ message: string;
5
+ /** 更新ボタン */
6
+ update: string;
7
+ };
8
+ /** Warning(閉じられるボトムシート)の文言。`{remote}` / `{local}` は表示時に置き換える */
9
+ export type StoreVersionWarningTexts = {
10
+ title: string;
11
+ message: string;
12
+ /** 更新ボタン */
13
+ update: string;
14
+ /** 「あとで」ボタン */
15
+ later: string;
16
+ };
17
+ /**
18
+ * Provider の `texts`。`force` / `warning` ごとに、指定しなかったキーは英語の既定値で補完する(1 段だけマージ)。
19
+ * 日本語は `react-native-store-version/texts/ja` のプリセットを使う。変わったら表示中のモーダルにも反映する
20
+ */
21
+ export type StoreVersionTexts = {
22
+ force?: Partial<StoreVersionForceTexts>;
23
+ warning?: Partial<StoreVersionWarningTexts>;
24
+ };
25
+ /** すべてのキーが埋まった文言(英語の既定値、`texts/ja` のプリセット、{@link resolveTexts} の結果の形) */
26
+ export type StoreVersionResolvedTexts = {
27
+ force: StoreVersionForceTexts;
28
+ warning: StoreVersionWarningTexts;
29
+ };
30
+ /** 英語の既定値(元のデザインと同じ文言) */
31
+ export declare const DEFAULT_TEXTS: StoreVersionResolvedTexts;
32
+ /** 置き換えに使う値。最新バージョンが比較できず、最低バージョンで force になったときは `remote` が null */
33
+ export type TextVariables = {
34
+ local: string | null;
35
+ remote: string | null;
36
+ };
37
+ /**
38
+ * Provider の `texts` を表示用の文言にする。`force` / `warning` ごとに、指定しなかったキー(文字列でない値も)を英語の既定値で
39
+ * 補完し(1 段だけマージ)、`{remote}` / `{local}` のすべての出現を置き換える。
40
+ * `texts` / `local` / `remote` が変わるたびに呼ぶ(`texts` の変更を表示中のモーダルにも即座に反映するため)
41
+ */
42
+ export declare const resolveTexts: (texts: StoreVersionTexts | undefined, variables: TextVariables) => StoreVersionResolvedTexts;
43
+ //# sourceMappingURL=texts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"texts.d.ts","sourceRoot":"","sources":["../../src/react/texts.ts"],"names":[],"mappings":"AAIA,4EAA4E;AAC5E,MAAM,MAAM,sBAAsB,GAAG;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY;IACZ,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,gEAAgE;AAChE,MAAM,MAAM,wBAAwB,GAAG;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,eAAe;IACf,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAC;IACxC,OAAO,CAAC,EAAE,OAAO,CAAC,wBAAwB,CAAC,CAAC;CAC7C,CAAC;AAEF,yEAAyE;AACzE,MAAM,MAAM,yBAAyB,GAAG;IACtC,KAAK,EAAE,sBAAsB,CAAC;IAC9B,OAAO,EAAE,wBAAwB,CAAC;CACnC,CAAC;AAEF,0BAA0B;AAC1B,eAAO,MAAM,aAAa,EAAE,yBAa3B,CAAC;AAEF,oEAAoE;AACpE,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,CAAC;AAmCF;;;;GAIG;AACH,eAAO,MAAM,YAAY,UAChB,iBAAiB,GAAG,SAAS,aACzB,aAAa,KACvB,yBAkBF,CAAC"}
@@ -0,0 +1,57 @@
1
+ // デフォルト UI の文言。型、英語の既定値、既定値との 1 段のマージと `{remote}` / `{local}` の置き換え。
2
+ // React に依存しない純粋な関数にして、デフォルト UI と `components` の両方がこの結果を受け取る。
3
+ // 日本語のプリセットは src/texts/ja.ts(`react-native-store-version/texts/ja`)
4
+ /** 英語の既定値(元のデザインと同じ文言) */
5
+ export const DEFAULT_TEXTS = {
6
+ force: {
7
+ title: 'Update required',
8
+ message: 'A new version ({remote}) is available. Please update to continue using the app.',
9
+ update: 'Update',
10
+ },
11
+ warning: {
12
+ title: 'Update available',
13
+ message: 'Version {remote} is now available.',
14
+ update: 'Update',
15
+ later: 'Later',
16
+ },
17
+ };
18
+ // プレースホルダーと、それを直接囲む括弧(半角 / 全角)・その前の空白。1 回の replace で全部を扱う
19
+ // (置き換えた値をもう一度走査しないので、remote に "{local}" が入っていても二重に置換しない。
20
+ // String.prototype.replaceAll に頼らないので古い JSC でも動く)
21
+ const PLACEHOLDER = /(\s*)([((]?)\{(local|remote)\}([))]?)/g;
22
+ const isPair = (open, close) => (open === '(' && close === ')') || (open === '(' && close === ')');
23
+ // `{remote}` / `{local}` のすべての出現を置き換える。値が null(Play の版が比較できず、最低バージョンで force になったとき)は
24
+ // 空文字にし、プレースホルダーを直接囲んでいた空の括弧「()」「()」(とその前の空白)を取り除く。元の文言にある無関係な括弧は消さない。
25
+ // 余った空白(連続する空白・前後の空白)も取り除く。既定の英語は「A new version is available. …」、
26
+ // 日本語は「新しいバージョンが公開されています。…」になる。括弧を使っていない独自の文言では、ただ空文字に置き換わるだけ。
27
+ // 値がそろっているときは文言に手を加えない
28
+ const fill = (text, variables) => {
29
+ let emptied = false;
30
+ const out = text.replace(PLACEHOLDER, (_match, space, open, key, close) => {
31
+ const value = variables[key];
32
+ if (value !== null) {
33
+ return `${space}${open}${value}${close}`;
34
+ }
35
+ emptied = true;
36
+ return isPair(open, close) ? '' : `${space}${open}${close}`;
37
+ });
38
+ return emptied ? out.replace(/ {2,}/g, ' ').trim() : out;
39
+ };
40
+ // 指定された値が文字列のときだけ使う(JS からの利用で数値や null を渡されても既定の文言に戻す)
41
+ const textOrDefault = (value, fallback) => typeof value === 'string' ? value : fallback;
42
+ /**
43
+ * Provider の `texts` を表示用の文言にする。`force` / `warning` ごとに、指定しなかったキー(文字列でない値も)を英語の既定値で
44
+ * 補完し(1 段だけマージ)、`{remote}` / `{local}` のすべての出現を置き換える。
45
+ * `texts` / `local` / `remote` が変わるたびに呼ぶ(`texts` の変更を表示中のモーダルにも即座に反映するため)
46
+ */
47
+ export const resolveTexts = (texts, variables) => {
48
+ const force = { ...DEFAULT_TEXTS.force };
49
+ const warning = { ...DEFAULT_TEXTS.warning };
50
+ for (const key of Object.keys(force)) {
51
+ force[key] = fill(textOrDefault(texts?.force?.[key], force[key]), variables);
52
+ }
53
+ for (const key of Object.keys(warning)) {
54
+ warning[key] = fill(textOrDefault(texts?.warning?.[key], warning[key]), variables);
55
+ }
56
+ return { force, warning };
57
+ };
@@ -0,0 +1,3 @@
1
+ import { useLayoutEffect } from 'react';
2
+ export declare const useIsomorphicLayoutEffect: typeof useLayoutEffect;
3
+ //# sourceMappingURL=useIsomorphicLayoutEffect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useIsomorphicLayoutEffect.d.ts","sourceRoot":"","sources":["../../src/react/useIsomorphicLayoutEffect.ts"],"names":[],"mappings":"AAEA,OAAO,EAAa,eAAe,EAAE,MAAM,OAAO,CAAC;AAEnD,eAAO,MAAM,yBAAyB,wBAGjB,CAAC"}
@@ -0,0 +1,6 @@
1
+ // サーバーでの描画(react-native-web の SSR など)では useLayoutEffect が React 18 で警告を出すので、window が無ければ useEffect にする。
2
+ // Provider(props の ref)とデフォルト UI の Modal(閉じる途中に描く最後の props)が、commit の後・配下の passive effect より先に ref を書くために使う
3
+ import { useEffect, useLayoutEffect } from 'react';
4
+ export const useIsomorphicLayoutEffect = globalThis.window === undefined
5
+ ? useEffect
6
+ : useLayoutEffect;