В этом руководстве рассказывается, как очень легко создать автономное веб-приложение, не неся ответственности ни за данные пользователя, ни за обслуживание серверной части.

Мы начнем с изучения того, как использовать Удаленное хранилище (RS), которое дает нам все, кроме возможности зарабатывать деньги. Позже мы переключаемся на расширение RS, которое так же легко, как и остальная часть RS, позволяет нам предлагать покупки в приложении. Зачем переключать внимание на приложение, которое приносит деньги? Таким образом, вы можете не выключать свет и, конечно же, иметь время для создания большего количества приложений.

Remote Storage (RS) — это усилия сообщества, это многое. Большинство из них легко продемонстрировать с помощью этого практического руководства.

С помощью библиотеки NPM remoteStorage.js RS становится автономной серверной частью для вашего веб-приложения. Сначала в автономном режиме, поскольку данные ваших пользователей синхронизируются с серверной частью, когда они находятся в сети, но используют локальное хранилище браузеров, когда они отключены. Все без каких-либо усилий с вашей стороны, разработчика приложения.

Экосистема RS представляет собой неразмещенную концепцию Bring-Your-Own-Data (BYOD): ваши пользователи аутентифицируются и авторизуются на любом из хостов RS в экосистеме или на своих собственных, и подключаются к вашему приложению со своими данными, которые они приносят от поставщика РС по своему выбору. Вы, разработчик приложения, не владеете их данными и не несете ответственности за них. Они, ваши пользователи, могут свободно определять, где хранятся их данные и насколько они безопасны. Для всех намерений и целей поставщиком RS может быть публичное предложение, такое как overhide.io или другие серверы RS, или собственная система пользователя.

Если вы занимаетесь devops, экосистема RS — это экосистема самостоятельных кластеров, созданных сообществом для всеобщего использования. Вы можете создать частный кластер только для своего приложения или для общего доступа. Вы можете предоставить хранилище RS бесплатно или взимать плату в долларах или эфирах. Чтобы поднять платный сервер, не ищите ничего, кроме Lucchetto RS server, батарейки в комплекте.

Весь код является бесплатным программным обеспечением с открытым исходным кодом (FOSS). Сообщество решительно выступает за открытость и расширение прав собственности на данные.

Первая часть руководства представляет собой практическую материализацию всего вышеперечисленного в упрощенном веб-приложении. Во второй части мы расширим наш пример, чтобы включить покупки в приложении. Мы будем использовать RS — хотя и с болтовым креплением Lucchetto — чтобы легко получать платежи в долларах и криптовалютах за дополнительную функцию.

Вопросы, комментарии? Обсуждаем на RS forums или r/overhide.

Простые листинги кода

Чтобы следовать этому, вам нужно хорошо разбираться в основах HTML, JavaScript в том, что касается DOM, git и npm.

Для краткости полные листинги кода не являются частью этой статьи, но на них есть ссылки в каждом разделе выносок кода:

‹/› ПОЛНЫЙ КОД

[!!] ПРЕДВАРИТЕЛЬНЫЙ ПРОСМОТР

Все листинги кода находятся в репозитории учебника на github.

Вам не нужно делать следующий шаг — вы можете просто посмотреть, о чем мы говорим, с помощью предоставленных ссылок «RENDERED PREVIEW» — но для самостоятельного рендеринга HTML-превью в какой-либо папке на вашем локальном компьютере:

git clone https://github.com/overhide/remotestorage-tutorial.git
cd remotestorage-tutorial
npm install -g http-server
http-server

Теперь два HTML-файла, о которых мы поговорим, можно открыть в браузере на порту 8080. Откройте их:

откройте https://localhost:8080/1-app.html

откройте https://localhost:8080/2-iaps.html

Наше приложение

Давайте посмотрим на наш пример учебного приложения:

‹/› ПОЛНЫЙ КОД

[!!] ПРЕДПРОСМОТР ОТОБРАЖЕНИЯ

После посещения RENDERED PRVIEW мы видим приложение для выбора торта с виджетом удаленного хранилища в верхнем левом углу, информативным текстом и тремя кнопками выбора торта внизу. Белое пространство посередине — это то место, где появится наш текущий выбор торта. На данный момент он пуст, так как мы еще ни разу не выбрали торт:

Вы можете нажать на кнопки внизу и увидеть, как выбирается торт.

