diff --git a/frontend/admin/src/stores/settings.js b/frontend/admin/src/stores/settings.js
index 79a58e1..a5afb68 100644
--- a/frontend/admin/src/stores/settings.js
+++ b/frontend/admin/src/stores/settings.js
@@ -20,6 +20,7 @@ export const useSettingsStore = defineStore('settings', {
privacy_policy_link: null,
image_aspect_ratio: '1:1',
image_crop_algorithm: 'cover',
+ haptic_enabled: true,
},
telegram: {
diff --git a/frontend/admin/src/views/GeneralView.vue b/frontend/admin/src/views/GeneralView.vue
index 38b782e..dea2655 100644
--- a/frontend/admin/src/views/GeneralView.vue
+++ b/frontend/admin/src/views/GeneralView.vue
@@ -62,6 +62,10 @@
Resize - изменяет размер изображения с сохранением пропорций (без обрезки)
+
+
+ Включить виброотклик при взаимодействии с элементами интерфейса. Если выключено, тактильная обратная связь не будет использоваться.
+
diff --git a/frontend/spa/src/components/MainPage/Blocks/CategoriesTopBlock.vue b/frontend/spa/src/components/MainPage/Blocks/CategoriesTopBlock.vue
index 03a683a..d9c8529 100644
--- a/frontend/spa/src/components/MainPage/Blocks/CategoriesTopBlock.vue
+++ b/frontend/spa/src/components/MainPage/Blocks/CategoriesTopBlock.vue
@@ -30,8 +30,10 @@
diff --git a/frontend/spa/src/components/ProductsList.vue b/frontend/spa/src/components/ProductsList.vue
index 3b5db85..f5f25ba 100644
--- a/frontend/spa/src/components/ProductsList.vue
+++ b/frontend/spa/src/components/ProductsList.vue
@@ -66,9 +66,10 @@ import IconFunnel from "@/components/Icons/IconFunnel.vue";
import {useProductFiltersStore} from "@/stores/ProductFiltersStore.js";
import {useRouter} from "vue-router";
import PriceTitle from "@/components/ProductItem/PriceTitle.vue";
+import {useHapticFeedback} from "@/composables/useHapticFeedback.js";
const router = useRouter();
-const haptic = window.Telegram.WebApp.HapticFeedback;
+const haptic = useHapticFeedback();
const yaMetrika = useYaMetrikaStore();
const settings = useSettingsStore();
const filtersStore = useProductFiltersStore();
@@ -104,7 +105,7 @@ const props = defineProps({
});
function productClick(product, index) {
- window.Telegram.WebApp.HapticFeedback.selectionChanged();
+ haptic.selectionChanged();
yaMetrika.dataLayerPush({
"ecommerce": {
"currencyCode": settings.currency_code,
diff --git a/frontend/spa/src/components/Quantity.vue b/frontend/spa/src/components/Quantity.vue
index 7350d89..21db9c2 100644
--- a/frontend/spa/src/components/Quantity.vue
+++ b/frontend/spa/src/components/Quantity.vue
@@ -8,6 +8,7 @@
diff --git a/frontend/spa/src/composables/useHapticFeedback.js b/frontend/spa/src/composables/useHapticFeedback.js
index 00c8d92..04c5220 100644
--- a/frontend/spa/src/composables/useHapticFeedback.js
+++ b/frontend/spa/src/composables/useHapticFeedback.js
@@ -1,3 +1,39 @@
+import {useSettingsStore} from "@/stores/SettingsStore.js";
+
+/**
+ * Composable для безопасной работы с HapticFeedback
+ * Проверяет настройку haptic_enabled перед использованием
+ * @returns {object} Объект с методами haptic feedback, которые безопасно вызываются
+ */
export function useHapticFeedback() {
- return window.Telegram?.WebApp?.HapticFeedback;
+ const settings = useSettingsStore();
+ const haptic = window.Telegram?.WebApp?.HapticFeedback;
+
+ // Возвращаем обёртку с методами, которые проверяют настройку при каждом вызове
+ return {
+ impactOccurred: (style) => {
+ // Проверяем настройку при каждом вызове (реактивно)
+ const isHapticEnabled = settings.haptic_enabled !== false;
+ if (!haptic || !isHapticEnabled || !haptic.impactOccurred) {
+ return;
+ }
+ haptic.impactOccurred(style);
+ },
+ selectionChanged: () => {
+ // Проверяем настройку при каждом вызове (реактивно)
+ const isHapticEnabled = settings.haptic_enabled !== false;
+ if (!haptic || !isHapticEnabled || !haptic.selectionChanged) {
+ return;
+ }
+ haptic.selectionChanged();
+ },
+ notificationOccurred: (type) => {
+ // Проверяем настройку при каждом вызове (реактивно)
+ const isHapticEnabled = settings.haptic_enabled !== false;
+ if (!haptic || !isHapticEnabled || !haptic.notificationOccurred) {
+ return;
+ }
+ haptic.notificationOccurred(type);
+ },
+ };
}
diff --git a/frontend/spa/src/composables/useHapticScroll.js b/frontend/spa/src/composables/useHapticScroll.js
index f6003c1..47719c2 100644
--- a/frontend/spa/src/composables/useHapticScroll.js
+++ b/frontend/spa/src/composables/useHapticScroll.js
@@ -1,4 +1,5 @@
import {ref} from 'vue';
+import {useHapticFeedback} from './useHapticFeedback.js';
/**
* Composable для Haptic Feedback по свайпу
@@ -13,6 +14,7 @@ export function useHapticScroll(
feedback = 'soft'
) {
const lastTranslate = ref(0);
+ const haptic = useHapticFeedback();
return function (
swiper
@@ -20,14 +22,11 @@ export function useHapticScroll(
const current = Math.abs(swiper.translate);
const delta = Math.abs(current - lastTranslate.value);
- if (delta > threshold) {
- const haptic = window.Telegram?.WebApp?.HapticFeedback;
- if (haptic) {
- if (type === 'impactOccurred' && haptic.impactOccurred) {
- haptic.impactOccurred(feedback);
- } else if (type === 'selectionChanged' && haptic.selectionChanged) {
- haptic.selectionChanged();
- }
+ if (delta > threshold && haptic) {
+ if (type === 'impactOccurred' && haptic.impactOccurred) {
+ haptic.impactOccurred(feedback);
+ } else if (type === 'selectionChanged' && haptic.selectionChanged) {
+ haptic.selectionChanged();
}
lastTranslate.value = current;
}
diff --git a/frontend/spa/src/stores/CheckoutStore.js b/frontend/spa/src/stores/CheckoutStore.js
index 1649465..ccc9e65 100644
--- a/frontend/spa/src/stores/CheckoutStore.js
+++ b/frontend/spa/src/stores/CheckoutStore.js
@@ -9,6 +9,40 @@ import {usePulseStore} from "@/stores/Pulse.js";
import {TC_PULSE_EVENTS} from "@/constants/tPulseEvents.js";
import {nextTick} from "vue";
+/**
+ * Helper функция для получения haptic feedback (можно использовать в store)
+ * @returns {object} Объект с методами haptic feedback
+ */
+function getHapticFeedback() {
+ const settings = useSettingsStore();
+ const haptic = window.Telegram?.WebApp?.HapticFeedback;
+
+ // Возвращаем обёртку с методами, которые проверяют настройку при каждом вызове
+ return {
+ impactOccurred: (style) => {
+ const isHapticEnabled = settings.haptic_enabled !== false;
+ if (!haptic || !isHapticEnabled || !haptic.impactOccurred) {
+ return;
+ }
+ haptic.impactOccurred(style);
+ },
+ selectionChanged: () => {
+ const isHapticEnabled = settings.haptic_enabled !== false;
+ if (!haptic || !isHapticEnabled || !haptic.selectionChanged) {
+ return;
+ }
+ haptic.selectionChanged();
+ },
+ notificationOccurred: (type) => {
+ const isHapticEnabled = settings.haptic_enabled !== false;
+ if (!haptic || !isHapticEnabled || !haptic.notificationOccurred) {
+ return;
+ }
+ haptic.notificationOccurred(type);
+ },
+ };
+}
+
export const useCheckoutStore = defineStore('checkout', {
state: () => ({
form: {},
@@ -95,7 +129,8 @@ export const useCheckoutStore = defineStore('checkout', {
});
});
- await window.Telegram.WebApp.HapticFeedback.notificationOccurred('success');
+ const haptic = getHapticFeedback();
+ await haptic.notificationOccurred('success');
await useCartStore().getProducts();
} catch (error) {
if (error.response?.status === 422) {
@@ -104,7 +139,8 @@ export const useCheckoutStore = defineStore('checkout', {
console.error('Server error', error);
}
- window.Telegram.WebApp.HapticFeedback.notificationOccurred('error');
+ const haptic = getHapticFeedback();
+ haptic.notificationOccurred('error');
this.errorMessage = 'Возникла ошибка при создании заказа.';
diff --git a/frontend/spa/src/stores/SettingsStore.js b/frontend/spa/src/stores/SettingsStore.js
index d799398..77939a1 100644
--- a/frontend/spa/src/stores/SettingsStore.js
+++ b/frontend/spa/src/stores/SettingsStore.js
@@ -15,6 +15,7 @@ export const useSettingsStore = defineStore('settings', {
feature_vouchers: false,
show_category_products_button: true,
currency_code: null,
+ haptic_enabled: true,
theme: {
light: 'light', dark: 'dark', variables: {
'--product_list_title_max_lines': 2,
@@ -53,6 +54,7 @@ export const useSettingsStore = defineStore('settings', {
this.mainpage_blocks = settings.mainpage_blocks;
this.privacy_policy_link = settings.privacy_policy_link;
this.image_aspect_ratio = settings.image_aspect_ratio;
+ this.haptic_enabled = settings.haptic_enabled ?? true;
}
}
});
diff --git a/frontend/spa/src/views/Account.vue b/frontend/spa/src/views/Account.vue
index 9601eab..87aae2e 100644
--- a/frontend/spa/src/views/Account.vue
+++ b/frontend/spa/src/views/Account.vue
@@ -82,6 +82,7 @@ import {onMounted, onUnmounted} from "vue";
import {useRoute} from "vue-router";
import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js";
import {usePulseStore} from "@/stores/Pulse.js";
+import {useHapticFeedback} from "@/composables/useHapticFeedback.js";
defineOptions({
name: 'Account'
@@ -92,6 +93,7 @@ const settings = useSettingsStore();
const route = useRoute();
const yaMetrika = useYaMetrikaStore();
const pulse = usePulseStore();
+const haptic = useHapticFeedback();
const username = computed(() => {
if (tgData?.user?.first_name || tgData?.user?.last_name) {
@@ -136,7 +138,7 @@ function openManagerChat() {
return;
}
- window.Telegram.WebApp.HapticFeedback.selectionChanged();
+ haptic.selectionChanged();
// Формируем ссылку для открытия чата с менеджером
const managerUsername = String(settings.manager_username).trim();
@@ -149,7 +151,7 @@ function openManagerChat() {
}
function handleHomeScreenAdded() {
- window.Telegram.WebApp.HapticFeedback.notificationOccurred('success');
+ haptic.notificationOccurred('success');
window.Telegram.WebApp.showAlert('Приложение успешно добавлено на главный экран!');
isHomeScreenAdded.value = true;
}
@@ -169,7 +171,7 @@ function checkHomeScreenSupport() {
}
function addToHomeScreen() {
- window.Telegram.WebApp.HapticFeedback.selectionChanged();
+ haptic.selectionChanged();
// Используем Telegram Web App API для добавления на главный экран
if (window.Telegram?.WebApp?.addToHomeScreen) {
diff --git a/frontend/spa/src/views/Cart.vue b/frontend/spa/src/views/Cart.vue
index 3f8828f..caf0ff2 100644
--- a/frontend/spa/src/views/Cart.vue
+++ b/frontend/spa/src/views/Cart.vue
@@ -153,12 +153,14 @@ import {useSettingsStore} from "@/stores/SettingsStore.js";
import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js";
import {YA_METRIKA_GOAL} from "@/constants/yaMetrikaGoals.js";
import BaseViewWrapper from "@/views/BaseViewWrapper.vue";
+import {useHapticFeedback} from "@/composables/useHapticFeedback.js";
const route = useRoute();
const yaMetrika = useYaMetrikaStore();
const cart = useCartStore();
const router = useRouter();
const settings = useSettingsStore();
+const haptic = useHapticFeedback();
// const componentMap = {
// radio: OptionRadio,
@@ -174,7 +176,7 @@ const lastTotal = computed(() => {
function removeItem(cartItem, cartId, index) {
cart.removeItem(cartItem, cartId, index);
- window.Telegram.WebApp.HapticFeedback.notificationOccurred('error');
+ haptic.notificationOccurred('error');
}
function goToCheckout() {
diff --git a/frontend/spa/src/views/Filters.vue b/frontend/spa/src/views/Filters.vue
index 9150095..7519153 100644
--- a/frontend/spa/src/views/Filters.vue
+++ b/frontend/spa/src/views/Filters.vue
@@ -53,6 +53,7 @@ import {useRoute, useRouter} from "vue-router";
import ProductCategory from "@/components/ProductFilters/Components/ProductCategory/ProductCategory.vue";
import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js";
import {YA_METRIKA_GOAL} from "@/constants/yaMetrikaGoals.js";
+import {useHapticFeedback} from "@/composables/useHapticFeedback.js";
defineOptions({
name: 'Filters'
@@ -70,7 +71,7 @@ const route = useRoute();
const emit = defineEmits(['close', 'apply']);
const filtersStore = useProductFiltersStore();
-const haptic = window.Telegram.WebApp.HapticFeedback;
+const haptic = useHapticFeedback();
const applyFilters = async () => {
filtersStore.applied = JSON.parse(JSON.stringify(filtersStore.draft));
diff --git a/frontend/spa/src/views/Home.vue b/frontend/spa/src/views/Home.vue
index 2325bbf..e870349 100644
--- a/frontend/spa/src/views/Home.vue
+++ b/frontend/spa/src/views/Home.vue
@@ -17,6 +17,7 @@ import {useSettingsStore} from "@/stores/SettingsStore.js";
import MainPage from "@/components/MainPage/MainPage.vue";
import {useBlocksStore} from "@/stores/BlocksStore.js";
import Navbar from "@/components/Navbar.vue";
+import {useHapticFeedback} from "@/composables/useHapticFeedback.js";
defineOptions({
name: 'Home'
@@ -25,7 +26,7 @@ defineOptions({
const router = useRouter();
const filtersStore = useProductFiltersStore();
const yaMetrika = useYaMetrikaStore();
-const haptic = window.Telegram.WebApp.HapticFeedback;
+const haptic = useHapticFeedback();
const settings = useSettingsStore();
const blocks = useBlocksStore();
diff --git a/frontend/spa/src/views/Product.vue b/frontend/spa/src/views/Product.vue
index 73acb0e..cc246e3 100644
--- a/frontend/spa/src/views/Product.vue
+++ b/frontend/spa/src/views/Product.vue
@@ -231,6 +231,7 @@ import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js";
import {YA_METRIKA_GOAL} from "@/constants/yaMetrikaGoals.js";
import Loader from "@/components/Loader.vue";
import SingleProductImageSwiper from "@/components/SingleProductImageSwiper.vue";
+import {useHapticFeedback} from "@/composables/useHapticFeedback.js";
const route = useRoute();
const productId = computed(() => route.params.id);
@@ -247,6 +248,7 @@ const settings = useSettingsStore();
const yaMetrika = useYaMetrikaStore();
const imagesLoaded = ref(false);
const images = ref([]);
+const haptic = useHapticFeedback();
const canAddToCart = computed(() => {
if (!product.value || product.value.options === undefined || product.value.options?.length === 0) {
@@ -269,7 +271,7 @@ async function onCartBtnClick() {
if (isInCart.value === false) {
await cart.addProduct(productId.value, product.value.name, product.value.price, quantity.value, product.value.options);
isInCart.value = true;
- window.Telegram.WebApp.HapticFeedback.notificationOccurred('success');
+ haptic.notificationOccurred('success');
yaMetrika.reachGoal(YA_METRIKA_GOAL.ADD_TO_CART, {
price: product.value.final_price_numeric,
currency: product.value.currency,
@@ -294,12 +296,12 @@ async function onCartBtnClick() {
}
});
} else {
- window.Telegram.WebApp.HapticFeedback.selectionChanged();
+ haptic.selectionChanged();
await router.push({'name': 'cart'});
}
} catch (e) {
- await window.Telegram.WebApp.HapticFeedback.notificationOccurred('error');
+ await haptic.notificationOccurred('error');
error.value = e.message;
}
}
@@ -322,7 +324,7 @@ function openManagerChat() {
return;
}
- window.Telegram.WebApp.HapticFeedback.selectionChanged();
+ haptic.selectionChanged();
// Формируем ссылку для открытия чата с менеджером
// manager_username должен быть username (например, @username)
@@ -340,7 +342,7 @@ function openManagerChat() {
function setQuantity(newQuantity) {
quantity.value = newQuantity;
- window.Telegram.WebApp.HapticFeedback.selectionChanged();
+ haptic.selectionChanged();
}
onMounted(async () => {
diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/configs/app.php b/module/oc_telegram_shop/upload/oc_telegram_shop/configs/app.php
index 29764e7..cdf5b3d 100755
--- a/module/oc_telegram_shop/upload/oc_telegram_shop/configs/app.php
+++ b/module/oc_telegram_shop/upload/oc_telegram_shop/configs/app.php
@@ -10,6 +10,7 @@ return [
"app_debug" => false,
'image_aspect_ratio' => '1:1',
'image_crop_algorithm' => 'cover',
+ 'haptic_enabled' => true,
],
'telegram' => [
diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/DTO/Settings/AppDTO.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/DTO/Settings/AppDTO.php
index 2fe864d..de2804a 100755
--- a/module/oc_telegram_shop/upload/oc_telegram_shop/src/DTO/Settings/AppDTO.php
+++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/DTO/Settings/AppDTO.php
@@ -12,6 +12,7 @@ final class AppDTO
private bool $appDebug;
private int $languageId;
private string $shopBaseUrl;
+ private bool $hapticEnabled;
public function __construct(
bool $appEnabled,
@@ -21,7 +22,8 @@ final class AppDTO
string $themeDark,
bool $appDebug,
int $languageId,
- string $shopBaseUrl
+ string $shopBaseUrl,
+ bool $hapticEnabled = true
) {
$this->appEnabled = $appEnabled;
$this->appName = $appName;
@@ -31,6 +33,7 @@ final class AppDTO
$this->appDebug = $appDebug;
$this->languageId = $languageId;
$this->shopBaseUrl = $shopBaseUrl;
+ $this->hapticEnabled = $hapticEnabled;
}
public function isAppEnabled(): bool
@@ -73,6 +76,11 @@ final class AppDTO
return $this->shopBaseUrl;
}
+ public function isHapticEnabled(): bool
+ {
+ return $this->hapticEnabled;
+ }
+
public function toArray(): array
{
return [
@@ -84,6 +92,7 @@ final class AppDTO
'app_debug' => $this->isAppDebug(),
'language_id' => $this->getLanguageId(),
'shop_base_url' => $this->getShopBaseUrl(),
+ 'haptic_enabled' => $this->isHapticEnabled(),
];
}
}
diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/SettingsHandler.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/SettingsHandler.php
index c514282..7ed6e12 100755
--- a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/SettingsHandler.php
+++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/SettingsHandler.php
@@ -55,6 +55,7 @@ class SettingsHandler
'mainpage_blocks' => $this->settings->get('mainpage_blocks', []),
'privacy_policy_link' => $this->settings->get('app.privacy_policy_link'),
'image_aspect_ratio' => $this->settings->get('app.image_aspect_ratio', '1:1'),
+ 'haptic_enabled' => $appConfig->isHapticEnabled(),
]);
}
diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/SettingsSerializerService.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/SettingsSerializerService.php
index 30a507e..3d3b6e7 100755
--- a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/SettingsSerializerService.php
+++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/SettingsSerializerService.php
@@ -80,7 +80,8 @@ class SettingsSerializerService
$data['theme_dark'] ?? 'dark',
$data['app_debug'] ?? false,
$data['language_id'],
- $data['shop_base_url']
+ $data['shop_base_url'],
+ $data['haptic_enabled'] ?? true
);
}