react-native-store-version 1.4.2 → 2.0.0-beta.1
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/CHANGELOG.md +28 -0
- package/README.md +514 -43
- package/dist/checkVersion.d.ts +73 -0
- package/dist/checkVersion.d.ts.map +1 -0
- package/dist/checkVersion.js +193 -0
- package/dist/cli/check.js +38 -0
- package/dist/cli/checks/ids.js +111 -0
- package/dist/cli/checks/installed.js +51 -0
- package/dist/cli/checks/peers.js +31 -0
- package/dist/cli/checks/rollout.js +141 -0
- package/dist/cli/checks/store.js +71 -0
- package/dist/cli/checks/versions.js +83 -0
- package/dist/cli/index.js +43 -0
- package/dist/cli/main.js +182 -0
- package/dist/cli/output.js +77 -0
- package/dist/cli/packages.js +40 -0
- package/dist/cli/project/android.js +60 -0
- package/dist/cli/project/expo.js +194 -0
- package/dist/cli/project/files.js +42 -0
- package/dist/cli/project/gradle.js +145 -0
- package/dist/cli/project/index.js +10 -0
- package/dist/cli/project/ios.js +190 -0
- package/dist/cli/project/jsx.js +327 -0
- package/dist/cli/project/plist.js +166 -0
- package/dist/cli/project/sources.js +86 -0
- package/dist/cli/result.js +12 -0
- package/dist/compare.d.ts +31 -0
- package/dist/compare.d.ts.map +1 -0
- package/dist/compare.js +68 -0
- package/dist/detect.d.ts +68 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/detect.js +103 -0
- package/dist/errors.d.ts +33 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +23 -0
- package/dist/fetchers/android.d.ts +53 -0
- package/dist/fetchers/android.d.ts.map +1 -0
- package/dist/fetchers/android.js +91 -0
- package/dist/fetchers/custom.d.ts +29 -0
- package/dist/fetchers/custom.d.ts.map +1 -0
- package/dist/fetchers/custom.js +41 -0
- package/dist/fetchers/ios.d.ts +61 -0
- package/dist/fetchers/ios.d.ts.map +1 -0
- package/dist/fetchers/ios.js +79 -0
- package/dist/index.d.ts +15 -14
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -58
- package/dist/openStore.d.ts +29 -0
- package/dist/openStore.d.ts.map +1 -0
- package/dist/openStore.js +46 -0
- package/dist/policy.d.ts +75 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +128 -0
- package/dist/react/DefaultForceModal.d.ts +26 -0
- package/dist/react/DefaultForceModal.d.ts.map +1 -0
- package/dist/react/DefaultForceModal.js +58 -0
- package/dist/react/DefaultWarningSheet.d.ts +26 -0
- package/dist/react/DefaultWarningSheet.d.ts.map +1 -0
- package/dist/react/DefaultWarningSheet.js +66 -0
- package/dist/react/StoreVersionModal.d.ts +52 -0
- package/dist/react/StoreVersionModal.d.ts.map +1 -0
- package/dist/react/StoreVersionModal.js +250 -0
- package/dist/react/StoreVersionProvider.d.ts +51 -0
- package/dist/react/StoreVersionProvider.d.ts.map +1 -0
- package/dist/react/StoreVersionProvider.js +69 -0
- package/dist/react/defaultUi.d.ts +72 -0
- package/dist/react/defaultUi.d.ts.map +1 -0
- package/dist/react/defaultUi.js +118 -0
- package/dist/react/engine.d.ts +146 -0
- package/dist/react/engine.d.ts.map +1 -0
- package/dist/react/engine.js +541 -0
- package/dist/react/texts.d.ts +43 -0
- package/dist/react/texts.d.ts.map +1 -0
- package/dist/react/texts.js +57 -0
- package/dist/react/useIsomorphicLayoutEffect.d.ts +3 -0
- package/dist/react/useIsomorphicLayoutEffect.d.ts.map +1 -0
- package/dist/react/useIsomorphicLayoutEffect.js +6 -0
- package/dist/react/useStoreVersion.d.ts +121 -0
- package/dist/react/useStoreVersion.d.ts.map +1 -0
- package/dist/react/useStoreVersion.js +23 -0
- package/dist/request.d.ts +78 -0
- package/dist/request.d.ts.map +1 -0
- package/dist/request.js +227 -0
- package/dist/resolve.d.ts +98 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +218 -0
- package/dist/texts/ja.d.ts +8 -0
- package/dist/texts/ja.d.ts.map +1 -0
- package/dist/texts/ja.js +18 -0
- package/dist/vendor/compare-versions.d.ts +52 -0
- package/dist/vendor/compare-versions.d.ts.map +1 -0
- package/dist/vendor/compare-versions.js +111 -0
- package/package.json +83 -44
- package/src/checkVersion.ts +273 -0
- package/src/cli/check.ts +59 -0
- package/src/cli/checks/ids.ts +163 -0
- package/src/cli/checks/installed.ts +75 -0
- package/src/cli/checks/peers.ts +34 -0
- package/src/cli/checks/rollout.ts +183 -0
- package/src/cli/checks/store.ts +105 -0
- package/src/cli/checks/versions.ts +108 -0
- package/src/cli/index.ts +51 -0
- package/src/cli/main.ts +229 -0
- package/src/cli/output.ts +122 -0
- package/src/cli/packages.ts +43 -0
- package/src/cli/project/android.ts +96 -0
- package/src/cli/project/expo.ts +232 -0
- package/src/cli/project/files.ts +56 -0
- package/src/cli/project/gradle.ts +181 -0
- package/src/cli/project/index.ts +18 -0
- package/src/cli/project/ios.ts +284 -0
- package/src/cli/project/jsx.ts +354 -0
- package/src/cli/project/plist.ts +179 -0
- package/src/cli/project/sources.ts +118 -0
- package/src/cli/result.ts +34 -0
- package/src/compare.ts +101 -0
- package/src/detect.ts +178 -0
- package/src/errors.ts +63 -0
- package/src/fetchers/android.ts +136 -0
- package/src/fetchers/custom.ts +84 -0
- package/src/fetchers/ios.ts +160 -0
- package/src/index.ts +37 -0
- package/src/openStore.ts +74 -0
- package/src/policy.ts +223 -0
- package/src/react/DefaultForceModal.tsx +100 -0
- package/src/react/DefaultWarningSheet.tsx +115 -0
- package/src/react/StoreVersionModal.tsx +365 -0
- package/src/react/StoreVersionProvider.tsx +149 -0
- package/src/react/defaultUi.tsx +205 -0
- package/src/react/engine.ts +768 -0
- package/src/react/texts.ts +119 -0
- package/src/react/useIsomorphicLayoutEffect.ts +8 -0
- package/src/react/useStoreVersion.ts +153 -0
- package/src/request.ts +328 -0
- package/src/resolve.ts +388 -0
- package/src/texts/ja.ts +26 -0
- package/src/vendor/compare-versions.ts +128 -0
- package/texts/ja.d.ts +2 -0
- package/texts/ja.js +3 -0
- package/.eslintrc.js +0 -51
- package/.github/FUNDING.yml +0 -3
- package/.github/workflows/pr.yml +0 -48
- package/.github/workflows/release.yml +0 -259
- package/.prettierrc.js +0 -6
- package/babel.config.js +0 -3
- package/dist/android.d.ts +0 -1
- package/dist/android.js +0 -17
- package/dist/ios.d.ts +0 -1
- package/dist/ios.js +0 -18
|
@@ -0,0 +1,768 @@
|
|
|
1
|
+
// Provider の判定の状態遷移と副作用(取得・AppState・onResult / onError)。React を import しない(React の state へは
|
|
2
|
+
// setModel で写すだけ。配線は StoreVersionProvider.tsx)。判定そのものは resolve に任せ、ここでは
|
|
3
|
+
// 「いつ取り直すか」「どの結果をどの状態にするか」「どのエラーを onError に渡すか」だけを決める
|
|
4
|
+
import { AppState, type AppStateStatus } from 'react-native';
|
|
5
|
+
|
|
6
|
+
import type { CompareDetail, CompareResult, DiffType } from '../compare.ts';
|
|
7
|
+
import { type DetectResult, detect } from '../detect.ts';
|
|
8
|
+
import { StoreVersionError } from '../errors.ts';
|
|
9
|
+
import type { VersionGetter } from '../fetchers/custom.ts';
|
|
10
|
+
import { openStore as openStorePage } from '../openStore.ts';
|
|
11
|
+
import {
|
|
12
|
+
type Action,
|
|
13
|
+
evaluatePolicy,
|
|
14
|
+
type Policy,
|
|
15
|
+
validatePolicy,
|
|
16
|
+
} from '../policy.ts';
|
|
17
|
+
import { isAbortError } from '../request.ts';
|
|
18
|
+
import {
|
|
19
|
+
attachedErrorsOf,
|
|
20
|
+
type ResolveResult,
|
|
21
|
+
resolve,
|
|
22
|
+
type StoreUrlResult,
|
|
23
|
+
} from '../resolve.ts';
|
|
24
|
+
import type {
|
|
25
|
+
StoreVersionStatus,
|
|
26
|
+
UseStoreVersionResult,
|
|
27
|
+
} from './useStoreVersion.ts';
|
|
28
|
+
|
|
29
|
+
// Metro / Babel が定義する開発時のフラグ。react-native の型には無いので、ここで型だけを付ける(無い環境では undefined)
|
|
30
|
+
declare const __DEV__: boolean | undefined;
|
|
31
|
+
|
|
32
|
+
/** 判定全体の既定のタイムアウト(Android の HTML が約 1.25MB あるため) */
|
|
33
|
+
export const DEFAULT_TIMEOUT_MS = 10000;
|
|
34
|
+
|
|
35
|
+
/** `onResult` に渡す値(`ready` になるたびに 1 回) */
|
|
36
|
+
export type StoreVersionCheckResult = {
|
|
37
|
+
/** v1 の `checkVersion()` と同じ比較の結果。最新バージョンが比較できず、最低バージョンで force になったときは null */
|
|
38
|
+
result: CompareResult | null;
|
|
39
|
+
detail: CompareDetail | null;
|
|
40
|
+
action: Action;
|
|
41
|
+
diff: DiffType | null;
|
|
42
|
+
local: string;
|
|
43
|
+
/** Play の版が比較できず、最低バージョンで force になったときは null */
|
|
44
|
+
remote: string | null;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Provider の props のうち、判定に使うもの(表示の props は StoreVersionProvider.tsx) */
|
|
48
|
+
export type StoreVersionCheckProps = {
|
|
49
|
+
/**
|
|
50
|
+
* false なら何もしない(`status: 'skipped'`。`onError` も呼ばない)。既定 true。
|
|
51
|
+
* true → false に変わったら判定を取り消して `skipped` にし、false → true に変わったら判定を始める
|
|
52
|
+
*/
|
|
53
|
+
enabled?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* アプリのバージョン。必ずネイティブのバージョン(iOS の `CFBundleShortVersionString` / Android の `versionName`)を渡す。
|
|
56
|
+
* `Constants.expoConfig.version` や `package.json` の値は JS バンドルに含まれ、OTA 更新で変わってしまう
|
|
57
|
+
* (ネイティブは古いのに「更新済み」と誤判定し、force が出なくなる)ので使わないこと。
|
|
58
|
+
* 省略すると expo-application の `nativeApplicationVersion` → react-native-device-info の `getVersion()` の順に取る
|
|
59
|
+
*/
|
|
60
|
+
version?: string | null;
|
|
61
|
+
/** iOS の bundleId。省略すると expo-application の `applicationId` → react-native-device-info の `getBundleId()` の順に取る */
|
|
62
|
+
bundleId?: string | null;
|
|
63
|
+
/** Android の package name。省略すると expo-application の `applicationId` → react-native-device-info の `getBundleId()` の順に取る */
|
|
64
|
+
packageName?: string | null;
|
|
65
|
+
/** iTunes Lookup の国(`jp` など)。既定は端末ロケールの地域(取れなければ `us`) */
|
|
66
|
+
country?: string;
|
|
67
|
+
/**
|
|
68
|
+
* ストアとの差分に対する扱い。既定 `{ major: 'force', minor: 'warning', patch: 'warning' }`。
|
|
69
|
+
* `'force'` のような 1 つの値、`{ minor: 'force' }` のような個別指定(省略したキーは既定)、`(local, remote) => Action` の関数で書ける。
|
|
70
|
+
* 変わったら取り直さずに、直近の判定の最新 / 最低バージョンで `action` だけを計算し直す
|
|
71
|
+
* (関数は判定のたびと policy が変わるたびに呼ばれ、インラインで書いた関数は描画のたびに変わる)。
|
|
72
|
+
* 判定が `POLICY_FAILED` で止まり取得結果が無いとき(取得の前に検証で止まった・取得の後に policy の関数が throw した)は、
|
|
73
|
+
* policy を直しても `recheck()` まで `error` のまま。
|
|
74
|
+
*
|
|
75
|
+
* 注意(Android の段階公開): Android で段階公開を使う場合、公開対象外のユーザーは新バージョンを
|
|
76
|
+
* 入手できないため、ストア差分による `force` でアプリを使えなくなる可能性がある。
|
|
77
|
+
* `Platform.OS === 'android'` のときは `major: 'warning'` にするか、`getMinimumVersion` で
|
|
78
|
+
* force を制御すること。iOS の段階的リリースは自動アップデートの配信ペースだけを制御し、
|
|
79
|
+
* App Store から手動ならいつでも更新できるので問題ない。
|
|
80
|
+
*/
|
|
81
|
+
policy?: Policy | null;
|
|
82
|
+
/** 判定全体のタイムアウト(ms)。既定 10000。最新バージョンが間に合わなければ `TIMEOUT`(UI は出さない) */
|
|
83
|
+
timeoutMs?: number;
|
|
84
|
+
/**
|
|
85
|
+
* バックグラウンドからの復帰時(`AppState` が `background` → `active`)にも再チェックする。既定 false。
|
|
86
|
+
* iOS の通知センターや権限ダイアログによる `inactive` → `active` では再チェックしない。
|
|
87
|
+
* Android には `inactive` が無く、権限ダイアログやほかのアプリから戻ったときも `background` → `active` になり再チェックする。
|
|
88
|
+
* Android は再チェックのたびに Play Store のページ(約 1.25MB)を取得する
|
|
89
|
+
*/
|
|
90
|
+
recheckOnForeground?: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* 最新バージョンの取得元(Remote Config / 自前の JSON など)。指定するとストアのバージョンは使わない
|
|
93
|
+
* (iOS はストア URL を得るためだけに lookup する。Android はストアに問い合わせない)。失敗はストアにフォールバックせず `error`。
|
|
94
|
+
* StrictMode では開発時だけ、マウント時に 2 回呼ばれうる(ストアへの fetch は 1 回にまとまる)
|
|
95
|
+
*/
|
|
96
|
+
getLatestVersion?: VersionGetter | null;
|
|
97
|
+
/**
|
|
98
|
+
* 最低バージョンの取得元。これ未満は差分に関係なく force。失敗は最低バージョンの判定だけを飛ばす(`onError` には渡す)。
|
|
99
|
+
* StrictMode では開発時だけ、マウント時に 2 回呼ばれうる
|
|
100
|
+
*/
|
|
101
|
+
getMinimumVersion?: VersionGetter | null;
|
|
102
|
+
/**
|
|
103
|
+
* 判定が `ready` になるたびに 1 回呼ぶ(`skipped` / `error` では呼ばない)。
|
|
104
|
+
* Promise を返しても待たない(reject しても unhandled rejection にはしない。開発時は `console.warn`)
|
|
105
|
+
*/
|
|
106
|
+
onResult?: (result: StoreVersionCheckResult) => void;
|
|
107
|
+
/**
|
|
108
|
+
* エラーを受け取る(UI は出さない)。1 回の判定で複数回呼ばれうる(例: 最低バージョンの取得の失敗と、
|
|
109
|
+
* 最新バージョンの取得の失敗、iOS のストア URL の lookup の失敗)。判定の取り消し(アンマウント・バックグラウンド)では呼ばない。
|
|
110
|
+
* 再チェックの失敗(前回の結果は残る)と `openStore()` の失敗(`STORE_URL_UNAVAILABLE`)も渡す。
|
|
111
|
+
* 渡さない場合は `__DEV__` のときだけ `console.warn` する。
|
|
112
|
+
* Promise を返しても待たない(reject しても unhandled rejection にはしない。開発時は `console.warn`)
|
|
113
|
+
*/
|
|
114
|
+
onError?: (error: StoreVersionError) => void;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const LOG_PREFIX = '[react-native-store-version]';
|
|
118
|
+
|
|
119
|
+
const isDev = (): boolean => typeof __DEV__ !== 'undefined' && __DEV__ === true;
|
|
120
|
+
|
|
121
|
+
/** locale(BCP 47。`ja-JP` / `zh-Hant-TW` / `en_US`)の地域を小文字の 2 文字で返す。無ければ null */
|
|
122
|
+
const regionOf = (locale: unknown): string | null => {
|
|
123
|
+
if (typeof locale !== 'string') {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
const subtags = locale.split(/[-_]/);
|
|
127
|
+
// 先頭は言語。2 文字の英字の subtag は地域だけ(extlang は 3 文字、script は 4 文字、variant は 4 文字以上)。
|
|
128
|
+
// 1 文字の subtag(`-u-` などの拡張)から後は見ない。数字 3 桁の地域(`419` など)は国ではないので使わない
|
|
129
|
+
for (let i = 1; i < subtags.length; i += 1) {
|
|
130
|
+
const subtag = subtags[i];
|
|
131
|
+
if (subtag.length === 1) {
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
if (/^[A-Za-z]{2}$/.test(subtag)) {
|
|
135
|
+
return subtag.toLowerCase();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* `country` の既定。端末ロケールの地域を JS だけで取る(`Intl.DateTimeFormat().resolvedOptions().locale`)。
|
|
143
|
+
* 取れなければ `us`。例外は投げない(パッケージの入口からは export しない)
|
|
144
|
+
*/
|
|
145
|
+
export const defaultCountry = (): string => {
|
|
146
|
+
try {
|
|
147
|
+
return regionOf(Intl.DateTimeFormat().resolvedOptions().locale) ?? 'us';
|
|
148
|
+
} catch {
|
|
149
|
+
return 'us';
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
type Evaluation = {
|
|
154
|
+
result: CompareResult | null;
|
|
155
|
+
detail: CompareDetail | null;
|
|
156
|
+
action: Action;
|
|
157
|
+
diff: DiffType | null;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/** policy を当て直すために残す、直近の判定の取得結果 */
|
|
161
|
+
type Snapshot = {
|
|
162
|
+
latest: string | null;
|
|
163
|
+
minimum: string | null;
|
|
164
|
+
storeUrl: string | null;
|
|
165
|
+
releaseNotes: string | undefined;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/** engine の状態。React の state にはこれをそのまま写す */
|
|
169
|
+
export type Model = {
|
|
170
|
+
status: StoreVersionStatus;
|
|
171
|
+
local: string | null;
|
|
172
|
+
error: StoreVersionError | null;
|
|
173
|
+
/**
|
|
174
|
+
* 直近の判定の取得結果。`ready`、再チェック中(前回の ready を見せ続ける `checking`)、
|
|
175
|
+
* policy を当て直して `POLICY_FAILED` になった `error` のときにある
|
|
176
|
+
*/
|
|
177
|
+
snapshot: Snapshot | null;
|
|
178
|
+
/** 見せる判定の結果。`ready` と、再チェック中(前回の ready の値)のときだけある */
|
|
179
|
+
evaluation: Evaluation | null;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export const IDLE: Model = {
|
|
183
|
+
status: 'idle',
|
|
184
|
+
local: null,
|
|
185
|
+
error: null,
|
|
186
|
+
snapshot: null,
|
|
187
|
+
evaluation: null,
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/** 1 回の判定。取り直すたびに作り直す */
|
|
191
|
+
type Run = {
|
|
192
|
+
controller: AbortController;
|
|
193
|
+
/** resolve が終わった(結果を受け取った / エラーにした) */
|
|
194
|
+
settled: boolean;
|
|
195
|
+
/** 判定は終わったが、iOS のストア URL の lookup(getLatestVersion を使うとき)がまだ終わっていない */
|
|
196
|
+
lookupPending: boolean;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/** ストアを開くのに要る、直近に検出したアプリ */
|
|
200
|
+
type Target = {
|
|
201
|
+
platform: 'ios' | 'android';
|
|
202
|
+
appId: string;
|
|
203
|
+
country: string;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/** 直近の判定の最新 / 最低バージョンに policy を当て直す。値で指定した policy は差分によらず検証する(resolve と同じ) */
|
|
207
|
+
const reevaluate = (
|
|
208
|
+
local: string,
|
|
209
|
+
snapshot: Snapshot,
|
|
210
|
+
policy: Policy | null | undefined
|
|
211
|
+
): { evaluation: Evaluation } | { error: StoreVersionError } => {
|
|
212
|
+
try {
|
|
213
|
+
validatePolicy(policy);
|
|
214
|
+
const { result, detail, action, diff } = evaluatePolicy({
|
|
215
|
+
local,
|
|
216
|
+
remote: snapshot.latest ?? '',
|
|
217
|
+
minimum: snapshot.minimum,
|
|
218
|
+
policy,
|
|
219
|
+
});
|
|
220
|
+
return { evaluation: { result, detail, action, diff } };
|
|
221
|
+
} catch (error) {
|
|
222
|
+
// 取得の結果は resolve で一度判定できているので、ここで投げうるのは policy の POLICY_FAILED だけ
|
|
223
|
+
return { error: error as StoreVersionError };
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/** Provider の判定の操作(StoreVersionProvider.tsx が effect と hook の値に配線する) */
|
|
228
|
+
export type Engine = {
|
|
229
|
+
/** マウント(AppState の購読と最初の判定) */
|
|
230
|
+
mount(): void;
|
|
231
|
+
/** アンマウント(判定を取り消し、それ以降は setState / onResult / onError を呼ばない) */
|
|
232
|
+
unmount(): void;
|
|
233
|
+
/** 判定を始める(`recheck()`)。バックグラウンドの間は active まで待つ */
|
|
234
|
+
start(): void;
|
|
235
|
+
/** `enabled` の切り替え */
|
|
236
|
+
setEnabled(enabled: boolean): void;
|
|
237
|
+
/** `policy` の変更(取り直さずに action を計算し直す) */
|
|
238
|
+
applyPolicy(policy: Policy | null | undefined): void;
|
|
239
|
+
openStore(): Promise<boolean>;
|
|
240
|
+
dismiss(): void;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Provider の engine を作る。判断はすべてここの変数(最新の値)で行い、React の state は `setModel` で写すだけにする。
|
|
245
|
+
* props はいつも `getProps()` で最新のものを読む(変わっても取り直さない。例外は `enabled` の切り替え)
|
|
246
|
+
*/
|
|
247
|
+
export const createEngine = (
|
|
248
|
+
getProps: () => StoreVersionCheckProps,
|
|
249
|
+
setModel: (model: Model) => void,
|
|
250
|
+
setDismissedRemote: (remote: string) => void
|
|
251
|
+
): Engine => {
|
|
252
|
+
let model: Model = IDLE;
|
|
253
|
+
// マウント中か。false の間は setState も onResult / onError も呼ばない
|
|
254
|
+
let mounted = false;
|
|
255
|
+
// 直近に始めた判定
|
|
256
|
+
let run: Run | null = null;
|
|
257
|
+
// いま見せている結果(ready と、再チェック中に見せ続けている前回の ready)を出した判定。
|
|
258
|
+
// 再チェックの間も、この判定の iOS の URL の lookup は止めずに残し、終わったら見せている結果の storeUrl に反映する
|
|
259
|
+
let shownRun: Run | null = null;
|
|
260
|
+
let target: Target | null = null;
|
|
261
|
+
let storeUrlResult: Promise<StoreUrlResult> | null = null;
|
|
262
|
+
// snapshot の evaluation を計算した policy(policy の変更を見分ける)
|
|
263
|
+
let evaluatedPolicy: Policy | null | undefined;
|
|
264
|
+
// 判定の途中でバックグラウンドになって取り消した。active に戻ったら取り直す
|
|
265
|
+
let abortedByBackground = false;
|
|
266
|
+
// バックグラウンドの間に取得を始めようとした(マウント・recheck・enabled の切り替え)。active になったら始める
|
|
267
|
+
let pendingStart = false;
|
|
268
|
+
// 直近の active の後に background になったか(background → active の復帰だけを見分ける。inactive → active は数えない)
|
|
269
|
+
let wentBackground = false;
|
|
270
|
+
// 直近に見た enabled(false → true / true → false の切り替えを見分ける)
|
|
271
|
+
let lastEnabled = true;
|
|
272
|
+
let subscription: { remove(): void } | null = null;
|
|
273
|
+
|
|
274
|
+
const publish = (next: Model) => {
|
|
275
|
+
model = next;
|
|
276
|
+
if (mounted) {
|
|
277
|
+
setModel(next);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const warnThrown = (name: string, error: unknown) => {
|
|
282
|
+
if (isDev()) {
|
|
283
|
+
console.warn(`${LOG_PREFIX} ${name} threw`, error);
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// 開発者のコールバックが throw しても、Promise を返して reject しても、unhandled rejection にしない(アプリを止めない)。
|
|
288
|
+
// 返した Promise は待たない
|
|
289
|
+
const call = <T>(name: string, fn: (arg: T) => unknown, arg: T) => {
|
|
290
|
+
try {
|
|
291
|
+
const returned = fn(arg);
|
|
292
|
+
if (
|
|
293
|
+
typeof returned === 'object' &&
|
|
294
|
+
returned !== null &&
|
|
295
|
+
typeof (returned as PromiseLike<unknown>).then === 'function'
|
|
296
|
+
) {
|
|
297
|
+
(returned as PromiseLike<unknown>).then(undefined, (error: unknown) =>
|
|
298
|
+
warnThrown(name, error)
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
} catch (error) {
|
|
302
|
+
warnThrown(name, error);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const report = (error: StoreVersionError) => {
|
|
307
|
+
if (!mounted) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const { onError } = getProps();
|
|
311
|
+
if (onError) {
|
|
312
|
+
call('onError', onError, error);
|
|
313
|
+
} else if (isDev()) {
|
|
314
|
+
console.warn(`${LOG_PREFIX} ${error.code}: ${error.message}`, error);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const notifyResult = () => {
|
|
319
|
+
const { onResult } = getProps();
|
|
320
|
+
const { evaluation, snapshot, local } = model;
|
|
321
|
+
if (!mounted || !onResult || !evaluation || !snapshot || local === null) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
call('onResult', onResult, {
|
|
325
|
+
result: evaluation.result,
|
|
326
|
+
detail: evaluation.detail,
|
|
327
|
+
action: evaluation.action,
|
|
328
|
+
diff: evaluation.diff,
|
|
329
|
+
local,
|
|
330
|
+
remote: snapshot.latest,
|
|
331
|
+
});
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const isCurrent = (r: Run) =>
|
|
335
|
+
mounted && run === r && !r.controller.signal.aborted;
|
|
336
|
+
|
|
337
|
+
/** 判定をしない / できない状態にする(前回の結果も消す) */
|
|
338
|
+
const finishWithoutCheck = (next: Model) => {
|
|
339
|
+
target = null;
|
|
340
|
+
storeUrlResult = null;
|
|
341
|
+
publish(next);
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
/** 見せている判定の結果(ready と、それを見せ続けている再チェック中)。無ければ null */
|
|
345
|
+
const shownReady = (): Model | null =>
|
|
346
|
+
(model.status === 'ready' || model.status === 'checking') &&
|
|
347
|
+
model.evaluation !== null &&
|
|
348
|
+
model.snapshot !== null
|
|
349
|
+
? model
|
|
350
|
+
: null;
|
|
351
|
+
|
|
352
|
+
/** 前回の判定の、まだ終わっていない iOS の URL の lookup を止める(取り直さない。onError にも渡さない) */
|
|
353
|
+
const stopLookup = (r: Run | null) => {
|
|
354
|
+
if (r?.settled && r.lookupPending) {
|
|
355
|
+
r.controller.abort();
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* 再チェックが失敗したら、見せ続けていた前回の ready に戻す(force を再チェックの失敗で消さない)。
|
|
361
|
+
* エラーは onError に渡すだけで hook の `error` には入れない(status が ready のまま error だけが入る状態を作らない)。
|
|
362
|
+
* 再チェック中に policy が変わっていたら、前回の取得結果に当て直す
|
|
363
|
+
*/
|
|
364
|
+
const restore = () => {
|
|
365
|
+
publish({ ...model, status: 'ready', error: null });
|
|
366
|
+
engine.applyPolicy(getProps().policy);
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
/** 判定の結果を受け取ったとき */
|
|
370
|
+
const onResolved = (
|
|
371
|
+
current: Run,
|
|
372
|
+
local: string,
|
|
373
|
+
policy: Policy | null | undefined,
|
|
374
|
+
resolved: ResolveResult
|
|
375
|
+
) => {
|
|
376
|
+
if (!isCurrent(current)) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
current.settled = true;
|
|
380
|
+
const snapshot: Snapshot = {
|
|
381
|
+
latest: resolved.latest,
|
|
382
|
+
minimum: resolved.minimum,
|
|
383
|
+
storeUrl: resolved.storeUrl,
|
|
384
|
+
releaseNotes: resolved.releaseNotes,
|
|
385
|
+
};
|
|
386
|
+
// 判定中に policy が変わっていたら、最新の policy で当て直してから出す(onResult を 2 回呼ばないように)
|
|
387
|
+
const latestPolicy = getProps().policy;
|
|
388
|
+
const outcome =
|
|
389
|
+
latestPolicy === policy
|
|
390
|
+
? { evaluation: resolved }
|
|
391
|
+
: reevaluate(local, snapshot, latestPolicy);
|
|
392
|
+
evaluatedPolicy = latestPolicy;
|
|
393
|
+
storeUrlResult = resolved.storeUrlResult;
|
|
394
|
+
// 新しい判定の結果を出すので、前回の判定の URL の lookup はもう要らない
|
|
395
|
+
if (shownRun !== current) {
|
|
396
|
+
stopLookup(shownRun);
|
|
397
|
+
}
|
|
398
|
+
shownRun = current;
|
|
399
|
+
|
|
400
|
+
if ('error' in outcome) {
|
|
401
|
+
publish({
|
|
402
|
+
status: 'error',
|
|
403
|
+
local,
|
|
404
|
+
error: outcome.error,
|
|
405
|
+
snapshot,
|
|
406
|
+
evaluation: null,
|
|
407
|
+
});
|
|
408
|
+
} else {
|
|
409
|
+
const { result, detail, action, diff } = outcome.evaluation;
|
|
410
|
+
publish({
|
|
411
|
+
status: 'ready',
|
|
412
|
+
local,
|
|
413
|
+
error: null,
|
|
414
|
+
snapshot,
|
|
415
|
+
evaluation: { result, detail, action, diff },
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
// 判定を止めなかったエラー(MINIMUM_VERSION_FAILED と、最新が比較できず最低バージョンで force にしたときの INVALID_VERSION)
|
|
419
|
+
for (const error of resolved.errors) {
|
|
420
|
+
report(error);
|
|
421
|
+
}
|
|
422
|
+
if ('error' in outcome) {
|
|
423
|
+
report(outcome.error);
|
|
424
|
+
} else {
|
|
425
|
+
notifyResult();
|
|
426
|
+
}
|
|
427
|
+
watchStoreUrl(current, resolved.storeUrlResult);
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* iOS で getLatestVersion を使うときは、URL の lookup が終わった時点で storeUrl を更新する。
|
|
432
|
+
* 起動時の URL の lookup の失敗(trackViewUrl が無いことも)は STORE_URL_UNAVAILABLE で onError へ(storeUrlResult は reject しない)。
|
|
433
|
+
* 再チェックの間(この判定の結果を見せ続けている間)に終わったときも、見せている結果に反映する
|
|
434
|
+
*/
|
|
435
|
+
const watchStoreUrl = (current: Run, result: Promise<StoreUrlResult>) => {
|
|
436
|
+
current.lookupPending = true;
|
|
437
|
+
void result.then((found) => {
|
|
438
|
+
current.lookupPending = false;
|
|
439
|
+
if (
|
|
440
|
+
!mounted ||
|
|
441
|
+
shownRun !== current ||
|
|
442
|
+
current.controller.signal.aborted
|
|
443
|
+
) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (found.error) {
|
|
447
|
+
report(found.error);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (model.snapshot && model.snapshot.storeUrl !== found.storeUrl) {
|
|
451
|
+
publish({
|
|
452
|
+
...model,
|
|
453
|
+
snapshot: { ...model.snapshot, storeUrl: found.storeUrl },
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
/** 判定が失敗したとき(取り消しは何もしない) */
|
|
460
|
+
const onRejected = (current: Run, local: string, error: unknown) => {
|
|
461
|
+
// 取り消した判定(アンマウント・バックグラウンド・取り直し)は何も出さない
|
|
462
|
+
if (!isCurrent(current) || isAbortError(error)) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
current.settled = true;
|
|
466
|
+
if (shownReady()) {
|
|
467
|
+
restore();
|
|
468
|
+
} else {
|
|
469
|
+
publish({
|
|
470
|
+
...IDLE,
|
|
471
|
+
status: 'error',
|
|
472
|
+
local,
|
|
473
|
+
error: error as StoreVersionError,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
report(error as StoreVersionError);
|
|
477
|
+
// 判定を止めたエラーに添えられた、ほかのエラー(その時点で分かっている最低バージョンのエラー)
|
|
478
|
+
for (const attached of attachedErrorsOf(error)) {
|
|
479
|
+
report(attached);
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
/** 取得を始める(detect の後。バックグラウンドの間は active まで待つ) */
|
|
484
|
+
const fetchFor = (
|
|
485
|
+
current: Run,
|
|
486
|
+
detected: Extract<DetectResult, { skipped: null }>
|
|
487
|
+
) => {
|
|
488
|
+
// RN のタイマーはバックグラウンドで止まるので、バックグラウンドの間は取得を始めず、active になったら始める
|
|
489
|
+
if (AppState.currentState === 'background') {
|
|
490
|
+
pendingStart = true;
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const props = getProps();
|
|
494
|
+
const { platform, version: local, appId } = detected;
|
|
495
|
+
const country = props.country ?? defaultCountry();
|
|
496
|
+
target = { platform, appId, country };
|
|
497
|
+
current.settled = false;
|
|
498
|
+
// 再チェックの間は前回の ready の値を見せ続ける(初回は前回の結果が無い)。
|
|
499
|
+
// 前回の URL の取得結果も、新しい判定が終わるまで openStore に使う
|
|
500
|
+
const previous = shownReady();
|
|
501
|
+
if (previous) {
|
|
502
|
+
publish({ ...previous, status: 'checking', error: null });
|
|
503
|
+
} else {
|
|
504
|
+
storeUrlResult = null;
|
|
505
|
+
publish({ ...IDLE, status: 'checking', local });
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const policy = props.policy;
|
|
509
|
+
// reject は onRejected で受ける(unhandled rejection にしない)
|
|
510
|
+
void resolve({
|
|
511
|
+
platform,
|
|
512
|
+
version: local,
|
|
513
|
+
appId,
|
|
514
|
+
country,
|
|
515
|
+
policy,
|
|
516
|
+
getLatestVersion: props.getLatestVersion,
|
|
517
|
+
getMinimumVersion: props.getMinimumVersion,
|
|
518
|
+
timeoutMs: props.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
519
|
+
signal: current.controller.signal,
|
|
520
|
+
}).then(
|
|
521
|
+
(resolved) => onResolved(current, local, policy, resolved),
|
|
522
|
+
(error: unknown) => onRejected(current, local, error)
|
|
523
|
+
);
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
const start = () => {
|
|
527
|
+
if (!mounted) {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
pendingStart = false;
|
|
531
|
+
abortedByBackground = false;
|
|
532
|
+
// 判定中なら取り消す(onError には渡さない)。同じストアへの取得は request.ts が次の判定に引き継ぐ。
|
|
533
|
+
// 判定が終わって URL の lookup だけが残っている判定は止めない(前回の結果の storeUrl に反映するため)
|
|
534
|
+
if (run && !run.settled) {
|
|
535
|
+
run.controller.abort();
|
|
536
|
+
}
|
|
537
|
+
const current: Run = {
|
|
538
|
+
controller: new AbortController(),
|
|
539
|
+
settled: true,
|
|
540
|
+
lookupPending: false,
|
|
541
|
+
};
|
|
542
|
+
run = current;
|
|
543
|
+
|
|
544
|
+
// 取得しない(skipped)・できない(LOCAL_VERSION_UNAVAILABLE)は、バックグラウンドでもすぐ決める
|
|
545
|
+
const props = getProps();
|
|
546
|
+
if (props.enabled === false) {
|
|
547
|
+
finishWithoutCheck({ ...IDLE, status: 'skipped' });
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
let detected: DetectResult;
|
|
551
|
+
try {
|
|
552
|
+
detected = detect({
|
|
553
|
+
version: props.version,
|
|
554
|
+
bundleId: props.bundleId,
|
|
555
|
+
packageName: props.packageName,
|
|
556
|
+
});
|
|
557
|
+
} catch (error) {
|
|
558
|
+
// LOCAL_VERSION_UNAVAILABLE
|
|
559
|
+
if (shownReady()) {
|
|
560
|
+
restore();
|
|
561
|
+
} else {
|
|
562
|
+
finishWithoutCheck({
|
|
563
|
+
...IDLE,
|
|
564
|
+
status: 'error',
|
|
565
|
+
error: error as StoreVersionError,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
// 報告は microtask に回し、そのときまだこの判定が最新のときだけ渡す(StrictMode のマウント・アンマウント・マウントで
|
|
569
|
+
// 1 回目の判定の報告を捨て、2 回報告しないように)
|
|
570
|
+
void Promise.resolve().then(() => {
|
|
571
|
+
if (isCurrent(current)) {
|
|
572
|
+
report(error as StoreVersionError);
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
if (detected.skipped !== null) {
|
|
578
|
+
finishWithoutCheck({ ...IDLE, status: 'skipped' });
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
fetchFor(current, detected);
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
const onAppStateChange = (next: AppStateStatus) => {
|
|
585
|
+
if (next === 'background') {
|
|
586
|
+
wentBackground = true;
|
|
587
|
+
// RN のタイマーはバックグラウンドで止まり、復帰時に期限切れで発火する(TIMEOUT になり、この起動で force を出す機会を失う)ので、
|
|
588
|
+
// 終わっていない判定は取り消して、復帰時に取り直す(recheckOnForeground に関係なく)
|
|
589
|
+
if (run && !run.settled && !run.controller.signal.aborted) {
|
|
590
|
+
run.controller.abort();
|
|
591
|
+
abortedByBackground = true;
|
|
592
|
+
}
|
|
593
|
+
// 判定は終わったが iOS の URL の lookup がまだなら、それだけを止める(取り直さない。storeUrlResult はエラーで終わり、
|
|
594
|
+
// openStore はその場で lookup し直す)
|
|
595
|
+
stopLookup(run);
|
|
596
|
+
stopLookup(shownRun);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (next === 'active') {
|
|
600
|
+
const returned = wentBackground;
|
|
601
|
+
wentBackground = false;
|
|
602
|
+
// 待っていた取得の開始・取り消した判定の取り直し・recheckOnForeground が重なっても、始めるのは 1 回
|
|
603
|
+
if (
|
|
604
|
+
pendingStart ||
|
|
605
|
+
(returned &&
|
|
606
|
+
(abortedByBackground || getProps().recheckOnForeground === true))
|
|
607
|
+
) {
|
|
608
|
+
start();
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
const engine: Engine = {
|
|
614
|
+
mount() {
|
|
615
|
+
mounted = true;
|
|
616
|
+
wentBackground = false;
|
|
617
|
+
pendingStart = false;
|
|
618
|
+
lastEnabled = getProps().enabled !== false;
|
|
619
|
+
subscription = AppState.addEventListener('change', onAppStateChange);
|
|
620
|
+
// バックグラウンドのままマウントされたら、取得は active になるまで始めない(status は idle のまま)
|
|
621
|
+
start();
|
|
622
|
+
},
|
|
623
|
+
unmount() {
|
|
624
|
+
// 取り消した後は onResult / onError / setState を呼ばない(resolve の abort は何も添えずに AbortError で終わる)
|
|
625
|
+
mounted = false;
|
|
626
|
+
pendingStart = false;
|
|
627
|
+
subscription?.remove();
|
|
628
|
+
subscription = null;
|
|
629
|
+
run?.controller.abort();
|
|
630
|
+
shownRun?.controller.abort();
|
|
631
|
+
run = null;
|
|
632
|
+
shownRun = null;
|
|
633
|
+
},
|
|
634
|
+
start,
|
|
635
|
+
setEnabled(enabled) {
|
|
636
|
+
if (!mounted || enabled === lastEnabled) {
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
lastEnabled = enabled;
|
|
640
|
+
if (enabled) {
|
|
641
|
+
// false → true: 判定を始める
|
|
642
|
+
start();
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
// true → false: 判定を取り消して skipped にする(onError は呼ばない)
|
|
646
|
+
run?.controller.abort();
|
|
647
|
+
shownRun?.controller.abort();
|
|
648
|
+
run = null;
|
|
649
|
+
shownRun = null;
|
|
650
|
+
pendingStart = false;
|
|
651
|
+
abortedByBackground = false;
|
|
652
|
+
finishWithoutCheck({ ...IDLE, status: 'skipped' });
|
|
653
|
+
},
|
|
654
|
+
applyPolicy(policy) {
|
|
655
|
+
const { snapshot, local } = model;
|
|
656
|
+
// 再チェック中は当て直さない(終わったときに最新の policy で当て直す。失敗したら前回の結果に当て直す)
|
|
657
|
+
if (
|
|
658
|
+
!mounted ||
|
|
659
|
+
model.status === 'checking' ||
|
|
660
|
+
!snapshot ||
|
|
661
|
+
local === null ||
|
|
662
|
+
policy === evaluatedPolicy
|
|
663
|
+
) {
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
evaluatedPolicy = policy;
|
|
667
|
+
const outcome = reevaluate(local, snapshot, policy);
|
|
668
|
+
if ('error' in outcome) {
|
|
669
|
+
// 不正な policy が続く(インラインで書いた関数が描画のたびに throw するなど)間は、onError を繰り返さない
|
|
670
|
+
if (model.status === 'ready') {
|
|
671
|
+
publish({
|
|
672
|
+
...model,
|
|
673
|
+
status: 'error',
|
|
674
|
+
error: outcome.error,
|
|
675
|
+
evaluation: null,
|
|
676
|
+
});
|
|
677
|
+
report(outcome.error);
|
|
678
|
+
}
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
// action が変わらなければ何もしない(インラインで書いた policy で、描画のたびに onResult を呼ばないように)
|
|
682
|
+
if (
|
|
683
|
+
model.status === 'ready' &&
|
|
684
|
+
model.evaluation?.action === outcome.evaluation.action
|
|
685
|
+
) {
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
publish({
|
|
689
|
+
...model,
|
|
690
|
+
status: 'ready',
|
|
691
|
+
error: null,
|
|
692
|
+
evaluation: outcome.evaluation,
|
|
693
|
+
});
|
|
694
|
+
notifyResult();
|
|
695
|
+
},
|
|
696
|
+
async openStore() {
|
|
697
|
+
const opening = target;
|
|
698
|
+
if (opening === null) {
|
|
699
|
+
// 判定しない環境(skipped)や、アプリのバージョン / ID が取れなかったときは、開く先が分からない
|
|
700
|
+
report(
|
|
701
|
+
new StoreVersionError(
|
|
702
|
+
'STORE_URL_UNAVAILABLE',
|
|
703
|
+
`cannot open the store: the app was not detected (status: ${model.status})`
|
|
704
|
+
)
|
|
705
|
+
);
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
try {
|
|
709
|
+
await openStorePage({
|
|
710
|
+
...opening,
|
|
711
|
+
storeUrl: storeUrlResult,
|
|
712
|
+
timeoutMs: getProps().timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
713
|
+
});
|
|
714
|
+
return true;
|
|
715
|
+
} catch (error) {
|
|
716
|
+
// STORE_URL_UNAVAILABLE(iOS の lookup の失敗 / Linking.openURL の失敗)。onError に渡すだけで reject しない
|
|
717
|
+
// (`onPress={openStore}` のまま使っても unhandled rejection にならないように)。status / error は変えない
|
|
718
|
+
report(error as StoreVersionError);
|
|
719
|
+
return false;
|
|
720
|
+
}
|
|
721
|
+
},
|
|
722
|
+
dismiss() {
|
|
723
|
+
// 再チェック中も、前回の ready の warning なら閉じられる
|
|
724
|
+
const shown = shownReady();
|
|
725
|
+
if (
|
|
726
|
+
mounted &&
|
|
727
|
+
shown?.evaluation?.action === 'warning' &&
|
|
728
|
+
shown.snapshot?.latest != null
|
|
729
|
+
) {
|
|
730
|
+
setDismissedRemote(shown.snapshot.latest);
|
|
731
|
+
}
|
|
732
|
+
},
|
|
733
|
+
};
|
|
734
|
+
return engine;
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* engine の状態から hook の戻り値を作る。判定の結果(result / action / remote / storeUrl など)は ready のとき
|
|
739
|
+
* (再チェック中は前回の ready の値)だけ見せる
|
|
740
|
+
*/
|
|
741
|
+
export const stateOf = (
|
|
742
|
+
model: Model,
|
|
743
|
+
dismissedRemote: string | null,
|
|
744
|
+
engine: Engine
|
|
745
|
+
): UseStoreVersionResult => {
|
|
746
|
+
const evaluation = model.evaluation;
|
|
747
|
+
const snapshot = evaluation ? model.snapshot : null;
|
|
748
|
+
const action = evaluation?.action ?? null;
|
|
749
|
+
const remote = snapshot?.latest ?? null;
|
|
750
|
+
return {
|
|
751
|
+
status: model.status,
|
|
752
|
+
result: evaluation?.result ?? null,
|
|
753
|
+
detail: evaluation?.detail ?? null,
|
|
754
|
+
action,
|
|
755
|
+
diff: evaluation?.diff ?? null,
|
|
756
|
+
// 「あとで」は remote のバージョン単位。remote が変われば解除、force なら常に出す
|
|
757
|
+
dismissed:
|
|
758
|
+
action === 'warning' && remote !== null && remote === dismissedRemote,
|
|
759
|
+
local: model.local,
|
|
760
|
+
remote,
|
|
761
|
+
storeUrl: snapshot?.storeUrl ?? null,
|
|
762
|
+
releaseNotes: snapshot?.releaseNotes,
|
|
763
|
+
openStore: engine.openStore,
|
|
764
|
+
dismiss: engine.dismiss,
|
|
765
|
+
recheck: engine.start,
|
|
766
|
+
error: model.error,
|
|
767
|
+
};
|
|
768
|
+
};
|