Применительно к ПОЛНОМУ КОДУ каждая кнопка имеет onClick обработчик, который вызывает setState глобальную функцию:

<button ... onClick="setState({...appstate, cake_choice: 'Black Forest'})">Set "Black Forest"</button>
...
<button ... onClick="setState({...appstate, cake_choice: 'Carrot'})">Set "Carrot"</button>
...
<button ... onClick="setState({...appstate, cake_choice: 'Tiramisu'})">Set "Tiramisu"</button>

(1-rs.html :: строки 32–38)

Глобальная функция setState просто устанавливает имена тортов в client:

function setState(newState) {
  client.storeObject('AppState', 'appstate', newState);
}

(1-rs.html :: строки 95–97)

Параметры просто указывают, что некоторый объект newState, соответствующий схеме JSON 'AppState', должен храниться как объект JSON по относительному пути 'appstate'. Пути и схемы вскоре должны стать понятными.

Во-первых, client — это хранилище состояний нашего приложения, инициализированное ранее в коде:

// Construct and dependency inject
const remoteStorage = new RemoteStorage({changeEvents: { local: true, window: true, remote: true, conflicts: true }});
remoteStorage.access.claim('remotestorage-tutorial', 'rw');     
const client = remoteStorage.scope('/remotestorage-tutorial/');
// Initialize
document.addEventListener('DOMContentLoaded', function() {
  var widget = new Widget(remoteStorage, { leaveOpen:true });
  widget.attach('remotestorage-widget-anchor');
  client.cache('');

(1-rs.html :: строки 64–73)

Полную документацию по созданию RemoteStorage и созданию экземпляра client читайте в документах:

https://remotestoragejs.readthedocs.io/en/latest/js-api/remotestorage.html

https://remotestoragejs.readthedocs.io/en/latest/js-api/base-client.html

Короче говоря, мы создаем экземпляр RemoteStorage с полным кэшированием, удаленной синхронизацией и уведомлениями об изменении состояния. Это основной объект, о котором мы поговорим с экосистемой удаленного хранения.

Мы также создаем экземпляр widget после загрузки нашей DOM — визуальный компонент, с которым взаимодействует пользователь для входа в систему. Мы создаем его с помощью leaveOpen:true, чтобы он не схлопывался и всегда оставался открытым на полную ширину. Мы также прикрепляем его к remotestorage-widget-anchor div ранее на нашей странице: это просто то место, где он находится в DOM.

remoteStorage.access.claim(..) ограничивает каждый раз, когда пользователь подключает свою учетную запись хранения к нашему приложению, пользователь должен согласиться на доступ для чтения и записи ('rw') для пространства имен 'remotestorage-tutorial'. Нас это пока не касается: мы еще не пробовали подключаться к удаленному серверу. Но это коснется нас достаточно скоро.

Фактический client ограничивается путем /remotestorage-tutorial/ для его взаимодействий — это означает, что изменения в файлах по этому пути вызовут уведомления об изменениях через этот клиент. Обратите внимание, что путь повторяет пространство имен утверждения.

Наконец, мы запускаем уведомление об изменении нашего кеша с помощью client.cache(''). Пустая строка параметра означает, что нас интересуют все подпути для уведомлений об изменениях, хотя в этом приложении у нас есть только один подпуть; 'appstate'.

Имея эти экземпляры, мы готовы использовать уникальное пространство имен наших приложений, «remotestorage-tutorial», как в локальных, так и в удаленных хранилищах.

Называть это пространством имен и делать вид, что оно должно быть уникальным, — это чрезмерное упрощение. Существует целый спектр взаимодействий, возможных благодаря совместному использованию этих путей в соответствии с модулями данных RS. Но это более сложная тема.

https://remotestoragejs.readthedocs.io/en/latest/data-modules.html

Давайте перейдем к важности параметров { local: true, window: true, remote: true, conflicts: true }, переданных в конструктор RemoteStorage и вызове client.cache(''). С ними мы просим, ​​чтобы, поскольку результирующий client отслеживал состояние нашего приложения, он обновлял наши виджеты рендеринга с односторонним потоком состояния.

Мы уже видели, как client получает новое состояние через функцию client.storeObject(..). Полностью отделенный от этих записей, у нас есть прослушиватель изменения состояния:

client.on('change', (event) => {
  if (event.relativePath === 'appstate') {
    appstate = event.newValue;
	document.getElementById('choice').innerHTML = appstate.cake_choice;
  }
});

(1-rs.html :: строки 83–88)

Здесь мы регистрируем обработчик событий для client. Этот код будет вызываться при каждом локальном изменении в браузере или удаленном изменении любого объекта в нашей ранее зарегистрированной области (корневой путь '/remotestorage-tutorial/').

В нашем обработчике выше нас интересуют только изменения вложенного пути /appstate.

При обновлении мы просто устанавливаем значение нашего #choice div на новый выбор торта.

То, чего мы достигли, — это односторонний поток состояния через наш client:

Чего мы также прозрачно добиваемся, так это надежного сохранения состояния приложения в предоставленном нашим пользователем хранилище: как локальном, так и удаленном. На самом деле, если вы нажмете F5 в RENDERED PRVIEW, после обновления страницы первое, что сделает client, — это отправит уведомление change нашему обработчику с последним состоянием приложения, которое оно постоянно кэшировало в локальном хранилище браузера.

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

Для этого туториала мы будем использовать сервер @test.rs.overhide.io, но есть и другие.

@test.rs.overhide.io — это бесплатный сервер удаленного хранения, предназначенный для тестирования и экспериментов. Он делает вид, что требует денег, но все это фальшивые транзакции в долларах США и транзакции Ethereum в тестовой сети.

Вы можете поставить точно такой же сервер на своей машине из репозитория Armadietto+Lucchetto, или пока просто использовать @test.rs.overhide.io.

Нажмите на виджет remote-storage, чтобы войти в систему:

В поле ввода введите @test.rs.overhide.io и нажмите «Подключиться».

Вам предлагается множество вариантов входа в систему. А пока прокрутите страницу вниз до карточки «секретный токен» и нажмите «создать новый». Система сгенерирует новый секретный токен для вашего входа в систему. Сохраните это в менеджере паролей вашего браузера, когда будет предложено, или скопируйте его (маленький синий буфер обмена) и сохраните его вручную.

Нажмите «Продолжить».

Вам будет предложено произвести оплату через Stripe.com. Это поддельный платеж в долларах США в тестовой сети Stripe.com.

Введите поддельную информацию в поля в соответствии с инструкциями на экране:

Теперь вы снова в нашем примере приложения для торта и подключены к своему недавно приобретенному удаленному хранилищу.

Точно такое же хранилище можно использовать в любом другом приложении RS. Надеюсь, вы сохранили свой секретный токен! Хотя ваш менеджер паролей в браузере должен помнить, пока вы придерживаетесь его.

Чтобы облегчить себе жизнь, лучше всего использовать провайдеров социальных сетей на этом экране входа или кошелек Ethereum, если вы можете его подключить. Поиграйте с различными вариантами.

Теперь, когда вы нажимаете на различные пирожные, вы должны видеть, как виджет remote-storage вращается и синхронизируется: он синхронизируется с вашим недавно приобретенным remote-storage.

Прежде чем мы завершим этот раздел, давайте вернемся к нашему объекту appstate, поскольку он сохранен в client:

client.storeObject('AppState', 'appstate', newState);

(1-rs.html :: строка 95)

Мы упоминали, что первый параметр 'AppState' указывает схему JSON, ожидаемую от третьего параметра. Эта схема JSON зарегистрирована для client:

const AppState = {
  type: 'object',
  properties: {
	cakce_choice: {
	  type: 'string'
	}
  },
  required: ['cake_choice']
};
...
client.declareType('AppState', AppState);

(1-rs.html :: строки 49–57 и 77)

Схема 'AppState' со значением из newState сохраняется по пути appstate, который является путем относительно пути области видимости нашего client. Следовательно, мы можем думать, что объект находится в /remotestorage-tutorial/appstate.

Наше приложение с покупками в приложении

Теперь, когда мы научились использовать Remote Storage для управления и кэширования состояния нашего приложения, давайте добавим в него покупки в приложении:

‹/› ПОЛНЫЙ КОД

[!!] ПРЕДПРОСМОТР ОТОБРАЖЕНИЯ

Основным отличием здесь является серая кнопка покупки в приложении, предлагаемая вашим пользователям в нижней части нашего приложения, в соответствии с новым RENDERED PRVIEW:

Новая опция «Купить торт NFT» за 3 доллара обеспечивает приятную добавленную стоимость для вашего клиента.

Действие этой кнопки зависит от нескольких факторов подключения вашего пользователя к удаленному хранилищу. Однако в конце дня кнопка авторизует, а затем вызовет событие, побуждающее наше приложение загрузить часть данных, которая находится за платным доступом в 3 доллара. Это часть данных, которую мы — разработчики приложений — настраиваем (ну, я настроил для этого примера). Это не часть данных пользователя, это данные из удаленного хранилища разработчика.

Если вы следуете за пользователем, который только что вошел в @test.rs.overhide.io remote-storage, вы все равно должны войти на эту новую страницу с вашим действительным remote-storage соединение: это тот же домен, следовательно, авторизуется тот же токен. Поскольку @test.rs.overhide.io — это так называемый расширенный RS-сервер Lucchetto, то есть он хорошо сочетается с этой новой кнопкой покупки в приложении от https://pay2my.app. виджеты — наш клик должен быть бесшовным. Нам просто нужно пополнить 3 доллара для нового IAP.

Почему мы снова платим?

Имейте в виду, что предыдущая оплата в размере 1,99 доллара США была произведена поставщику remote-storage @test.rs.overhide.io: за хранилище. Этот платеж в размере 3 долларов США предназначен для дополнительной добавленной стоимости в приложении.

Если ваш пользователь вошел в это приложение с помощью обычного сервера RS, а не расширенногоLucchetto, IAP будет работать почти так же хорошо. Заметным отличием являются дополнительные подсказки всякий раз, когда пользователь повторно посещает приложение; поскольку кнопка IAP не может использовать нерасширенную авторизацию RS для платежей: обычные серверы RS возвращают токены без дополнительных необходимых метаданных.

В любом случае нажатие на кнопку в конечном итоге приведет к выполнению следующего обработчика (из ПОЛНОГО КОДА):

window.addEventListener('pay2myapp-appsell-sku-clicked', async (event) => { 
  try {
    const result = await
      lucchetto.getSku(`https://test.rs.overhide.io`, event.detail);
    alert(result);
  } catch (event) {
    console.error(`error :: no file for sku ${event.detail.sku}`);
  }      
}, false);

(2-iaps.html :: строки 145–152)

Событие pay2myapp-appsell-sku-clicked происходит от виджетов https://pay2my.app, которые ранее были теми же виджетами, обеспечивающими аутентификацию и авторизацию на нашем сервере @test.rs.overhide.io.

Событие pay2myapp-appsell-sku-clicked задокументировано как часть веб-компонента pay2myapp-appsell.

Чтение кода обработчика выше; мы используем event для получения result через lucchetto.getSku(..) и alert(result) для пользователя. alert(result) — это добавленная стоимость нашего приложения; вы явно будете лучше работать с данными. Вызов lucchetto.getSku(..) принимает объект detail события и знает, как его интерпретировать, чтобы получить нужный контент с расширенного сервера RS Lucchetto, предоставленного в качестве первого параметра, 'https://test.rs.overhide.io' в нашем случае.

Имейте в виду, что lucchetto.getSku(..) использует ваше — разработчиков приложений —LucchettoRS-подключение для получения платных данных, поэтому необходимо предоставить строку подключения вашего RS-сервера. Объект event.detail предоставляет адреса from (клиента) и to (ваш) выбранной книги, а также signature пользователя для некоторых message, по сути, все, что необходимо серверуLucchettoRS для проверки подлинности и платежей в книге. : недостаточно сделать это в браузере, сервер RS проверяет сам.

Позже мы расскажем, как добавить этот контент SKU на сервер RS.

Этот объект lucchetto, используемый для lucchetto.getSku(..), является связующим звеном, соединяющим авторизацию remote-storage с этими кнопками IAP в вашем приложении. На самом деле мы используем его только в двух местах, в этом lucchetto.getSku(..) и ранее, для создания и инициализации:

const lucchetto = new Lucchetto({
  remoteStorage: remoteStorage, 
  overhideIsTest: true, 
  overhideApiKey: '0x6cc3b096ef...a44080b',
  pay2myAppHub: document.getElementById('demo-hub')});

(2-iaps.html :: строки 104–108)

При инициализации lucchetto мы предоставляем конструктору объект параметров, включающий экземпляр remote-storage, который мы уже используем, remoteStorage. Это позволяет lucchetto использовать текущие данные пользователя, выполнившего вход в remote-storage, чтобы сделать процессы покупок в приложении максимально банальными.

Затем мы устанавливаем для параметра overhideIsTest значение true, чтобы указать, что мы используем тестовые сети. В системе живого производства вы бы установили это значение на false.

Мы также предоставляем overhideApiKey. overhideApiKey – это ваш ключ разработчика для доступа к реестрам через кластер overhide. Получить API-ключ не составляет большого труда, кроме обычных ограничений скорости (см. https://pay2my.app) особых ограничений нет. overhideApiKey является необязательным, если при подключении remoteStorage ваши пользователи всегда подключаются с расширенного сервера RS Lucchetto, такого как https://rs.overhide.io. Но, поскольку такое подключение будет не у каждого пользователя, скорее всего, будут приходить пользователи с https://5apps.com и других провайдеров. Разумно указать здесь значение по умолчанию: иначе мы не сможем обращаться к кластеру overhide для взаимодействия с реестрами.

Чтобы получить собственный apiKey для своего приложения, посетите https://token.overhide.io/register.

Убедитесь, что вы получили правильный ключ для вашего приложения тестовой сети и рабочий ключ для развертывания приложения в реальном времени.

Наконец, для параметра pay2myAppHub устанавливается ссылка HTMLElement веб-компонента pay2myapp-hub, первого веб-компонента в приведенном ниже списке (на который ссылается id). Экземпляр lucchetto, который мы создаем, влияет на все веб-компоненты покупок в приложении в этом листинге кода.

<pay2myapp-hub id="demo-hub" isTest noCache></pay2myapp-hub>      
<pay2myapp-login  
  hubId="demo-hub"
  overhideSocialMicrosoftEnabled
  overhideSocialGoogleEnabled
  overhideWeb3Enabled
  ethereumWeb3Enabled
  overhideSecretTokenEnabled>
</pay2myapp-login>
<pay2myapp-appsell 
  hubId="demo-hub" 
  sku="buy-nft"
  priceDollars="3"
  authorizedMessage="Cake NFT"
  unauthorizedTemplate="Buy Cake NFT ($${topup})"
  ethereumAddress="0xd6106c445A07a6A1caF02FC8050F1FDe30d7cE8b"
  overhideAddress="0xd6106c445A07a6A1caF02FC8050F1FDe30d7cE8b">
</pay2myapp-appsell>

(2-iaps.html :: строки 55–74)

Первый веб-компонент — уже упомянутый pay2myapp-hub. Он помечен идентификатором "demo-hub", поэтому на него можно ссылаться из других компонентов (или программно в больших приложениях с фреймворками). Концентратор — это основной компонент, который взаимодействует с реестрами и сервисами от имени всех остальных компонентов. Атрибут isTest указывает, что концентратор будет обмениваться данными с реестрами тестовой сети. Не используйте этот атрибут для производственных развертываний. Атрибут noCache просит систему не кэшировать учетные данные: учитывая, что для этого мы используем remote-storage.

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

Третий компонент — это фактическая кнопка upsell (app-sell?). он указывает sku — тег, присвоенный функции, которую мы продаем. priceDollars - это стоимость. Стоимость всегда указывается в долларах США и при необходимости автоматически конвертируется в эфиры. authorizedMessage — это текст кнопки при авторизации. unauthrizedTemplate может включать заполнитель $${topup} для непогашенной суммы в долларах.

ethereumAddress и overhideAddress — это общедоступные адреса Ethereum, которые используются для приема платежей, но в двух разных реестрах. Эфириум говорит сам за себя. Книга overhide — это книга квитанций в долларах США, которая использует адреса Ethereum, но не выполняет переводы стоимости. Передача стоимости осуществляется через Stripe.com.

Существует множество других конфигураций этих кнопок, как описано в документации pay2myapp-appsell. Еще больше криптовалют, таких как Биткойн, и растет. Но дляLucchettoRS мы фокусируемся только на криптовалютах в адресном пространстве Ethereum.

Давайте сосредоточимся на этих адресах немного больше. Это кнопка дополнительной продажи функции, при нажатии на которую создается ранее упомянутое событие pay2myapp-appsell-sku-clicked и приводит нас к загрузке значения с использованием lucchetto.getSku(..). Авторизация здесь означает: с адреса нашего пользователя было совершено достаточно платежей либо на ethereumAddress за эфиры, либо на overhideAddress за доллары.

Позвольте мне описать, что я сделал, чтобы сделать этот адрес 0xd6106c445A07a6A1caF02FC8050F1FDe30d7cE8b доступным для этого веб-компонента, и, надеюсь, это будет иметь смысл, когда вы захотите добавить свой собственный.

ethereumAddress="0xd6106c445A07a6A1caF02FC8050F1FDe30d7cE8b"
overhideAddress="0xd6106c445A07a6A1caF02FC8050F1FDe30d7cE8b">

(2-iaps.html :: строки 72–73)

Процесс для mainnet/prod/live такой же, как и для testnet/fake, за исключением разных сетей.

Прежде всего, я настраиваю кошелек MetaMask в своем браузере, и один из моих адресов Ethereum — 0xd6106c445A07a6A1caF02FC8050F1FDe30d7cE8b.

Благодаря тому, что у меня есть этот адрес, я уже настроен на получение эфиров. Мне просто нужно указать этот адрес для ethereumAddress.

Для долларов США это немного сложнее, так как нам нужно подключить этот адрес Ethereum к учетной записи Stripe.com.

Наводим наш браузер на testnet onboarding и запускаем онбординг. Я подключил свой кошелек MetaMask к странице регистрации, чтобы использовать его адреса.

После перехода к учетной записи Stripe.com просто пропустите форму: к сожалению, когда вы будете готовы к работе, вы не можете просто пропустить эту часть регистрации Stripe.com.

После подключения скрытая бухгалтерская книга в долларах США будет записывать платежи в долларах США через Stripe.com между адресами ваших пользователей и вашим встроенным адресом, обеспечивая авторизацию на основе книги.

Для рабочей адаптации Ethereum просто используйте тот же адрес, но сосновной сетью.

Дляoverhideбухгалтерской книги в долларах США вам необходимо выполнить полную адаптацию к производственному потоку.

Теперь мы на борту. Мы знаем, как учитываются эти адреса. Но как насчет данных SKU?

Если вы помните, ранее при авторизации мы получали данные через lucchetto.getSku(..). Перед этим мы создали кнопку pay2myapp-appsell, указав sku и priceDollars.

Как это настроить?

Оказывается, SKU — это просто удаленные хранилища, хранящиеся фрагменты plain/text данных, обслуживаемые расширенными серверами RS Lucchetto, такими как @test.rs.overhide.io.

Для их настройки нам просто нужно использовать приложение RS.

На момент написания этой статьи…

Настройте SKU тестовой сети с помощью @test.rs.overhide.io.

Настройте рабочие SKU с помощью @rs.overhide.io.

Проверьте страницу серверов RS для получения дополнительных параметров, если таковые имеются.

Встроенное приложение Lucchetto SKU не требует пояснений. Вы подключаетесь к расширенному серверу RS Lucchetto, такому как один из перечисленных выше. Подключенный адрес должен соответствовать ethereumAddress и overhideAddress. Если вы сделали их разными, вам потребуется поддерживать отдельные соединения RS для управления вашими SKU. Для здравомыслия, вероятно, лучше оставить их одинаковыми.

В моем случае я бы подключил приложение к @test.rs.overhide.io с помощью 0xd6106c445A07a6A1caF02FC8050F1FDe30d7cE8b из моего криптокошелька.

После подключения приложение позволяет вам перечислить существующие SKU, удалить их, но самое главное, вставить новые с помощью карты UPSERT. Просто введите цену, 3 в нашем случае (3 доллара США), внутри, 0 в нашем случае (неопределенный) и SKU, buy-nft в нашем случае, все соответствует нашей кнопке. Наконец, установите данные, то есть содержимое, возвращаемое вызовом lucchetto.getSku(..). Для наших целей я установил The cake is a lie!.

После того, как вы «UPSERT», вы должны увидеть подтверждение, и все готово. Артикул готов к использованию:

Remote-storage — это отличный способ написания приложений.

Мы надеемся, что добавление покупок в приложении означает, что вы сможете получать откаты за замечательную работу, которую вы делаете.

Спасибо за прочтение.