Skip to content

2. Integration guide

TON Play edited this page Dec 19, 2024 · 40 revisions

PlayDeck Integration Guide

If your game is based on unity, then you will have to use our unity bridge for integration

To launch your game in PlayDeck, you need to meet the following requirements:

  • The build weight is less than 50 Mb
  • The game loads within 10 seconds
  • The game must have in-game analytics enabled
  • The game must implement the mandatory methods

Some methods are mandatory to be implemented:

  • loading() At the start of the game loading process, it's essential to transmit loading (1), and similarly, at the end when the game has finished loading (100). If the loading progress reaching 100% is not signaled, the Play button within the wrapper won't become active.

  • The game should consider to use correct user locale for rendering proper UI texts. You can find locale by calling getUserProfile() method OR use devices locale in order of prioriry: (navigator.languages && navigator.languages.length) ? navigator.languages[0] : navigator.language;

  • setData()/getData() It can be utilized for cloud saving and cross-device experiences. For example, if your game has levels or your players accumulate some in-game bonuses, you can save and share that information using these methods so that you do not ruin the user experience. This can also be implemented through other methods on the developer side.


Check out our FAQ page.

Inside Playdeck, your game runs in an iFrame in our Wrapper. The process of passing data between your game and our Wrapper is via window.postMessage. Window: postMessage() docs

Let's look at an example of listening to messages and processing data from our Wrapper.

// Creating an event listener playdeck
window.addEventListener('message', ({ data }) => {
  if (!message || !data['playdeck']) return;

  pdData = data['playdeck'];

  // By default, playdeck sends "{ playdeck: { method: "play" }}" after pressing the play button in the playdeck-menu
  if (pdData.method === 'play') { 
    if (runner.crashed && runner.gameOverPanel) {
      runner.restart();
    } else {
      var e = new KeyboardEvent('keydown', { keyCode: 32, which: 32 });
      document.dispatchEvent(e);
    }
  }
  
  // Getting the playdeck-menu status, after using the getPlaydeckState method
  if (pdData.method === 'getPlaydeckState') {
    window.playdeckIsOpen = data.value; // if true, then the playdeck is open
  }
});

const parent = window.parent.window;

const payload = {
  playdeck: {
  method: 'getPlaydeckState',
  },
};

// calling the method
parent.postMessage(payload, '*');

In your example, we track the event of pressing the "play" button in the playdeck-menu, and also get the result of the getPlaydeckState method. Obviously, you can't call the method directly. We have saved the logic of constructing data for messages. For example, you want to use the loading method. To do this, you need to create an object with 2 fields: method, value Where the value of the method field will be the name of the method to be called, and the value field will be the loading state data.

Message Example

const payload = {
  playdeck: {
    method: 'loading',
    value: 100,
  },
};

parent.postMessage(payload, '*');

You can find usage examples and detailed information on each method in our guide below.


Contents

  1. Getting user information
  2. Cloud save
  3. Sharing and opening links
  4. Getting Wrapper Information
  5. Analytics
  6. Payments

1. Getting user information

getUserProfile: () => Profile

// This method allows you to get user profile data

type Profile = {
  avatar: string,
  username: string,
  firstName: string,
  lastName: string,
  telegramId: number,
  locale: 'en' | 'ru',
  token: string, // You can read more about our jwt token https://github.com/ton-play/playdeck-integration-guide/wiki/4.--User-JWT
  params: { [key: string]: string }, // You can create a link with a query string to the game using the method customShare or getShareLink
  sessionId: string,
  currentGameStarted: number
};

const parent = window.parent.window;
parent.postMessage({ playdeck: { method: 'getUserProfile' } }, '*');

window.addEventListener('message', ({ data }) => {
  const playdeck = data?.playdeck;
  if (!playdeck) return;

  if (playdeck.method === 'getUserProfile') {
    console.log(playdeck.value); // Profile
  }
});

2. Cloud save

  /**
   * Set Data - use to save arbitrary data in between sessions.
   * @param {string} data - value (limit 10Kb)
   * @param {string} key - key name (length limit 50 symbols) 
  */
  setData: (key: string, data: string) => void

  /**
   * Get Data - use to obtain saved data.
   * @param {string} key - key name
   * @return
   * `{"playdeck": {
   *   "method": "getData",
   *   "value": "value",
   *   "key": "key"}
   * }`
  */
  getData: (key: string) => Object

setData: (key: string, data: string) => void

