module = $module; } public function register(): void { add_action('dokan_product_edit_after_inventory_variants', [$this, 'render'], 50, 2); add_action('dokan_process_product_meta', [$this, 'save'], 20, 1); } /** * @param mixed $post * @param mixed $post_id */ public function render($post = null, $post_id = 0): void { $post_id = (int) $post_id; if ($post_id <= 0 || get_post_type($post_id) !== 'product' || !$this->module->policy()->currentUserMayAuthor()) { return; } try { $state = $this->module->model()->prepare($post_id, get_post_status($post_id) === 'auto-draft'); } catch (\Throwable $e) { Dokan::debugLog('classic render failed for product ' . $post_id . ': ' . $e->getMessage()); return; } $allowed = (array) $state['allowedFields']; ?>

fileFields() as $field => $spec) { if (in_array($field, $allowed, true)) { $this->renderFileField($field, $spec, (array) $state[$field], $state); } } if (in_array('ar', $allowed, true)) { ?>
renderPosition($state); } if (in_array('background', $allowed, true)) { $this->renderBackground($state); } ?>
module->policy()->currentUserMayAuthor()) { return; } try { $has_position = isset($raw['viewer_position']) && is_string($raw['viewer_position']); $has_bg = isset($raw['bg_color']) && is_string($raw['bg_color']); $shown = $has_position || $has_bg ? $this->module->model()->prepare($post_id, false) : []; // The select always posts; an untouched value means 'keep', as in the React editor. if ($has_position && $raw['viewer_position'] === $shown['position']) { $raw['viewer_position'] = 'keep'; } // Same for the color input, which always posts; the checkbox wins over it. if ($has_bg) { $posted = !empty($raw['bg_transparent']) ? 'transparent' : strtolower($raw['bg_color']); $raw['bg_color'] = $posted === $shown['bg_color'] ? 'keep' : $posted; } $result = $this->module->model()->save($post_id, $raw, $this->module->policy()->currentVendorId()); } catch (\Throwable $e) { Dokan::debugLog('classic save failed for product ' . $post_id . ': ' . $e->getMessage()); return; } if (is_wp_error($result)) { global $woocommerce_errors; if (!is_array($woocommerce_errors)) { $woocommerce_errors = []; } // Dokan's error template appends its own period. $woocommerce_errors[] = rtrim($result->get_error_message(), '.'); } } /** * @return array> */ protected function fileFields(): array { return [ 'model' => [ 'ext' => 'glb', 'kind' => 'model', 'label' => __('3D model (GLB)', '3d-viewer'), 'button' => __('Upload / select GLB', '3d-viewer'), 'help' => __('A .glb file. It is shown in an interactive viewer on the product page.', '3d-viewer'), 'size' => '1', ], 'poster' => [ 'ext' => implode(',', VendorPolicy::POSTER_EXTENSIONS), 'kind' => 'image', 'label' => __('Poster image', '3d-viewer'), 'button' => __('Upload / select image', '3d-viewer'), 'help' => __('Shown while the 3D model loads.', '3d-viewer'), 'size' => '', ], 'usdz' => [ 'ext' => 'usdz', 'kind' => 'model', 'label' => __('USDZ model (iOS AR)', '3d-viewer'), 'button' => __('Upload / select USDZ', '3d-viewer'), 'help' => __('Optional. Used for AR on iPhone and iPad.', '3d-viewer'), 'size' => '1', ], ]; } /** * @param array $spec * @param array $file * @param array $state */ protected function renderFileField(string $field, array $spec, array $file, array $state): void { $url = isset($file['url']) && is_string($file['url']) ? $file['url'] : ''; $name = isset($file['name']) && is_string($file['name']) ? $file['name'] : ''; $poster_url = isset($state['poster']['url']) && is_string($state['poster']['url']) ? $state['poster']['url'] : ''; ?>
> "> ">×

$state */ protected function renderPosition(array $state): void { $current = is_string($state['position']) ? $state['position'] : ''; ?>

$state */ protected function renderBackground(array $state): void { $current = is_string($state['bg_color']) ? $state['bg_color'] : 'transparent'; $transparent = $current === 'transparent'; ?>