VK Mini Apps

VK Mini Apps ↗ – это кроссплатформенные мини-приложения в экосистеме социальной сети ВКонтакте ↗. При создании VKUI изначально задумывался как набор компонентов для таких приложений. Несмотря на то, что с течением времени VKUI стал использоваться не только для мини-приложений, но по-прежнему отлично подходит и для них. Ниже приведены рекомендации по интеграции с VK Mini Apps.

Для начала устанавливаем библиотеки:

npm i --save @vkontakte/vk-bridge @vkontakte/vk-bridge-react

Далее подписываемся на изменения из VK Bridge и передаём их в компонент ConfigProvider из VKUI. Для этого используем готовые хуки из @vkontakte/vk-bridge-react ↗.

App.tsx
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import vkBridge, { parseURLSearchParamsForGetLaunchParams } from '@vkontakte/vk-bridge';
import { useAppearance, useInsets, useAdaptivity } from '@vkontakte/vk-bridge-react';
import { Platform, ConfigProvider, AdaptivityProvider, AppRoot } from '@vkontakte/vkui';
import { transformVKBridgeAdaptivity } from './helpers/transformVKBridgeAdaptivity';
 
// Инициализируем VK  Mini App
vkBridge.send('VKWebAppInit');
 
const App = () => {
  const vkBridgeColorScheme = useAppearance() || undefined; // Вместо undefined можно задать значение по умолчанию
  const vkBridgeInsets = useInsets() || undefined; // Вместо undefined можно задать значение по умолчанию
  const vkBridgeAdaptivityProps = transformVKBridgeAdaptivity(useAdaptivity()); // Конвертируем значения из VK Bridge в параметры AdaptivityProvider
  const { vk_platform } = parseURLSearchParamsForGetLaunchParams(window.location.search); // [опционально] Платформа может передаваться через URL (см. https://dev.vk.com/mini-apps/development/launch-params#vk_platform)
 
  return (
    <ConfigProvider
      colorScheme={vkBridgeColorScheme}
      platform={vk_platform === 'desktop_web' ? 'vkcom' : undefined}
      isWebView={vkBridge.isWebView()}
      hasCustomPanelHeaderAfter={true} // Резервируем правую часть PanelHeader под кнопки управления VK Mini Apps. Через параметр customPanelHeaderAfterMinWidth можно регулировать ширину этой области (по умолчанию, используется 90)
    >
      <AdaptivityProvider {...vkBridgeAdaptivityProps}>
        {/* Для VK Mini Apps рекомендуем использовать mode="full" (выставлен по умолчанию, для примера указан явно) */}
        <AppRoot mode="full" safeAreaInsets={vkBridgeInsets}>
          {/* Ваше приложение */}
        </AppRoot>
      </AdaptivityProvider>
    </ConfigProvider>
  );
};
 
const container = document.getElementById('root');
const root = createRoot(container); // createRoot(container!), если используется TypeScript
root.render(<App />);
./helpers/transformVKBridgeAdaptivity.ts
import {
  type AdaptivityProps,
  getViewWidthByViewportWidth,
  getViewHeightByViewportHeight,
  ViewWidth,
} from '@vkontakte/vkui';
import type { UseAdaptivity } from '@vkontakte/vk-bridge-react';
 
/**
 * Требуется конвертировать данные из VK Bridge в те, что принимает AdaptivityProvider из VKUI.
 */
export const transformVKBridgeAdaptivity = ({
  type,
  viewportWidth,
  viewportHeight,
}: UseAdaptivity): AdaptivityProps => {
  switch (type) {
    case 'adaptive':
      return {
        viewWidth: getViewWidthByViewportWidth(viewportWidth),
        viewHeight: getViewHeightByViewportHeight(viewportHeight),
      };
    case 'force_mobile':
    case 'force_mobile_compact':
      return {
        viewWidth: ViewWidth.MOBILE,
        sizeX: 'compact',
        sizeY: type === 'force_mobile_compact' ? 'compact' : 'regular',
      };
    default:
      return {};
  }
};

На стартовой странице мини-приложения вы должны включить свайпбек нативного клиента, чтобы пользователь смог выйти из мини-приложения. Для этого нужно вызывать определенные методы VK Bridge в зависимости от типа мини-приложения:

  • Если вы делаете стандартное мини-приложение ВКонтакте, то при переходах отправляйте VKWebAppSetSwipeSettings ↗ с history: true на первой панели и history: false на других.
  • Если вы делаете внутреннее мини-приложения ВКонтакте, то отправляйте событие VKWebAppEnableSwipeBack ↗ при переходе на первый экран и событие VKWebAppDisableSwipeBack ↗ при переходе на любую другую.
import vkBridge from '@vkontakte/vk-bridge';
 
const SomeViews = () => {
  const [history, setHistory] = useState(['main']);
  const activePanel = history[history.length - 1];
  const isFirst = history.length === 1;
 
  const go = React.useCallback((panel) => setHistory((prevHistory) => [...prevHistory, panel]), []);
  const goBack = React.useCallback(() => setHistory((prevHistory) => prevHistory.slice(0, -1)), []);
  const handleProfileClick = () => go('profile');
  const handleMainClick = () => go('main');
 
  React.useEffect(() => {
    // Для стандартных мини-приложений делайте так:
    vkBridge.send('VKWebAppSetSwipeSettings', { history: isFirst });
    // Для внутренних мини-приложений делайте так:
    vkBridge.send(isFirst ? 'VKWebAppEnableSwipeBack' : 'VKWebAppDisableSwipeBack');
  }, [isFirst]);
 
  return (
    <View history={history} activePanel={activePanel} onSwipeBack={goBack}>
      <Panel id="main">
        <div onClick={handleProfileClick}>Main</div>
      </Panel>
      <Panel id="profile">
        <div onClick={handleMainClick}>Profile</div>
      </Panel>
    </View>
  );
};

Мини-приложению доступна почти вся площадь экрана, поэтому для корректной работы навигации необходимо использовать компонент PanelHeader на каждом экране приложения. Он должен содержать название приложения и значок Назад (см. PanelHeaderBack) на тех экранах, где тот требуется.

Для улучшения пользовательского опыта при взаимодействии с вашим мини-приложением в VK Bridge существуют события вызова вибрации на устройстве, которое их поддерживает.

@vkontakte/vk-bridge-react ↗ предоставляет функцию runTapticImpactOccurred, которая отправляет событие VKWebAppTapticImpactOccurred ↗, как раз заранее проверяя, что возможность вызова вибрации доступна на устройстве.

Виброотликом можно воспользоваться, например, при использовании компонента PullToRefresh на onRefresh.

import * as React from 'react';
import { runTapticImpactOccurred } from '@vkontakte/vk-bridge-react';
 
const Users = () => {
  const [users, setUsers] = React.useState([
    { id: 1, name: 'Placeholder', avatarUrl: 'https://placehold.co/100' },
  ]);
  const [fetching, setFetching] = React.useState(false);
 
  const onRefresh = React.useCallback(() => {
    setFetching(true);
    // Вызываем виброотклик
    runTapticImpactOccurred('light');
  }, []);
 
  return (
    <View activePanel="users">
      <Panel id="users">
        <PanelHeader>Пользователи</PanelHeader>
 
        <PullToRefresh onRefresh={onRefresh} isFetching={fetching}>
          <Group>
            <List>
              {users.map(({ id, name, avatarUrl }, i) => {
                return (
                  <Cell key={i} before={<Avatar src={avatarUrl} />}>
                    {name}
                  </Cell>
                );
              })}
            </List>
          </Group>
        </PullToRefresh>
      </Panel>
    </View>
  );
};