itmar-block-packages 1.7.1 → 1.9.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "itmar-block-packages",
3
- "version": "1.7.1",
3
+ "version": "1.9.0",
4
4
  "description": "We have put together a package of common React components used for WordPress custom blocks.",
5
5
  "main": "build/index.js",
6
6
  "scripts": {
@@ -0,0 +1,179 @@
1
+ import { __ } from "@wordpress/i18n";
2
+ import {
3
+ PanelBody,
4
+ PanelRow,
5
+ RangeControl,
6
+ TextControl,
7
+ SelectControl,
8
+ } from "@wordpress/components";
9
+ import { format, getSettings } from "@wordpress/date";
10
+
11
+ //日付のフォーマット
12
+ const dateFormats = [
13
+ { label: "YYYY-MM-DD HH:mm:ss", value: "Y-m-d H:i:s" },
14
+ { label: "MM/DD/YYYY", value: "m/d/Y" },
15
+ { label: "DD/MM/YYYY", value: "d/m/Y" },
16
+ { label: "MMMM D, YYYY", value: "F j, Y" },
17
+ { label: "HH:mm:ss", value: "H:i:s" },
18
+ { label: "YYYY.M.D", value: "Y.n.j" },
19
+ { label: "Day, MMMM D, YYYY", value: "l, F j, Y" },
20
+ { label: "ddd, MMM D, YYYY", value: "D, M j, Y" },
21
+ { label: "YYYY年M月D日 (曜日)", value: "Y年n月j日 (l)" },
22
+ ];
23
+ //プレーンのフォーマット
24
+ const plaineFormats = [
25
+ {
26
+ key: "str_free",
27
+ label: __("Free String", "block-collections"),
28
+ value: "%s",
29
+ },
30
+ {
31
+ key: "num_comma",
32
+ label: __("Numbers (comma separated)", "block-collections"),
33
+ value: {
34
+ style: "decimal",
35
+ useGrouping: true, // カンマ区切り
36
+ },
37
+ },
38
+ {
39
+ key: "num_no_comma",
40
+ label: __("Numbers (no commas)", "block-collections"),
41
+ value: {
42
+ style: "decimal",
43
+ useGrouping: false,
44
+ },
45
+ },
46
+ {
47
+ key: "num_amount",
48
+ label: __("Amount", "block-collections"),
49
+ value: {
50
+ style: "currency",
51
+ currency: "JPY",
52
+ },
53
+ },
54
+ ];
55
+
56
+ export const FormatSelectControl = ({
57
+ titleType,
58
+ userFormat,
59
+ freeStrFormat,
60
+ decimal,
61
+ onFormatChange,
62
+ }) => {
63
+ const isPlaine = titleType === "plaine";
64
+ const isDate = titleType === "date";
65
+ const isUser = titleType === "user";
66
+
67
+ //SelectControlのオプションを生成
68
+ const options = isDate
69
+ ? dateFormats
70
+ : plaineFormats.map((f) => ({ label: f.label, value: f.key }));
71
+
72
+ return (
73
+ <PanelBody title={__("Display Format Setting", "block-collections")}>
74
+ {(isPlaine || isDate) && (
75
+ <>
76
+ <SelectControl
77
+ label={__("Select Format", "block-collections")}
78
+ value={userFormat}
79
+ options={options}
80
+ onChange={(newFormat) =>
81
+ onFormatChange({
82
+ userFormat: newFormat,
83
+ freeStrFormat,
84
+ decimal,
85
+ })
86
+ }
87
+ />
88
+
89
+ {userFormat?.startsWith("str_") && (
90
+ <TextControl
91
+ label={__("String Format", "block-collections")}
92
+ value={freeStrFormat}
93
+ onChange={(newFormat) =>
94
+ onFormatChange({
95
+ userFormat,
96
+ freeStrFormat: newFormat,
97
+ decimal,
98
+ })
99
+ }
100
+ />
101
+ )}
102
+ {userFormat?.startsWith("num_") && (
103
+ <PanelRow className="itmar_post_blocks_pannel">
104
+ <RangeControl
105
+ value={decimal}
106
+ label={__("Decimal Num", "query-blocks")}
107
+ max={5}
108
+ min={0}
109
+ onChange={(val) =>
110
+ onFormatChange({
111
+ userFormat,
112
+ freeStrFormat,
113
+ decimal: val,
114
+ })
115
+ }
116
+ />
117
+ </PanelRow>
118
+ )}
119
+ </>
120
+ )}
121
+
122
+ {isUser && (
123
+ <TextControl
124
+ label={__("User Format", "block-collections")}
125
+ value={freeStrFormat}
126
+ onChange={(newFormat) =>
127
+ onFormatChange({
128
+ userFormat: "str_free",
129
+ freeStrFormat: newFormat,
130
+ decimal,
131
+ })
132
+ }
133
+ />
134
+ )}
135
+ </PanelBody>
136
+ );
137
+ };
138
+
139
+ export const displayFormated = (
140
+ content,
141
+ userFormat,
142
+ freeStrFormat,
143
+ decimal
144
+ ) => {
145
+ // 内部で使用するロケール
146
+ const locale = getSettings().l10n?.locale || "en";
147
+
148
+ //日付にフォーマットがあれば、それで書式設定してリターン
149
+ const isDateFormat = dateFormats.find((f) => f.value === userFormat);
150
+ if (isDateFormat) {
151
+ const ret_val = format(userFormat, content, getSettings());
152
+ return ret_val;
153
+ }
154
+ //数値や文字列のフォーマット
155
+ const selectedFormat = plaineFormats.find((f) => f.key === userFormat)?.value;
156
+ if (typeof selectedFormat === "object") {
157
+ // Intl.NumberFormat オプション
158
+ try {
159
+ const numeric = parseFloat(content);
160
+ // `selectedFormat` を元に新しいフォーマット設定を生成(mutateしない)
161
+ const options = { ...selectedFormat };
162
+
163
+ if (typeof decimal === "number" && decimal > 0) {
164
+ options.minimumFractionDigits = decimal;
165
+ options.maximumFractionDigits = decimal;
166
+ }
167
+
168
+ const formatter = new Intl.NumberFormat(locale, options);
169
+ return formatter.format(numeric);
170
+ } catch (e) {
171
+ console.warn("Number format failed:", e);
172
+ return content;
173
+ }
174
+ } else if (typeof selectedFormat === "string") {
175
+ return freeStrFormat.replace("%s", content);
176
+ }
177
+ //フォーマットが見つからないときはそのまま返す
178
+ return content;
179
+ };
package/src/index.js CHANGED
@@ -110,3 +110,13 @@ export { default as UpdateAllPostsBlockAttributes } from "./UpdateAllPostsBlockA
110
110
 
111
111
  //住所変換関連の関数
112
112
  export { fetchZipToAddress } from "./ZipAddress";
113
+
114
+ //書式設定用のコンポーネント
115
+ export { FormatSelectControl, displayFormated } from "./formatCreate";
116
+
117
+ //ShopifyAPI関連のコンポーネント
118
+ export {
119
+ checkCustomerLoginState,
120
+ redirectCustomerAuthorize,
121
+ sendRegistrationRequest,
122
+ } from "./shopfiApi";
@@ -0,0 +1,187 @@
1
+ function generateState() {
2
+ const timestamp = Date.now().toString();
3
+ const randomString = Math.random().toString(36).substring(2);
4
+ return timestamp + randomString;
5
+ }
6
+
7
+ function generateNonce(length = 32) {
8
+ const characters =
9
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
10
+ let nonce = "";
11
+ for (let i = 0; i < length; i++) {
12
+ nonce += characters.charAt(Math.floor(Math.random() * characters.length));
13
+ }
14
+ return nonce;
15
+ }
16
+
17
+ function generateCodeVerifier(length = 128) {
18
+ const charset =
19
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
20
+ let result = "";
21
+ for (let i = 0; i < length; i++) {
22
+ result += charset.charAt(Math.floor(Math.random() * charset.length));
23
+ }
24
+ return result;
25
+ }
26
+
27
+ async function generateCodeChallenge(codeVerifier) {
28
+ const encoder = new TextEncoder();
29
+ const data = encoder.encode(codeVerifier);
30
+ const digest = await crypto.subtle.digest("SHA-256", data);
31
+ return btoa(String.fromCharCode(...new Uint8Array(digest)))
32
+ .replace(/\+/g, "-")
33
+ .replace(/\//g, "_")
34
+ .replace(/=+$/, "");
35
+ }
36
+
37
+ // ✅ Shopifyへの認証リダイレクト
38
+ export async function redirectCustomerAuthorize(
39
+ shopId,
40
+ clientId,
41
+ userMail,
42
+ callbackUri,
43
+ redirectUri
44
+ ) {
45
+ //呼び出し元の戻り先
46
+ const statePayload = {
47
+ ts: Date.now(),
48
+ random: Math.random().toString(36).substring(2),
49
+ return_url: redirectUri,
50
+ };
51
+ //stateで渡しておく
52
+ const state = btoa(JSON.stringify(statePayload));
53
+ const nonce = generateNonce();
54
+ const codeVerifier = generateCodeVerifier();
55
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
56
+
57
+ localStorage.setItem("shopify_code_verifier", codeVerifier);
58
+ localStorage.setItem("shopify_state", state);
59
+ localStorage.setItem("shopify_nonce", nonce);
60
+ localStorage.setItem("shopify_client_id", clientId);
61
+ localStorage.setItem("shopify_user_mail", userMail);
62
+ localStorage.setItem("shopify_shop_id", shopId);
63
+ localStorage.setItem("shopify_redirect_uri", callbackUri); //ログアウトの処理で使用する
64
+
65
+ const url = new URL(
66
+ `https://shopify.com/authentication/${shopId}/oauth/authorize`
67
+ );
68
+ url.searchParams.append("scope", "openid email customer-account-api:full");
69
+ url.searchParams.append("client_id", clientId);
70
+ url.searchParams.append("response_type", "code");
71
+ url.searchParams.append("redirect_uri", callbackUri);
72
+ url.searchParams.append("state", state);
73
+ url.searchParams.append("nonce", nonce);
74
+ url.searchParams.append("code_challenge", codeChallenge);
75
+ url.searchParams.append("code_challenge_method", "S256");
76
+
77
+ window.location.href = url.toString();
78
+ }
79
+
80
+ // ✅ トークンがあれば検証(Storefront API 経由で customer を取得)
81
+ export async function checkCustomerLoginState() {
82
+ const token = localStorage.getItem("shopify_customer_token");
83
+
84
+ if (!token) return false;
85
+
86
+ const checkUrl = "/wp-json/itmar-ec-relate/v1/shopify-login-check";
87
+ const postData = {
88
+ nonce: itmar_option.nonce,
89
+ token: JSON.stringify({ token }),
90
+ };
91
+ sendRegistrationAjax(checkUrl, postData, true)
92
+ .done(function (res) {
93
+ console.log(res);
94
+ })
95
+ .fail(function (xhr, status, error) {
96
+ alert("サーバーエラー: " + error);
97
+ console.error(xhr.responseText);
98
+ });
99
+ }
100
+
101
+ /**
102
+ * @param {string} urlOrPath - RESTは '/itmar-shopify/v1/...' でも '/wp-json/...' でもOK。admin-ajaxは '/wp-admin/admin-ajax.php'
103
+ * @param {object} data - 送信するデータ。nonce は data._wpnonce か data.nonce に入れておけばOK
104
+ * @param {'auto'|'rest'|'ajax'} mode - 既定は 'auto'
105
+ */
106
+ export async function sendRegistrationRequest(
107
+ urlOrPath,
108
+ data = {},
109
+ mode = "auto"
110
+ ) {
111
+ const isRestUrlLike = (u) =>
112
+ u.startsWith("/wp-json") || !u.startsWith("/wp-admin");
113
+
114
+ // 送信先の確定(RESTのときは /wp-json 省略パスでもOKにする)
115
+ let isRest;
116
+ if (mode === "auto") {
117
+ isRest = isRestUrlLike(urlOrPath);
118
+ } else {
119
+ isRest = mode === "rest";
120
+ }
121
+
122
+ let url = urlOrPath;
123
+
124
+ if (isRest) {
125
+ // '/itmar-shopify/v1/...' のようなパスだけ渡されたら /wp-json を補う
126
+ if (!url.startsWith("/wp-json")) {
127
+ const root =
128
+ (window.wpApiSettings && window.wpApiSettings.root) || "/wp-json/";
129
+ url = root.replace(/\/+$/, "/") + url.replace(/^\/+/, "");
130
+ }
131
+ } else {
132
+ // admin-ajax の既定URL
133
+ if (!url) url = "/wp-admin/admin-ajax.php";
134
+ }
135
+
136
+ const fetchOptions = {
137
+ method: "POST",
138
+ credentials: "same-origin", // 同一オリジン Cookie
139
+ headers: {},
140
+ };
141
+
142
+ // ノンス抽出(_wpnonce を優先)
143
+ const nonce =
144
+ data._wpnonce ||
145
+ data.nonce ||
146
+ (window.wpApiSettings && window.wpApiSettings.nonce);
147
+
148
+ if (isRest) {
149
+ fetchOptions.headers["Content-Type"] = "application/json";
150
+ if (nonce) fetchOptions.headers["X-WP-Nonce"] = nonce;
151
+
152
+ // nonce系は本文から除く(任意)
153
+ const { _wpnonce, nonce: _legacyNonce, ...rest } = data;
154
+ fetchOptions.body = JSON.stringify(rest);
155
+ } else {
156
+ // admin-ajax は URLSearchParams で送る
157
+ const form = new URLSearchParams();
158
+ Object.entries(data).forEach(([k, v]) => {
159
+ if (v !== undefined && v !== null) form.append(k, String(v));
160
+ });
161
+ // ノンスは _wpnonce に統一
162
+ if (nonce && !form.has("_wpnonce")) form.append("_wpnonce", nonce);
163
+ fetchOptions.body = form;
164
+ }
165
+
166
+ const res = await fetch(url, fetchOptions);
167
+
168
+ // WP REST はメソッド不一致・ルートなし等でも JSON 返さないことがあるので分岐
169
+ const contentType = res.headers.get("content-type") || "";
170
+ const tryJson = contentType.includes("application/json");
171
+
172
+ if (!res.ok) {
173
+ let msg = `HTTP ${res.status}`;
174
+ if (tryJson) {
175
+ try {
176
+ const j = await res.json();
177
+ msg += j.message ? `: ${j.message}` : "";
178
+ } catch {}
179
+ } else {
180
+ const t = await res.text();
181
+ if (t) msg += `: ${t.slice(0, 200)}`;
182
+ }
183
+ throw new Error(msg);
184
+ }
185
+
186
+ return tryJson ? res.json() : res.text();
187
+ }