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,160 @@
|
|
|
1
|
+
// App Store(iTunes Lookup API)の取得。react-native を import しない(Node でも動かす)
|
|
2
|
+
import { StoreVersionError } from '../errors.ts';
|
|
3
|
+
import { requestText } from '../request.ts';
|
|
4
|
+
|
|
5
|
+
/** lookup の引き先。`bundleId`(v2 の既定)か、ストア URL から抜き出した数字の App ID(v1 互換の URL 方式) */
|
|
6
|
+
export type IOSLookupTarget = { bundleId: string } | { appId: string };
|
|
7
|
+
|
|
8
|
+
/** {@link lookupIOS} のオプション */
|
|
9
|
+
export type IOSLookupOptions = {
|
|
10
|
+
/** 未指定ならタイムアウトしない(`requestText` の `timeoutMs`) */
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** {@link lookupIOS} の結果 */
|
|
16
|
+
export type IOSLookupResult = {
|
|
17
|
+
/** `results[0].version` */
|
|
18
|
+
version: string;
|
|
19
|
+
/** `results[0].trackViewUrl`(ストアを開く URL)。応答に無ければ null */
|
|
20
|
+
storeUrl: string | null;
|
|
21
|
+
/** `results[0].releaseNotes`。応答に無ければ undefined */
|
|
22
|
+
releaseNotes?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* App Store の URL(`https://apps.apple.com/jp/app/github/id1477376905` など)から数字の App ID を抜き出す。
|
|
27
|
+
* v1 と同じ正規表現。取れなければ null
|
|
28
|
+
*/
|
|
29
|
+
export const appIdFromStoreUrl = (storeUrl: string): string | null =>
|
|
30
|
+
storeUrl.match(/.+id([0-9]+)\??/)?.[1] ?? null;
|
|
31
|
+
|
|
32
|
+
/** lookup の URL(タイムスタンプを付ける前。in-flight の共有のキーにもなる) */
|
|
33
|
+
export const lookupUrl = (target: IOSLookupTarget, country: string): string => {
|
|
34
|
+
const query =
|
|
35
|
+
'bundleId' in target
|
|
36
|
+
? `bundleId=${encodeURIComponent(target.bundleId)}`
|
|
37
|
+
: `id=${encodeURIComponent(target.appId)}`;
|
|
38
|
+
return `https://itunes.apple.com/lookup?${query}&country=${encodeURIComponent(country)}`;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const describeTarget = (target: IOSLookupTarget): string =>
|
|
42
|
+
'bundleId' in target
|
|
43
|
+
? `bundleId ${JSON.stringify(target.bundleId)}`
|
|
44
|
+
: `appId ${JSON.stringify(target.appId)}`;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* iTunes Lookup API で App Store の版を引く。v1 と同じく URL にタイムスタンプを付け、`cache-control: no-cache` を送る。
|
|
48
|
+
*
|
|
49
|
+
* - 通信の失敗 / 200 以外 / JSON として読めない / `results[0].version` が文字列でない → `NETWORK`
|
|
50
|
+
* - `results` が空(未リリース・その国で未配信)→ `NOT_FOUND`
|
|
51
|
+
* - タイムアウト → `TIMEOUT`、`signal` の abort → `AbortError`
|
|
52
|
+
*/
|
|
53
|
+
export const lookupIOS = async (
|
|
54
|
+
target: IOSLookupTarget,
|
|
55
|
+
country: string,
|
|
56
|
+
{ timeoutMs, signal }: IOSLookupOptions = {}
|
|
57
|
+
): Promise<IOSLookupResult> => {
|
|
58
|
+
const url = lookupUrl(target, country);
|
|
59
|
+
const { status, body } = await requestText(url, {
|
|
60
|
+
timeoutMs,
|
|
61
|
+
signal,
|
|
62
|
+
noCache: true,
|
|
63
|
+
});
|
|
64
|
+
if (status !== 200) {
|
|
65
|
+
throw new StoreVersionError(
|
|
66
|
+
'NETWORK',
|
|
67
|
+
`iTunes Lookup responded with status ${status}`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let json: unknown;
|
|
72
|
+
try {
|
|
73
|
+
json = JSON.parse(body);
|
|
74
|
+
} catch (cause) {
|
|
75
|
+
throw new StoreVersionError(
|
|
76
|
+
'NETWORK',
|
|
77
|
+
'iTunes Lookup returned a response that is not JSON',
|
|
78
|
+
{ cause }
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// v1 と同じく、results が無い・空なら「ストアに無い」(v1 は `appID(<id>) is not released.`)
|
|
83
|
+
const results = (json as { results?: unknown } | null)?.results;
|
|
84
|
+
if (!Array.isArray(results) || results.length === 0) {
|
|
85
|
+
throw new StoreVersionError(
|
|
86
|
+
'NOT_FOUND',
|
|
87
|
+
`the app (${describeTarget(target)}) is not found on the App Store (${country})`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const app = results[0] as {
|
|
92
|
+
version?: unknown;
|
|
93
|
+
trackViewUrl?: unknown;
|
|
94
|
+
releaseNotes?: unknown;
|
|
95
|
+
} | null;
|
|
96
|
+
if (typeof app?.version !== 'string') {
|
|
97
|
+
throw new StoreVersionError(
|
|
98
|
+
'NETWORK',
|
|
99
|
+
'iTunes Lookup returned a result without a version'
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const result: IOSLookupResult = {
|
|
104
|
+
version: app.version,
|
|
105
|
+
storeUrl:
|
|
106
|
+
typeof app.trackViewUrl === 'string' && app.trackViewUrl !== ''
|
|
107
|
+
? app.trackViewUrl
|
|
108
|
+
: null,
|
|
109
|
+
};
|
|
110
|
+
if (typeof app.releaseNotes === 'string') {
|
|
111
|
+
result.releaseNotes = app.releaseNotes;
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* ストアを開く URL の取得結果。reject しない形にしてある(判定とは別に後から受け取るので、
|
|
118
|
+
* 誰も待っていなくても unhandled rejection にならないように)
|
|
119
|
+
*/
|
|
120
|
+
export type StoreUrlResult =
|
|
121
|
+
| { storeUrl: string; error: null }
|
|
122
|
+
| { storeUrl: null; error: StoreVersionError };
|
|
123
|
+
|
|
124
|
+
/** lookup に失敗して URL を取れなかったときの `STORE_URL_UNAVAILABLE`。cause に lookup のエラー */
|
|
125
|
+
export const storeUrlUnavailable = (
|
|
126
|
+
cause: unknown
|
|
127
|
+
): { storeUrl: null; error: StoreVersionError } => ({
|
|
128
|
+
storeUrl: null,
|
|
129
|
+
error: new StoreVersionError(
|
|
130
|
+
'STORE_URL_UNAVAILABLE',
|
|
131
|
+
'could not get the App Store URL',
|
|
132
|
+
{ cause }
|
|
133
|
+
),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
/** lookup の `storeUrl` から {@link StoreUrlResult} を作る(trackViewUrl が無ければ `STORE_URL_UNAVAILABLE`) */
|
|
137
|
+
export const toStoreUrlResult = (storeUrl: string | null): StoreUrlResult =>
|
|
138
|
+
storeUrl === null
|
|
139
|
+
? {
|
|
140
|
+
storeUrl: null,
|
|
141
|
+
error: new StoreVersionError(
|
|
142
|
+
'STORE_URL_UNAVAILABLE',
|
|
143
|
+
'iTunes Lookup returned no trackViewUrl'
|
|
144
|
+
),
|
|
145
|
+
}
|
|
146
|
+
: { storeUrl, error: null };
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* bundleId で lookup して、ストアを開く URL(`trackViewUrl`)だけを取る。`version` / `releaseNotes` は使わない
|
|
150
|
+
* (`getLatestVersion` の値と食い違いうるため)。失敗は reject せず `STORE_URL_UNAVAILABLE`(cause に lookup のエラー)で返す
|
|
151
|
+
*/
|
|
152
|
+
export const lookupStoreUrl = (
|
|
153
|
+
bundleId: string,
|
|
154
|
+
country: string,
|
|
155
|
+
options: IOSLookupOptions = {}
|
|
156
|
+
): Promise<StoreUrlResult> =>
|
|
157
|
+
lookupIOS({ bundleId }, country, options).then(
|
|
158
|
+
(result) => toStoreUrlResult(result.storeUrl),
|
|
159
|
+
storeUrlUnavailable
|
|
160
|
+
);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// パッケージの入口。ここから export したものだけが公開 API になる
|
|
2
|
+
// (resolve / evaluatePolicy / request / fetcher / detect / openStore などの内部の関数と、Provider の Context は出さない)
|
|
3
|
+
import { checkVersion } from './checkVersion.ts';
|
|
4
|
+
|
|
5
|
+
export type {
|
|
6
|
+
CheckVersionParams,
|
|
7
|
+
CheckVersionResponse,
|
|
8
|
+
} from './checkVersion.ts';
|
|
9
|
+
export type { CompareDetail, CompareResult, DiffType } from './compare.ts';
|
|
10
|
+
export { compareVersion } from './compare.ts';
|
|
11
|
+
export type { StoreVersionErrorCode } from './errors.ts';
|
|
12
|
+
export { StoreVersionError } from './errors.ts';
|
|
13
|
+
export type { VersionGetter, VersionGetterContext } from './fetchers/custom.ts';
|
|
14
|
+
export type { Action, Policy } from './policy.ts';
|
|
15
|
+
export type {
|
|
16
|
+
StoreVersionCheckResult,
|
|
17
|
+
StoreVersionProviderProps,
|
|
18
|
+
} from './react/StoreVersionProvider.tsx';
|
|
19
|
+
export { StoreVersionProvider } from './react/StoreVersionProvider.tsx';
|
|
20
|
+
export type {
|
|
21
|
+
StoreVersionForceTexts,
|
|
22
|
+
StoreVersionResolvedTexts,
|
|
23
|
+
StoreVersionTexts,
|
|
24
|
+
StoreVersionWarningTexts,
|
|
25
|
+
} from './react/texts.ts';
|
|
26
|
+
export type {
|
|
27
|
+
StoreVersionComponents,
|
|
28
|
+
StoreVersionForceProps,
|
|
29
|
+
StoreVersionStatus,
|
|
30
|
+
StoreVersionWarningProps,
|
|
31
|
+
UseStoreVersionResult,
|
|
32
|
+
} from './react/useStoreVersion.ts';
|
|
33
|
+
export { useStoreVersion } from './react/useStoreVersion.ts';
|
|
34
|
+
export { checkVersion };
|
|
35
|
+
|
|
36
|
+
// v1 互換の default export。MIGRATION では named import(`import { checkVersion } from ...`)を勧める
|
|
37
|
+
export default checkVersion;
|
package/src/openStore.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// ストアを開く。ストアの URL は常にアプリ自身の ID から求める(開発者が URL や App ID を指定する手段は作らない。
|
|
2
|
+
// 指定できると、意図的に別のアプリへ誘導できてしまう)
|
|
3
|
+
import { Linking } from 'react-native';
|
|
4
|
+
|
|
5
|
+
import { StoreVersionError } from './errors.ts';
|
|
6
|
+
import { playStoreUrl } from './fetchers/android.ts';
|
|
7
|
+
import { lookupStoreUrl, type StoreUrlResult } from './fetchers/ios.ts';
|
|
8
|
+
|
|
9
|
+
/** `Linking.openURL` の失敗を、lookup の失敗と同じ `STORE_URL_UNAVAILABLE` に包む(cause に元のエラー) */
|
|
10
|
+
const openURL = async (url: string): Promise<void> => {
|
|
11
|
+
try {
|
|
12
|
+
await Linking.openURL(url);
|
|
13
|
+
} catch (cause) {
|
|
14
|
+
throw new StoreVersionError(
|
|
15
|
+
'STORE_URL_UNAVAILABLE',
|
|
16
|
+
`could not open the store URL: ${url}`,
|
|
17
|
+
{ cause }
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** {@link openStore} の引数 */
|
|
23
|
+
export type OpenStoreParams = {
|
|
24
|
+
platform: 'ios' | 'android';
|
|
25
|
+
/** iOS は bundleId、Android は package name(detect の `appId`) */
|
|
26
|
+
appId: string;
|
|
27
|
+
/** iOS でもう一度 lookup するときの `country`(判定と同じ値) */
|
|
28
|
+
country: string;
|
|
29
|
+
/**
|
|
30
|
+
* iOS: 判定で取得中 / 取得済みの URL(resolve の `storeUrlResult`)。取得中なら完了を待ってから開き、
|
|
31
|
+
* 失敗していた・渡されなかったら、その場でもう一度 lookup する。Android では使わない
|
|
32
|
+
*/
|
|
33
|
+
storeUrl?: PromiseLike<StoreUrlResult> | null;
|
|
34
|
+
/** iOS でもう一度 lookup するときのタイムアウト(ms)。未指定ならタイムアウトしない */
|
|
35
|
+
timeoutMs?: number;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* ストアのアプリのページを開く。失敗はすべて `STORE_URL_UNAVAILABLE`(cause に元のエラー)で reject する
|
|
40
|
+
* (開発者が `onError` だけで失敗を拾えるように、Provider はこのエラーを `onError` にも渡す)。
|
|
41
|
+
*
|
|
42
|
+
* - iOS: lookup の `trackViewUrl` を開く。取得中なら待ち、失敗していたらもう一度 lookup する。
|
|
43
|
+
* それも失敗したら reject する(cause に lookup のエラー。もう一度呼べば再試行する)。
|
|
44
|
+
* `Linking.openURL` 自体の失敗も reject する(cause に `Linking.openURL` のエラー)
|
|
45
|
+
* - Android: `market://details?id=<package>` を開き、失敗したら `https://play.google.com/store/apps/details?id=<package>`。
|
|
46
|
+
* https も失敗したときだけ reject する(cause に https の `Linking.openURL` のエラー)。
|
|
47
|
+
* `canOpenURL` には頼らない(iOS の LSApplicationQueriesSchemes / Android 11 以降の queries の宣言が要るため)
|
|
48
|
+
*/
|
|
49
|
+
export const openStore = async ({
|
|
50
|
+
platform,
|
|
51
|
+
appId,
|
|
52
|
+
country,
|
|
53
|
+
storeUrl,
|
|
54
|
+
timeoutMs,
|
|
55
|
+
}: OpenStoreParams): Promise<void> => {
|
|
56
|
+
if (platform === 'ios') {
|
|
57
|
+
let found = storeUrl ? await storeUrl : null;
|
|
58
|
+
if (!found || found.error) {
|
|
59
|
+
found = await lookupStoreUrl(appId, country, { timeoutMs });
|
|
60
|
+
}
|
|
61
|
+
if (found.error) {
|
|
62
|
+
throw found.error;
|
|
63
|
+
}
|
|
64
|
+
await openURL(found.storeUrl);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
await Linking.openURL(`market://details?id=${encodeURIComponent(appId)}`);
|
|
70
|
+
} catch {
|
|
71
|
+
// Play ストアのアプリが無い端末(一部のエミュレータ・中国向けの端末など)はブラウザで開く
|
|
72
|
+
await openURL(playStoreUrl(appId));
|
|
73
|
+
}
|
|
74
|
+
};
|
package/src/policy.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertValidVersion,
|
|
3
|
+
type CompareDetail,
|
|
4
|
+
type CompareResult,
|
|
5
|
+
compareVersionOrThrow,
|
|
6
|
+
type DiffType,
|
|
7
|
+
diffType,
|
|
8
|
+
toDetail,
|
|
9
|
+
} from './compare.ts';
|
|
10
|
+
import { StoreVersionError } from './errors.ts';
|
|
11
|
+
|
|
12
|
+
/** 判定の結果。`force` は強制、`warning` は警告、`none` は何も出さない */
|
|
13
|
+
export type Action = 'force' | 'warning' | 'none';
|
|
14
|
+
|
|
15
|
+
export type { DiffType };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* ストアとの差分に対する扱い。
|
|
19
|
+
*
|
|
20
|
+
* - 文字列 1 つ: 差分の種類によらずその値
|
|
21
|
+
* - オブジェクト: 差分の種類(`major` / `minor` / `patch`)ごとの値。省略したキーはデフォルト
|
|
22
|
+
* (`{ major: 'force', minor: 'warning', patch: 'warning' }`)
|
|
23
|
+
* - 関数: `(local, remote)` から値を返す
|
|
24
|
+
*
|
|
25
|
+
* 注意(Android の段階公開): Android で段階公開を使う場合、公開対象外のユーザーは新バージョンを
|
|
26
|
+
* 入手できないため、ストア差分による `force` でアプリを使えなくなる可能性がある。
|
|
27
|
+
* `Platform.OS === 'android'` のときは `major: 'warning'` にするか、`getMinimumVersion` で
|
|
28
|
+
* force を制御すること。iOS の段階的リリースは自動アップデートの配信ペースだけを制御し、
|
|
29
|
+
* App Store から手動ならいつでも更新できるので問題ない。
|
|
30
|
+
*/
|
|
31
|
+
export type Policy =
|
|
32
|
+
| Action
|
|
33
|
+
| Partial<Record<DiffType, Action>>
|
|
34
|
+
| ((local: string, remote: string) => Action);
|
|
35
|
+
|
|
36
|
+
/** デフォルトの policy */
|
|
37
|
+
export const DEFAULT_POLICY: Readonly<Record<DiffType, Action>> = Object.freeze(
|
|
38
|
+
{
|
|
39
|
+
major: 'force',
|
|
40
|
+
minor: 'warning',
|
|
41
|
+
patch: 'warning',
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
/** {@link evaluatePolicy} の結果 */
|
|
46
|
+
export type PolicyResult = {
|
|
47
|
+
/** local と remote の比較(policy では変わらない)。remote が比較できず最低バージョンで force になったときだけ null */
|
|
48
|
+
result: CompareResult | null;
|
|
49
|
+
/** v1 と同じ `detail`(policy では変わらない)。`result` が null のときは null */
|
|
50
|
+
detail: CompareDetail | null;
|
|
51
|
+
/** policy と最低バージョンから決まる扱い */
|
|
52
|
+
action: Action;
|
|
53
|
+
/** 差分の種類。`result === 'new'` のときだけ値がある */
|
|
54
|
+
diff: DiffType | null;
|
|
55
|
+
/** 最低バージョンが比較できなかったとき(`MINIMUM_VERSION_FAILED`)。最低バージョンを飛ばして判定した */
|
|
56
|
+
minimumError?: StoreVersionError;
|
|
57
|
+
/** remote が比較できないが最低バージョン未満で force にしたとき(`INVALID_VERSION`) */
|
|
58
|
+
remoteError?: StoreVersionError;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** {@link evaluatePolicy} の引数 */
|
|
62
|
+
export type EvaluatePolicyParams = {
|
|
63
|
+
local: string;
|
|
64
|
+
remote: string;
|
|
65
|
+
/** 空文字 / null / undefined は指定なし */
|
|
66
|
+
minimum?: string | null;
|
|
67
|
+
/** 省略 / null / undefined はデフォルト */
|
|
68
|
+
policy?: Policy | null;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const ACTIONS: readonly string[] = ['force', 'warning', 'none'];
|
|
72
|
+
|
|
73
|
+
const toAction = (value: unknown, cause?: unknown): Action => {
|
|
74
|
+
if (typeof value === 'string' && ACTIONS.includes(value)) {
|
|
75
|
+
return value as Action;
|
|
76
|
+
}
|
|
77
|
+
throw new StoreVersionError(
|
|
78
|
+
'POLICY_FAILED',
|
|
79
|
+
`policy returned an invalid action: ${typeof value === 'string' ? JSON.stringify(value) : `(${typeof value})`}`,
|
|
80
|
+
{ cause }
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const resolveAction = (
|
|
85
|
+
policy: Policy,
|
|
86
|
+
diff: DiffType,
|
|
87
|
+
local: string,
|
|
88
|
+
remote: string
|
|
89
|
+
): Action => {
|
|
90
|
+
if (typeof policy === 'function') {
|
|
91
|
+
let value: unknown;
|
|
92
|
+
try {
|
|
93
|
+
value = policy(local, remote);
|
|
94
|
+
} catch (cause) {
|
|
95
|
+
throw new StoreVersionError('POLICY_FAILED', 'policy function threw', {
|
|
96
|
+
cause,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return toAction(value);
|
|
100
|
+
}
|
|
101
|
+
if (typeof policy === 'string') {
|
|
102
|
+
return toAction(policy);
|
|
103
|
+
}
|
|
104
|
+
if (typeof policy === 'object' && policy !== null) {
|
|
105
|
+
return toAction(policy[diff] ?? DEFAULT_POLICY[diff]);
|
|
106
|
+
}
|
|
107
|
+
return toAction(policy);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 最低バージョンが比較できない値なら、その `MINIMUM_VERSION_FAILED`(cause に compare-versions のエラー)を返す。
|
|
112
|
+
* 有効な値、または空文字 / null / undefined(指定なし)なら undefined。
|
|
113
|
+
* {@link evaluatePolicy} の `minimumError` と同じもの。判定が `INVALID_VERSION` で止まるときにも、
|
|
114
|
+
* 最低バージョンの不正を `onError` に渡せるようにするために分けてある
|
|
115
|
+
*/
|
|
116
|
+
export const minimumVersionError = (
|
|
117
|
+
minimum: string | null | undefined
|
|
118
|
+
): StoreVersionError | undefined => {
|
|
119
|
+
if (minimum == null || minimum === '') {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
assertValidVersion(minimum, 'minimum');
|
|
124
|
+
return undefined;
|
|
125
|
+
} catch (e) {
|
|
126
|
+
return new StoreVersionError(
|
|
127
|
+
'MINIMUM_VERSION_FAILED',
|
|
128
|
+
(e as Error).message,
|
|
129
|
+
{ cause: (e as StoreVersionError).cause }
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const POLICY_KEYS: readonly DiffType[] = ['major', 'minor', 'patch'];
|
|
135
|
+
|
|
136
|
+
const invalidPolicyValue = (value: unknown, key?: DiffType) =>
|
|
137
|
+
new StoreVersionError(
|
|
138
|
+
'POLICY_FAILED',
|
|
139
|
+
`policy${key ? `.${key}` : ''} is an invalid action: ${typeof value === 'string' ? JSON.stringify(value) : `(${typeof value})`}`
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 値で指定した policy(文字列・オブジェクト)を、差分が出る前に検証する。不正なら `POLICY_FAILED` を投げる。
|
|
144
|
+
* `evaluatePolicy` は差分が出たときにだけ policy を引くので、`local >= remote` の間は不正な値に気づけないため。
|
|
145
|
+
*
|
|
146
|
+
* - null / undefined(デフォルト)と関数は検証しない(関数の戻り値は判定のときに確かめる)
|
|
147
|
+
* - 文字列は `'force' | 'warning' | 'none'` のどれか
|
|
148
|
+
* - オブジェクトは `major` / `minor` / `patch` のうち、null / undefined でない値(省略はデフォルト)がどれも 3 値のどれか
|
|
149
|
+
* - それ以外の型(数値・真偽値など)は不正
|
|
150
|
+
*/
|
|
151
|
+
export const validatePolicy = (policy: unknown): void => {
|
|
152
|
+
if (policy == null || typeof policy === 'function') {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (typeof policy === 'object') {
|
|
156
|
+
for (const key of POLICY_KEYS) {
|
|
157
|
+
const value = (policy as Partial<Record<DiffType, unknown>>)[key];
|
|
158
|
+
if (value != null && !ACTIONS.includes(value as string)) {
|
|
159
|
+
throw invalidPolicyValue(value, key);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (!ACTIONS.includes(policy as string)) {
|
|
165
|
+
throw invalidPolicyValue(policy);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* ポリシー判定(純粋関数)。順序: 1. `minimum` があり `local < minimum` なら `force`、
|
|
171
|
+
* 2. `local >= remote` なら `none`、3. 差分の種類で policy を引く。
|
|
172
|
+
*
|
|
173
|
+
* - local が比較できない値なら `INVALID_VERSION` を投げる(minimum があっても)
|
|
174
|
+
* - minimum が比較できない値なら、minimum を飛ばして判定を続け、結果に `minimumError` を添える
|
|
175
|
+
* - remote が比較できない値で、minimum が有効かつ `local < minimum` なら、
|
|
176
|
+
* `{ result: null, detail: null, diff: null, action: 'force', remoteError }` を返す。それ以外は `INVALID_VERSION` を投げる
|
|
177
|
+
* - policy の関数が throw した / 不正な値を返したら `POLICY_FAILED` を投げる
|
|
178
|
+
*/
|
|
179
|
+
export const evaluatePolicy = ({
|
|
180
|
+
local,
|
|
181
|
+
remote,
|
|
182
|
+
minimum,
|
|
183
|
+
policy,
|
|
184
|
+
}: EvaluatePolicyParams): PolicyResult => {
|
|
185
|
+
assertValidVersion(local, 'local');
|
|
186
|
+
|
|
187
|
+
const minimumError = minimumVersionError(minimum);
|
|
188
|
+
// local は上で検証済みなので、minimum が有効なら比較は投げない
|
|
189
|
+
const belowMinimum =
|
|
190
|
+
minimum != null &&
|
|
191
|
+
minimum !== '' &&
|
|
192
|
+
minimumError === undefined &&
|
|
193
|
+
compareVersionOrThrow(local, minimum, ['local', 'minimum']) === 'new';
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
assertValidVersion(remote, 'remote');
|
|
197
|
+
} catch (e) {
|
|
198
|
+
if (belowMinimum) {
|
|
199
|
+
return {
|
|
200
|
+
result: null,
|
|
201
|
+
detail: null,
|
|
202
|
+
action: 'force',
|
|
203
|
+
diff: null,
|
|
204
|
+
remoteError: e as StoreVersionError,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
throw e;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const result = compareVersionOrThrow(local, remote);
|
|
211
|
+
const detail = toDetail(result);
|
|
212
|
+
const diff = result === 'new' ? diffType(local, remote) : null;
|
|
213
|
+
const extra = minimumError ? { minimumError } : {};
|
|
214
|
+
|
|
215
|
+
if (belowMinimum) {
|
|
216
|
+
return { result, detail, action: 'force', diff, ...extra };
|
|
217
|
+
}
|
|
218
|
+
if (diff === null) {
|
|
219
|
+
return { result, detail, action: 'none', diff, ...extra };
|
|
220
|
+
}
|
|
221
|
+
const action = resolveAction(policy ?? DEFAULT_POLICY, diff, local, remote);
|
|
222
|
+
return { result, detail, action, diff, ...extra };
|
|
223
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Force(強制アップデート)のデフォルト UI の中身。Force の元のデザインの実測値で作る。
|
|
2
|
+
// 閉じるボタンは無い。Modal の枠(Android の戻るボタンで閉じない、幕、配置、アニメーション)は StoreVersionModal.tsx が持ち、
|
|
3
|
+
// ここは `components.Force` が無いときにその中に描かれるカード
|
|
4
|
+
import { Image, Platform, StyleSheet, Text, View } from 'react-native';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
ActionButton,
|
|
8
|
+
BUTTON_TEXT_COLOR,
|
|
9
|
+
MAX_CONTENT_WIDTH,
|
|
10
|
+
messageStyle,
|
|
11
|
+
titleStyle,
|
|
12
|
+
usePalette,
|
|
13
|
+
} from './defaultUi.tsx';
|
|
14
|
+
import type { StoreVersionForceProps } from './useStoreVersion.ts';
|
|
15
|
+
|
|
16
|
+
/** デザインの幕 `rgba(0, 0, 0, 0.55)` */
|
|
17
|
+
export const FORCE_OVERLAY_COLOR = 'rgba(0, 0, 0, 0.55)';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* カードの影(デザイン: `box-shadow: 0 12px 40px rgba(0, 0, 0, .3)`)。iOS は shadow* にそのまま写し、
|
|
21
|
+
* Android は elevation に読み替える(影の y オフセット 12 を elevation に)。Platform.select はモジュールの読み込み時に評価される
|
|
22
|
+
*/
|
|
23
|
+
export const cardShadow = Platform.select({
|
|
24
|
+
ios: {
|
|
25
|
+
shadowColor: '#000000',
|
|
26
|
+
shadowOffset: { width: 0, height: 12 },
|
|
27
|
+
shadowOpacity: 0.3,
|
|
28
|
+
shadowRadius: 40,
|
|
29
|
+
},
|
|
30
|
+
android: { elevation: 12 },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/** デザインどおりのカード(`components.Force` が無いときの中身)。`texts` は置き換え済み */
|
|
34
|
+
export const ForceCard = ({
|
|
35
|
+
texts,
|
|
36
|
+
icon,
|
|
37
|
+
openStore,
|
|
38
|
+
}: StoreVersionForceProps) => {
|
|
39
|
+
const palette = usePalette();
|
|
40
|
+
return (
|
|
41
|
+
<View style={[styles.card, { backgroundColor: palette.card }]}>
|
|
42
|
+
{icon === undefined ? null : (
|
|
43
|
+
<Image
|
|
44
|
+
source={icon}
|
|
45
|
+
style={[styles.icon, { backgroundColor: palette.iconBg }]}
|
|
46
|
+
/>
|
|
47
|
+
)}
|
|
48
|
+
<Text
|
|
49
|
+
accessibilityRole="header"
|
|
50
|
+
style={[titleStyle, styles.centered, { color: palette.text }]}
|
|
51
|
+
>
|
|
52
|
+
{texts.title}
|
|
53
|
+
</Text>
|
|
54
|
+
<Text style={[messageStyle, styles.centered, { color: palette.sub }]}>
|
|
55
|
+
{texts.message}
|
|
56
|
+
</Text>
|
|
57
|
+
<View style={styles.buttonRow}>
|
|
58
|
+
<ActionButton
|
|
59
|
+
label={texts.update}
|
|
60
|
+
onPress={() => {
|
|
61
|
+
// 開けたかどうかは使わない(失敗は onError に渡る。reject しない)
|
|
62
|
+
void openStore();
|
|
63
|
+
}}
|
|
64
|
+
backgroundColor={palette.primary}
|
|
65
|
+
color={BUTTON_TEXT_COLOR}
|
|
66
|
+
/>
|
|
67
|
+
</View>
|
|
68
|
+
</View>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const styles = StyleSheet.create({
|
|
73
|
+
// デザイン: left/right 24(枠側の padding で取る)・radius 20・padding 28px 24px 24px・gap 14。
|
|
74
|
+
// 幅は枠いっぱい(スマホ縦向きではデザインと同じ)で、最大 480 に抑えて中央に置く(意図的に変える点)
|
|
75
|
+
card: {
|
|
76
|
+
width: '100%',
|
|
77
|
+
maxWidth: MAX_CONTENT_WIDTH,
|
|
78
|
+
alignSelf: 'center',
|
|
79
|
+
borderRadius: 20,
|
|
80
|
+
paddingTop: 28,
|
|
81
|
+
paddingHorizontal: 24,
|
|
82
|
+
paddingBottom: 24,
|
|
83
|
+
gap: 14,
|
|
84
|
+
...cardShadow,
|
|
85
|
+
},
|
|
86
|
+
// デザイン: 64×64・radius 15・align-self center
|
|
87
|
+
icon: {
|
|
88
|
+
width: 64,
|
|
89
|
+
height: 64,
|
|
90
|
+
borderRadius: 15,
|
|
91
|
+
alignSelf: 'center',
|
|
92
|
+
},
|
|
93
|
+
centered: {
|
|
94
|
+
textAlign: 'center',
|
|
95
|
+
},
|
|
96
|
+
// デザイン: ボタンの前の margin-top 6
|
|
97
|
+
buttonRow: {
|
|
98
|
+
marginTop: 6,
|
|
99
|
+
},
|
|
100
|
+
});
|