// This method will allow you to store any data you may need.
// Data is saved by key. To retrieve previously saved data, use the `getData` method.

const parent = window.parent.window;

parent.postMessage(
  {
    playdeck: {
      method: 'setData',
      key: key,
      value: yourData,
    },
  },
  '*'
);

getData: (key: string) => { key: key, data: data }

// This method allows you to read previously written data by key.
// If there is no value for this key, it will return an empty object.
// Use the `setData` method to save the data.

const parent = window.parent.window;
parent.postMessage({ playdeck: { method: 'getData', key: key } }, '*');

window.addEventListener('message', ({ data }) => {
  const playdeck = data?.playdeck;
  if (!playdeck) return;

  if (playdeck.method === 'getData') {
    if (playdeck.key === 'x') {
      window.customData = playdeck.value;
    }
  }
});

3. Sharing and opening links

customShare: (object) => void

  // Creating a telegram link with a query string and starts the share procedure
  // You can get a query string from the game using the following methods getUrlParams and getUserProfile

const parent = window.parent.window;
parent.postMessage({ playdeck: { method: "customShare", value: {[key: string]: string} } }, "*");

getShareLink: (object) => string

// Creating a telegram link with a query string
// You can get a query string from the game using the following methods getUrlParams and getUserProfile

const parent = window.parent.window;
parent.postMessage({ playdeck: { method: "getShareLink", value: {[key: string]: string} } }, "*");

window.addEventListener("message", ({ data }) => {
  const playdeck = data?.playdeck;
  if (!playdeck) return;

  if (playdeck.method === "getShareLink") {
    console.log(playdeck.value) // link to the game
  }
});

Our platform supports format links: https://t.me/playdeckbot/game?startapp=eyJnYW1lIjp7I... The params "startparam" contains a non-standard jwt-token. After clicking on such links, a number of parameters enter the game, which can later be obtained through the getUserProfile or getUrlParams methods

openTelegramLink: (url: string) => void

// This method allows you to open links from within telegram clients
// Method is device aware:
// On Desktop clients links always open in an external browser (user's default browser in OS)
// On mobile clients:
// -- if link is telegram link (t.me, telegram.me) --> opens inside // telegram's internal browser
// -- if link to external resource (google.com, etc) --> open in an // external browser

const parent = window.parent.window;

parent.postMessage(
  {
    playdeck: {
      method: 'openTelegramLink',
      value: 'https://t.me/playdeckbot/market',
    },
  },
  '*'
);

4. Exchange of information with the Wrapper

loading: (pct: number | undefined) => void

// This method is sent unilaterally only to our integration environment.
// Causes our integration environment to display the download percentage of your game.
// Accepts values from 0 to 100 as a percentage. If you do not pass a value when calling at all,
// then the method will automatically simulate loading up to 80%.
// In order for the progress bar to become a Play button, you need to pass the value 100.

const parent = window.parent.window;

// We call the loading method without passing a value
// so that the integration environment starts displaying the loading process
parent.postMessage({ playdeck: { method: 'loading' } }, '*');
// Artificially slow down the download completion call by 1 second
setTimeout(() => {
  parent.postMessage({ playdeck: { method: 'loading', value: 100 } }, '*');
}, 1000);

getPlaydeckState: () => boolean

// This method will return you information about
// whether our integration environment overlay is currently open.

const parent = window.parent.window;
parent.postMessage({ playdeck: { method: 'getPlaydeckState' } }, '*');

window.addEventListener('message', ({ data }) => {
  const playdeck = data?.playdeck;
  if (!playdeck) return;

  if (playdeck.method === 'getPlaydeckState') {
    window.isPlayDeckOpened = playdeck.value; // `value` === true or false;
  }
});

gameEnd: () => void

// This method is sent unilaterally only to our integration environment.
// It signals to our integration environment that the game has been over.
// After that we demonstrate the popup.

const parent = window.parent.window;
parent.postMessage({ playdeck: { method: 'gameEnd' } }, '*');

5. Analytics

sendGameProgress: (achievements: Achievement[], progress: Progress) => void

  interface Achievement {
    name: string;
    description: string (optional);
    points: number (optional);
    value: number (optional);
    additional_data: {
      [key: string]: any;
    };
  };

  interface Progress = {
    level: number (optional);
    xp: number (optional);
  };

  /**
  * Send game progress in analytics
  * @param {Achievement[]} achievements - a list of achievements to report. This parameter is optional, but you must specify at least one of either "achievements" or "progress".
  * @param {Progress} progress - current progress data to be reported. This parameter is optional, but you must specify at least one of either "achievements" or "progress".
  */
  sendGameProgress: (achievements: Achievement[], progress: Progress) => void
  (for example: sendGameProgress: (achievements: [{name: 'Level 5', description: 'Reach 5th level', points: 100}], progress: {level: 5}) )
