콘텐츠로 이동

앱 서비스를 위한 웹페이지 구성

이 가이드는 앱 개발사가 자체 구축한 웹페이지에서 크로스플레이 런처를 사용해 앱을 서비스할 때, 웹페이지에서 필수적으로 구현해야할 내용을 예시를 사용해 안내합니다

앱 사용자는 크로스플레이 런처를 사용해 크게 아래와 같은 과정으로 앱을 설치, 실행합니다.

  1. 앱을 서비스할 웹페이지에서 앱 실행 버튼을 누르고 크로스플레이 런처를 다운받아 설치
  2. 크로스플레이 런처를 실행
  3. 크로스플레이 런처가 자동으로 앱을 다운받고 설치
  4. 크로스플레이 런처가 자동으로 앱을 업데이트, 실행

앱 서비스 전체 과정

다음은 크로스플레이 런처를 사용해 웹페이지에서 앱을 서비스하는 전체 과정입니다.

앱을 서비스할 웹페이지는 다음 두 가지 방식으로 제공할 수 있습니다.

  1. 하이브 게임즈(https://withhive.com/)에서 서비스
  2. 앱 개발사가 자체 구축한 웹페이지에서 서비스

이 가이드는 **앱 개발사가 자체 구축한 웹페이지**에서 크로스플레이 런처를 사용해 앱을 서비스할 때, 웹페이지에서 필수적으로 구현해야할 내용을 예시를 사용해 안내합니다.

앱 서비스 조건 확인

사용자가 웹페이지에 접속했을 때, 사용자 PC OS 버전 등 PC 환경을 지원하는지 확인합니다.

  • 지원 OS: Windows 10 이상

조건을 확인하여 문제가 있다면, 사용자에게 오류 팝업을 노출해야 합니다.

앱 실행 버튼과 크로스플레이 런처 다운로드 팝업창 구현

웹페이지에 앱 실행 버튼을 구현합니다.

 

크로스플레이 런처 설치 파일을 다운로드하는 팝업창을 구현합니다. 팝업창은 크로스플레이 런처 설치 안내 메세지를 포함해야 합니다.

 

웹페이지에서 앱 실행 버튼을 누르면 팝업창이 나타나도록 구현합니다. 팝업창에는 크로스플레이 런처 설치 파일 다운로드 버튼이 있어야 합니다.

크로스플레이 런처 설치 여부 확인

웹페이지에서 사용자가 앱 실행 버튼을 눌렀을 때, 사용자 PC에 크로스플레이 런처가 설치되어 있는지를 확인해야 합니다. 웹페이지에서 아래 JavaScript 코드를 실행하여 사용자 PC에 크로스플레이 런처가 설치되어 있는지 여부를 확인합니다.

JavaScript Code
!function (b, a) {
"object" == typeof exports && "object" == typeof module
? module.exports = a()
: "function" == typeof define && define.amd
    ? define("customProtocolCheck", [], a)
    : "object" == typeof exports
        ? exports.customProtocolCheck = a()
        : b.customProtocolCheck = a()
}(window, function () {
return function (b) {
var c = {};
function a(d) {
    if (c[d]) 
        return c[d].exports;
    var e = c[d] = {
        i: d,
        l: !1,
        exports: {}
    };
    return b[d].call(e.exports, e, e.exports, a),
    e.l = !0,
    e.exports
}
return a.m = b,
a.c = c,
a.d = function (b, c, d) {
    a.o(b, c) || Object.defineProperty(b, c, {
        enumerable: !0,
        get: d
    })
},
a.r = function (a) {
    "undefined" != typeof Symbol && Symbol.toStringTag && Object.defineProperty(
        a,
        Symbol.toStringTag,
        {value: "Module"}
    ),
    Object.defineProperty(a, "__esModule", {
        value: !0
    })
},
a.t = function (b, c) {
    if (
        1 & c && (b = a(b)),
        8 & c || 4 & c && "object" == typeof b && b && b.__esModule
    ) 
        return b;
    var d = Object.create(null);
    if (a.r(d), Object.defineProperty(d, "default", {
        enumerable: !0,
        value: b
    }), 2 & c && "string" != typeof b) 
        for (var e in b) 
            a.d(d, e, (function (a) {
                return b[a]
            }).bind(null, e));
return d
},
a.n = function (c) {
    var b = c && c.__esModule
        ? function () {
            return c.default
        }
        : function () {
            return c
        };
    return a.d(b, "a", b),
    b
},
a.o = function (a, b) {
    return Object
        .prototype
        .hasOwnProperty
        .call(a, b)
},
a.p = "",
a(a.s = 0)
}({
"./index.js": function (module, exports) {
    eval(
        'var browser = {\n  getUserAgent: function getUserAgent() {\n    return window.' +
        'navigator.userAgent;\n  },\n  userAgentContains: function userAgentContains(br' +
        'owserName) {\n    browserName = browserName.toLowerCase();\n    return this.ge' +
        'tUserAgent().toLowerCase().indexOf(browserName) > -1;\n  },\n  isOSX: function' +
        ' isOSX() {\n    return this.userAgentContains("Macintosh");\n  },\n  isFirefox' +
        ': function isFirefox() {\n    return this.userAgentContains("firefox");\n  },' +
        '\n  isInternetExplorer: function isInternetExplorer() {\n    return this.userA' +
        'gentContains("trident");\n  },\n\n  /**\r\n   * Detects IE 11 and older\r\n   ' +
        '* @return {Boolean} Returns true when IE 11 and older\r\n   */\n  isIE: functi' +
        'on isIE() {\n    var ua = this.getUserAgent().toLowerCase(); // Test values.\n' +
        '    // Uncomment to check result\n    // IE 10\n    // ua = \'Mozilla/5.0 (com' +
        'patible; MSIE 10.0; Windows NT 6.2; Trident/6.0)\';\n    // IE 11\n    // ua =' +
        ' \'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko/20100101 Fire' +
        'fox/12.0\';\n\n    var msie = ua.indexOf("msie");\n\n    if (msie > 0) {\n    ' +
        '  // IE 10 or older\n      return true;\n    }\n\n    var trident = ua.indexOf' +
        '("trident/");\n\n    if (trident > 0) {\n      // IE 11\n      return true;\n ' +
        '   } // other browser\n\n\n    return false;\n  },\n  isEdge: function isEdge(' +
        ') {\n    var ua = this.getUserAgent().toLowerCase(); // Test values.\n    // U' +
        'ncomment to check result\n    // Edge\n    // ua = \'Mozilla/5.0 (Windows NT 1' +
        '0.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 S' +
        'afari/537.36 Edge/12.10240\';\n\n    var edge = ua.indexOf("edge");\n\n    if ' +
        '(edge > 0) {\n      return true;\n    }\n\n    return false;\n  },\n  isChrome' +
        ': function isChrome() {\n    // IE11 returns undefined for window.chrome\n    ' +
        '// and new Opera 30 outputs true for window.chrome\n    // but needs to check ' +
        'if window.opr is not undefined\n    // and new IE Edge outputs to true for win' +
        'dow.chrome\n    // and if not iOS Chrome check\n    var isChromium = window.ch' +
        'rome;\n    var winNav = window.navigator;\n    var vendorName = winNav.vendor;' +
        '\n    var isOpera = typeof window.opr !== "undefined";\n    var isIEedge = win' +
        'Nav.userAgent.indexOf("Edge") > -1;\n    var isIOSChrome = winNav.userAgent.ma' +
        'tch("CriOS");\n    return isChromium !== null && typeof isChromium !== "undefi' +
        'ned" && vendorName === "Google Inc." && isOpera === false && isIEedge === fals' +
        'e || isIOSChrome;\n  },\n  isWhale: function isWhale() {\n    // IE11 returns ' +
        'undefined for window.chrome\n    // and new Opera 30 outputs true for window.c' +
        'hrome\n    // but needs to check if window.opr is not undefined\n    // and ne' +
        'w IE Edge outputs to true for window.chrome\n    // and if not iOS Chrome chec' +
        'k\n    var isChromium = window.chrome;\n    var winNav = window.navigator;\n  ' +
        '  var vendorName = winNav.vendor;\n    var isOpera = typeof window.opr !== "un' +
        'defined";\n    var isIEedge = winNav.userAgent.indexOf("Edge") > -1;\n    var ' +
        'isIOSChrome = winNav.userAgent.match("CriOS");\n    return isChromium !== null' +
        ' && typeof isChromium !== "undefined" && vendorName === "NAVER Corp." && isOpe' +
        'ra === false && isIEedge === false || isIOSChrome;\n  },\n  isOpera: function ' +
        'isOpera() {\n    return this.userAgentContains(" OPR/");\n  }\n};\nvar DEFAULT' +
        '_CUSTOM_PROTOCOL_FAIL_CALLBACK_TIMEOUT;\n\nvar registerEvent = function regist' +
        'erEvent(target, eventType, cb) {\n  if (target.addEventListener) {\n    target' +
        '.addEventListener(eventType, cb);\n    return {\n      remove: function remove' +
        '() {\n        target.removeEventListener(eventType, cb);\n      }\n    };\n  }' +
        ' else {\n    target.attachEvent(eventType, cb);\n    return {\n      remove: f' +
        'unction remove() {\n        target.detachEvent(eventType, cb);\n      }\n    }' +
        ';\n  }\n};\n\nvar createHiddenIframe = function createHiddenIframe(target, uri' +
        ') {\n  var iframe = document.createElement("iframe");\n  iframe.src = uri;\n  ' +
        'iframe.id = "hiddenIframe";\n  iframe.style.display = "none";\n  target.append' +
        'Child(iframe);\n  return iframe;\n};\n\nvar openUriWithHiddenFrame = function ' +
        'openUriWithHiddenFrame(uri, failCb, successCb) {\n  var timeout = setTimeout(f' +
        'unction () {\n    failCb();\n    handler.remove();\n  }, DEFAULT_CUSTOM_PROTOC' +
        'OL_FAIL_CALLBACK_TIMEOUT);\n  var iframe = document.querySelector("#hiddenIfra' +
        'me");\n\n  if (!iframe) {\n    iframe = createHiddenIframe(document.body, "abo' +
        'ut:blank");\n  }\n\n  onBlur = function onBlur() {\n    clearTimeout(timeout);' +
        '\n    handler.remove();\n    successCb();\n  };\n\n  var handler = registerEve' +
        'nt(window, "blur", onBlur);\n  iframe.contentWindow.location.href = uri;\n};\n' +
        '\nvar openUriWithTimeoutHack = function openUriWithTimeoutHack(uri, failCb, su' +
        'ccessCb) {\n  var timeout = setTimeout(function () {\n    failCb();\n    handl' +
        'er.remove();\n  }, DEFAULT_CUSTOM_PROTOCOL_FAIL_CALLBACK_TIMEOUT); //handle pa' +
        'ge running in an iframe (blur must be registered with top level window)\n\n  v' +
        'ar target = window;\n\n  while (target.parent && target != target.parent) {\n ' +
        '   target = target.parent;\n  }\n\n  onBlur = function onBlur() {\n    clearTi' +
        'meout(timeout);\n    handler.remove();\n    successCb();\n  };\n\n  var handle' +
        'r = registerEvent(target, "blur", onBlur);\n  window.location = uri;\n};\n\nva' +
        'r openUriUsingFirefox = function openUriUsingFirefox(uri, failCb, successCb) {' +
        '\n  var iframe = document.querySelector("#hiddenIframe");\n\n  if (!iframe) {' +
        '\n    iframe = createHiddenIframe(document.body, "about:blank");\n  }\n\n  try' +
        ' {\n    iframe.contentWindow.location.href = uri;\n    successCb();\n  } catch' +
        ' (e) {\n    if (e.name == "NS_ERROR_UNKNOWN_PROTOCOL") {\n      failCb();\n   ' +
        ' }\n  }\n};\n\nvar openUriWithMsLaunchUri = function openUriWithMsLaunchUri(ur' +
        'i, failCb, successCb) {\n  navigator.msLaunchUri(uri, successCb, failCb);\n};' +
        '\n\nvar getBrowserVersion = function getBrowserVersion() {\n  var ua = window.' +
        'navigator.userAgent;\n  var tem,\n      M = ua.match(/(opera|chrome|safari|fir' +
        'efox|msie|trident(?=\\/))\\/?\\s*(\\d+)/i) || [];\n\n  if (/trident/i.test(M[1' +
        '])) {\n    tem = /\\brv[ :]+(\\d+)/g.exec(ua) || [];\n    return parseFloat(te' +
        'm[1]) || "";\n  }\n\n  if (M[1] === "Chrome") {\n    tem = ua.match(/\\b(OPR|E' +
        'dge)\\/(\\d+)/);\n\n    if (tem != null) {\n      return parseFloat(tem[2]);\n' +
        '    }\n  }\n\n  M = M[2] ? [M[1], M[2]] : [window.navigator.appName, window.na' +
        'vigator.appVersion, "-?"];\n  if ((tem = ua.match(/version\\/(\\d+)/i)) != nul' +
        'l) M.splice(1, 1, tem[1]);\n  return parseFloat(M[1]);\n};\n\nvar protocolChec' +
        'k = function protocolCheck(uri, failCb, successCb) {\n  var timeout = argument' +
        's.length > 3 && arguments[3] !== undefined ? arguments[3] : 2000;\n  var unsup' +
        'portedCb = arguments.length > 4 ? arguments[4] : undefined;\n\n  var failCallb' +
        'ack = function failCallback() {\n    failCb && failCb();\n  };\n\n  var succes' +
        'sCallback = function successCallback() {\n    successCb && successCb();\n  };' +
        '\n\n  var unsupportedCallback = function unsupportedCallback() {\n    unsuppor' +
        'tedCb && unsupportedCb();\n  };\n\n  var openUri = function openUri() {\n    i' +
        'f (browser.isFirefox()) {\n      var browserVersion = getBrowserVersion();\n\n' +
        '      if (browserVersion >= 64) {\n        openUriWithHiddenFrame(uri, failCal' +
        'lback, successCallback);\n      } else {\n        openUriUsingFirefox(uri, fai' +
        'lCallback, successCallback);\n      }\n    } else if (browser.isWhale()) {\n  ' +
        '    openUriWithTimeoutHack(uri, failCallback, successCallback);\n    } else if' +
        ' (browser.isChrome()) {\n      openUriWithTimeoutHack(uri, failCallback, succe' +
        'ssCallback);\n    } else if (browser.isOSX()) {\n      openUriWithHiddenFrame(' +
        'uri, failCallback, successCallback);\n    } else {\n      //not supported, imp' +
        'lement please\n      unsupportedCallback();\n    }\n  };\n\n  if (timeout) {\n' +
        '    DEFAULT_CUSTOM_PROTOCOL_FAIL_CALLBACK_TIMEOUT = timeout;\n  }\n\n  if (bro' +
        'wser.isEdge() || browser.isIE()) {\n    //for IE and Edge in Win 8 and Win 10' +
        '\n    openUriWithMsLaunchUri(uri, failCb, successCb);\n  } else {\n    if (doc' +
        'ument.hasFocus()) {\n      openUri();\n    } else {\n      var focusHandler = ' +
        'registerEvent(window, "focus", function () {\n        focusHandler.remove();\n' +
        '        openUri();\n      });\n    }\n  }\n};\n\nmodule.exports = protocolChec' +
        'k;\n\n//# sourceURL=webpack://customProtocolCheck/./index.js?'
    )
},
0: function (module, exports, __webpack_require__) {
    eval(
        'module.exports = __webpack_require__(/*! /Users/shahv/Viresh/work/rnd/custom-p' +
        'rotocol-check/index.js */"./index.js");\n\n\n//# sourceURL=webpack://customPro' +
        'tocolCheck/multi_./index.js?'
    )
}
})
})

사용자 PC에 크로스플레이 런처가 설치되지 않았다면, 크로스플레이 런처와 앱을 함께 설치하도록 구현합니다. 크로스플레이 런처가 이미 설치된 상태라면, 크로스플레이 런처를 실행하도록 구현합니다.

크로스플레이 런처와 앱을 함께 설치

크로스플레이 런처를 설치하면, 크로스플레이 런처가 앱을 자동으로 설치하고 실행할 수 있습니다.

크로스플레이 런처 설치 파일 다운로드 이벤트: 설치 파일 다운로드

위 안내를 따라 앱 실행 버튼과 팝업창을 구현 후, 사용자가 팝업창에 있는 다운로드 버튼을 누르면 크로스플레이 런처 설치 파일인 hivecrossplay-fn.qpyou.cn/hivecrossplay/p/w/Installer.exe를 다운로드하도록 구현합니다.

크로스플레이 런처 설치 파일 다운로드 이벤트: 앱 자동 설치

사용자가 다운로드 버튼을 누르는 이벤트 발생 시 게임 자동 설치 URI를 사용자 PC 클립보드에 복사하도록 구현해야 합니다. 크로스플레이 런처를 설치한 후, 크로스플레이 런처는 자동으로 클립보드를 탐색하여 게임 자동 설치 URI를 찾습니다. 게임 자동 설치 URI를 찾으면, 크로스플레이 런처는 URI에 해당하는 앱을 사용자 PC에 설치합니다. 만약, 클립보드에 게임 자동 설치 URI가 없으면 사용자 PC에 크로스플레이 런처만 설치하는 것으로 설치 과정을 마무리합니다. 앱을 설치하려면 사용자는 다시 웹페이지로 돌아가 앱 실행 버튼을 눌러야 합니다. 자세한 내용은 크로스플레이 런처 실행을 참고하세요.

게임 자동 설치 URI 획득

Hive 콘솔(샌드박스 또는 상용) > 크로스플레이 런처 > 앱 관리 > 다운로드 설정 > 런처 설치/실행 URI에서 자동 설치 URI를 얻습니다.

# 예시
hivelauncher:?app_id=com.com2us.hivesdk.windows.microsoftstore.global.normal&start_point=9 f387268b8ea6c3e0016c8fd41562ed53d4c38338

URI를 클립보드에 복사

획득한 자동 설치 URI를 사용자 PC 클립보드 공간에 복사하도록 구현합니다.

크로스플레이 런처 실행

사용자 PC에 크로스플레이 런처가 설치된 상태에서, 웹페이지에서 앱 실행 버튼을 누르는 이벤트 발생 시 JavaScript 코드를 실행합니다. JavaScript 코드는 게임 실행 URI로 크로스플레이 런처를 실행하고, 크로스플레이 런처는 앱을 자동으로 설치 및 실행합니다.

실행 URI 획득

Hive 콘솔(샌드박스 또는 상용) > 크로스플레이 런처 > 앱 관리 > 다운로드 설정> 런처 설치/실행 URI에서 실행 URI를 얻습니다.

# 예시
hivelauncher:?app_id=com.com2us.hivesdk.windows.microsoftstore.global.normal&start_point=9

JavaScript 코드 구현과 실행

사용자 PC에 크로스플레이 런처가 설치된 상태에서, 웹페이지에서 앱 실행 버튼을 누르는 이벤트 발생 시 JavaScript 코드를 실행하도록 구현합니다.

    event.preventDefault
        ? event.preventDefault()
        : (event.returnValue = false);
    window.customProtocolCheck({
        # 게임 실행 URI를 실행하도록 구현합니다.
        # 게임 실행 URI는 크로스플레이 런처를 실행합니다. 크로스플레이 런처는 앱을 자동으로 설치  실행합니다.
    }, function (e) {
        console.log("FAIL");
        {
            # 크로스플레이 런처 실행 실패  처리
        }
    }, function (e) {
        console.log("SUCCESS");
    }, 2500, function (e) {
        console.log("Not Supported");
        {
            # 크로스플레이 런처 실행 요청  2500ms 이상 응답이 없을  처리
        }
    });

 

아래는 앱 실행 버튼이 "PC로 플레이" 버튼인 상황에서 이 버튼을 눌렀을 때 JavaScript 코드를 실행한 결과 예시입니다. 크로스플레이 런처가 실행되면서 크로스플레이 런처 자체를 업데이트하는 Crossplay Updater 실행 여부를 묻는 브라우저 알림이 나타납니다.

Note

JAVA URLEncoder.encode()와 JavaScript encodeURIComponent() 간 차이로, JAVA에서 앱 실행을 구현할 경우 replaceAll("\+", "%20") 작업이 필요합니다.