itmar-block-packages 1.8.0 → 1.10.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.
@@ -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
+ }
@@ -68,8 +68,9 @@ const ChoiceControl = (props) => {
68
68
  const fetchData = async () => {
69
69
  try {
70
70
  const fetchChoices = await fetchFunction(selectedSlug);
71
-
72
71
  setChoices(fetchChoices);
72
+ //指定の投稿タイプに含まれないフィールドを削除する
73
+ pruneChoiceItemsByObjectKeys(fetchChoices[0], choiceItems);
73
74
  } catch (error) {
74
75
  console.error("Error fetching data:", error.message);
75
76
  }
@@ -90,22 +91,78 @@ const ChoiceControl = (props) => {
90
91
  }
91
92
  return setItems;
92
93
  };
94
+ /**
95
+ * dataObj のキー一覧を「choiceItems と比較する形」に変換して Set で返す
96
+ * - 通常キー: そのまま
97
+ * - acf / meta: 子キーに `${parent}_` を付けたもの(例: acf_relate_url, meta_footnotes)
98
+ */
99
+ function buildComparableKeySet(dataObj) {
100
+ const keySet = new Set();
101
+
102
+ if (!dataObj || typeof dataObj !== "object") return keySet;
103
+
104
+ for (const [key, val] of Object.entries(dataObj)) {
105
+ if (
106
+ (key === "acf" || key === "meta") &&
107
+ val &&
108
+ typeof val === "object" &&
109
+ !Array.isArray(val)
110
+ ) {
111
+ for (const childKey of Object.keys(val)) {
112
+ keySet.add(`${key}_${childKey}`);
113
+ }
114
+ continue;
115
+ }
116
+
117
+ keySet.add(key);
118
+ }
119
+
120
+ return keySet;
121
+ }
122
+
123
+ /**
124
+ * choiceItems を dataObj のキーに合わせて削除する
125
+ * - choiceItems が string 配列でも、{value: "..."} の配列でも動くようにしてあります
126
+ */
127
+ function pruneChoiceItemsByObjectKeys(dataObj, choiceItems) {
128
+ const validKeys = buildComparableKeySet(dataObj);
129
+
130
+ const getItemKey = (item) => {
131
+ if (typeof item === "string") return item;
132
+ if (item && typeof item === "object")
133
+ return item.value ?? item.key ?? item.name ?? "";
134
+ return "";
135
+ };
136
+
137
+ const next = (choiceItems ?? []).filter((item) =>
138
+ validKeys.has(getItemKey(item))
139
+ );
140
+
141
+ // ★ 配列の参照はそのまま、中身だけ置き換える
142
+ choiceItems.splice(0, choiceItems.length, ...next);
143
+
144
+ return choiceItems; // 必要なら返す
145
+ }
93
146
 
94
147
  //階層化されたカスタムフィールドのフィールド名を表示する関数
95
148
  let groupLabel = "";
96
- const dispCustumFields = (obj, prefix = "", onChange) => {
149
+ const dispCustumFields = (obj, prefix = "", isImage = false, onChange) => {
97
150
  return Object.entries(obj).map(([key, value]) => {
98
151
  const fieldName = prefix ? `${prefix}.${key}` : key; //prefixはグループ名
99
152
 
100
153
  const fieldLabel = key.replace(/^(meta_|acf_)/, "");
101
-
102
- if (typeof value === "object" && value !== null) {
154
+ //オブジェクトであって配列でないものがグループと考える
155
+ if (
156
+ typeof value === "object" &&
157
+ !Array.isArray(value) &&
158
+ value !== null
159
+ ) {
103
160
  groupLabel = `${fieldLabel}.`;
104
161
  return (
105
162
  <div className="group_area">
106
163
  <div className="group_label">{fieldLabel}</div>
107
164
  <div key={fieldName} className="field_group">
108
- {dispCustumFields(value, fieldName, onChange)}
165
+ {dispCustumFields(value, fieldName, isImage, onChange)}
109
166
  </div>
110
167
  </div>
111
168
  );
@@ -117,6 +174,7 @@ const ChoiceControl = (props) => {
117
174
  { value: "itmar/design-title", label: "itmar/design-title" },
118
175
  { value: "core/paragraph", label: "core/paragraph" },
119
176
  { value: "core/image", label: "core/image" },
177
+ { value: "itmar/slide-mv", label: "itmar/slide-mv" },
120
178
  ];
121
179
  return (
122
180
  <div className="itmar_custom_field_set">
@@ -136,20 +194,23 @@ const ChoiceControl = (props) => {
136
194
  props.onChange(newChoiceFields);
137
195
  }}
138
196
  />
139
- <ComboboxControl
140
- options={options}
141
- value={
142
- blockMap[`${prefix ? groupLabel : ""}${fieldLabel}`] ||
143
- "itmar/design-title"
144
- }
145
- onChange={(newValue) => {
146
- const fieldKey = prefix
147
- ? `${groupLabel}${fieldLabel}`
148
- : `${fieldLabel}`;
149
- const newBlockMap = { ...blockMap, [fieldKey]: newValue };
150
- props.onBlockMapChange(newBlockMap);
151
- }}
152
- />
197
+ {!isImage && (
198
+ <ComboboxControl
199
+ options={options}
200
+ value={
201
+ //blockMap[`${prefix ? groupLabel : ""}${key}`] || "itmar/design-title"
202
+ blockMap[`${prefix ? prefix + "." : ""}${key}`] ||
203
+ "itmar/design-title"
204
+ }
205
+ onChange={(newValue) => {
206
+ //const fieldKey = prefix ? `${groupLabel}${key}` : `${key}`;
207
+ const fieldKey = prefix ? `${prefix}.${key}` : `${key}`;
208
+
209
+ const newBlockMap = { ...blockMap, [fieldKey]: newValue };
210
+ props.onBlockMapChange(newBlockMap);
211
+ }}
212
+ />
213
+ )}
153
214
  </div>
154
215
  );
155
216
  }
@@ -240,6 +301,23 @@ const ChoiceControl = (props) => {
240
301
  }}
241
302
  />
242
303
  )}
304
+ {choice.content && (
305
+ <ToggleControl
306
+ className="field_choice"
307
+ label={__("Content", "block-collections")}
308
+ checked={choiceItems.some(
309
+ (choiceField) => choiceField === "content"
310
+ )}
311
+ onChange={(checked) => {
312
+ const newChoiceFields = handleChoiceChange(
313
+ checked,
314
+ "content",
315
+ choiceItems
316
+ );
317
+ props.onChange(newChoiceFields);
318
+ }}
319
+ />
320
+ )}
243
321
  {choice.date && (
244
322
  <ToggleControl
245
323
  className="field_choice"
@@ -343,6 +421,7 @@ const ChoiceControl = (props) => {
343
421
  </div>
344
422
  <div className="custom_field_area">
345
423
  {dispCustumFields({
424
+ // meta はそのまま
346
425
  ...Object.entries(choice.meta).reduce(
347
426
  (acc, [key, value]) => ({
348
427
  ...acc,
@@ -350,13 +429,16 @@ const ChoiceControl = (props) => {
350
429
  }),
351
430
  {}
352
431
  ),
353
- ...Object.entries(choice.acf).reduce(
354
- (acc, [key, value]) => ({
355
- ...acc,
356
- [`acf_${key}`]: value,
357
- }),
358
- {}
359
- ),
432
+ // acf は「同名で _source があるもののベース側を除く」
433
+ ...Object.entries(choice.acf)
434
+ .filter(([key]) => !key.endsWith("_source"))
435
+ .reduce(
436
+ (acc, [key, value]) => ({
437
+ ...acc,
438
+ [`acf_${key}`]: value,
439
+ }),
440
+ {}
441
+ ),
360
442
  })}
361
443
  </div>
362
444
  </>
@@ -364,6 +446,93 @@ const ChoiceControl = (props) => {
364
446
  </div>
365
447
  );
366
448
  })}
449
+ {type === "imgField" &&
450
+ choices.map((choice, index) => {
451
+ //metaの対象カスタムフィールドが含まれるかのフラグ
452
+ const metaFlg =
453
+ choice.meta &&
454
+ !Object.keys(choice.meta).every(
455
+ (key) => key === "_acf_changed" || key === "footnotes"
456
+ );
457
+ //acfの対象カスタムフィールドが含まれるかのフラグ
458
+ const acfFlg =
459
+ choice.acf &&
460
+ typeof choice.acf === "object" &&
461
+ !Array.isArray(choice.acf);
462
+
463
+ return (
464
+ <div key={index} className="field_section">
465
+ {choice.content && (
466
+ <ToggleControl
467
+ className="field_choice"
468
+ label={__("Content", "block-collections")}
469
+ checked={choiceItems.some(
470
+ (choiceField) => choiceField === "content"
471
+ )}
472
+ onChange={(checked) => {
473
+ const newChoiceFields = handleChoiceChange(
474
+ checked,
475
+ "content",
476
+ choiceItems
477
+ );
478
+ props.onChange(newChoiceFields);
479
+ }}
480
+ />
481
+ )}
482
+ {(choice.featured_media || choice.featured_media === 0) && (
483
+ <ToggleControl
484
+ className="field_choice"
485
+ label={__("Featured Image", "block-collections")}
486
+ checked={choiceItems.some(
487
+ (choiceField) => choiceField === "featured_media"
488
+ )}
489
+ onChange={(checked) => {
490
+ const newChoiceFields = handleChoiceChange(
491
+ checked,
492
+ "featured_media",
493
+ choiceItems
494
+ );
495
+ props.onChange(newChoiceFields);
496
+ }}
497
+ />
498
+ )}
499
+
500
+ {(metaFlg || acfFlg) && (
501
+ <>
502
+ <div className="custom_field_label">
503
+ {__("Custom Field", "block-collections")}
504
+ </div>
505
+ <div className="custom_field_area">
506
+ {dispCustumFields(
507
+ {
508
+ // meta はそのまま
509
+ ...Object.entries(choice.meta).reduce(
510
+ (acc, [key, value]) => ({
511
+ ...acc,
512
+ [`meta_${key}`]: value,
513
+ }),
514
+ {}
515
+ ),
516
+ // acf は「同名で _source があるもののベース側を除く」
517
+ ...Object.entries(choice.acf)
518
+ .filter(([key]) => !key.endsWith("_source"))
519
+ .reduce(
520
+ (acc, [key, value]) => ({
521
+ ...acc,
522
+ [`acf_${key}`]: value,
523
+ }),
524
+ {}
525
+ ),
526
+ },
527
+ "",
528
+ true
529
+ )}
530
+ </div>
531
+ </>
532
+ )}
533
+ </div>
534
+ );
535
+ })}
367
536
  </div>
368
537
  );
369
538
  };
@@ -483,6 +652,7 @@ export const restFieldes = async (rest_base) => {
483
652
  //投稿データに以下のフィールドが含まれているかを調べる
484
653
  const selectedFields = [
485
654
  "title",
655
+ "content",
486
656
  "date",
487
657
  "excerpt",
488
658
  "featured_media",
@@ -495,6 +665,7 @@ export const restFieldes = async (rest_base) => {
495
665
  const response = await apiFetch({
496
666
  path: `/wp/v2/${rest_base}?_fields=${fieldsParam}&per_page=1&order=desc`,
497
667
  });
668
+
498
669
  return response;
499
670
  };
500
671