const parent = window.parent.window;
parent.postMessage({ playdeck: { method: 'sendGameProgress', value: {achievements: Achievement[], progress: Progress} } },'*');

sendAnalyticNewSession: (event: Event) => void

  /**
  * Recording a new game session. If your game has short sessions (for example, floppy bird), 
  * then count each attempt as a new session
  */
  sendAnalyticNewSession: () => void
const parent = window.parent.window;
parent.postMessage({ playdeck: { method: 'sendAnalyticNewSession' } },'*');

For more other events, there is a method for collecting any information:

sendAnalytics: (event: Event) => void

/* This method allows you to send game events to our integration environment*/

const parent = window.parent.window;

type Event = {
  name: string,
  type: string,
  user_properties: Record<string, any>,
  event_properties: {
    name: string,
    [key: string]: any,
  },
};

const event_example: Event = {
  type: 'click',
  user_properties: {},
  event_properties: {
    name: 'play_button',
  },
};
parent.postMessage(
  { playdeck: { method: 'sendAnalytics', value: event_example } },
  '*'
);

In our platform, your event will be recorded as:

{
  type: string,
  created: number,
  user_properties: {
    device_model: string,
    device_platform: string,
    device_os: string,
    telegram_id: string,
    // your data
  }
  event_properties: {
    name: string, 
    game_session_id: string
    // your data
  }
}

6. Payments

Next, the native payment method via Telegram Stars will be described. Other payment methods inside the telegram are not allowed.

requestPayment: ( { "amount": number, "description": string, "photoUrl?": string, "externalId": string } ) => void

  /**
  * constraints:
  * amount >= 1 and integer
  * description.length <= 255
  */

  requestPayment: ("amount": number, "description": string, "photoUrl?": string, "externalId": string) => { url: string }
const parent = window.parent.window;
parent.postMessage({ playdeck: { method: 'requestPayment', value: {"amount": number, 
                                                                   "description": string, 
                                                                   "photoUrl?": string, 
                                                                   "externalId": string} } },'*'); // Unique order identifier in your system, which you can use to check in postback that payment was


window.addEventListener('message', ({ data }) => {
  const playdeck = data?.playdeck;
  if (!playdeck) return;

  if (playdeck.method === 'requestPayment') {
    console.log(playdeck.value); // { url: 'https://t.me/$XIVLvBpfOEsBBwAARs....' } // payment link
  }
});

How to open links is described in the chapter Sharing and opening links.

You can also track the payment using the getPaymentInfo or invoiceClosed methods.

invoiceClosed: () => { status: 'paid' | 'cancelled' | 'failed' | 'pending' }

Payment tracking via the telegram client. As soon as there is a change in the payment process, the variable update will be sent to the game. You cannot call it manually.

const parent = window.parent.window;

window.addEventListener('message', ({ data }) => {
  const playdeck = data?.playdeck;
  if (!playdeck) return;

  if (playdeck.method === 'invoiceClosed') {
    console.log(playdeck.value); // { status: 'paid' | 'cancelled' | 'failed' | 'pending' }
  }
});

If you open two payment screens by mistake, the results will always be "cancelled".

getPaymentInfo: ( { externalId: string } ) => { paid: boolean, telegramId: number | null, datetime: number | null; }

Get the payment status via our backend. It is performed using the externalId that was used at the time the payment was created.

const parent = window.parent.window;

parent.postMessage({ playdeck: { method: "getPaymentInfo", value: { "externalId": string } } },'*');

window.addEventListener('message', ({ data }) => {
  const playdeck = data?.playdeck;
  if (!playdeck) return;

  if (playdeck.method === 'getPaymentInfo') {
    console.log(playdeck.value); // PaymentInfo
  }
});

The PaymentInfo object looks like this:

interface PaymentInfo {
  paid: boolean;
  telegramId: number | null;
  datetime: number | null;
  amount: number;
}

If externalId order was paid, than paid=true, telegramId=payerTgId, datetime=payTime. Otherwise paid=false, telegramId=null, datetime=null