앱에 소매 데모(RDX) 기능 추가

매장에서 PC 및 디바이스를 사용해 보려는 고객이 바로 시작할 수 있도록 Windows 앱에 소매 데모 모드를 포함하세요.

고객이 소매점에 있으면 PC와 디바이스의 데모를 사용해 볼 수 있는 것으로 간주됩니다. RDX(소매 데모 환경)를 통해 앱에서 재생하는 데 상당한 시간이 걸리는 경우가 많습니다.

표준 또는 소매 모드에서 다양한 환경을 제공하도록 앱을 설정할 수 있습니다. 예를 들어, 앱이 설정 프로세스로 시작하는 경우, 정품 모드에서는 이를 건너뛸 수 있으며, 샘플 데이터 및 기본 설정을 사용하여 앱을 미리 채워서 바로 시작하도록 할 수 있습니다.

고객의 관점에서 앱은 하나뿐입니다. 고객이 두 모드를 구분할 수 있도록 앱이 정품 모드일 때 제목 표시줄 또는 적합한 위치에 "정품" 단어를 눈에 띄게 표시하는 것이 좋습니다.

RDX 인식 앱이 소매점에서 고객이 일관되게 긍정적인 경험을 하도록 보장하려면, 앱에 대한 Microsoft Store 요구 사항 외에도 RDX, 설정, 정리 및 업데이트 프로세스와 호환되어야 합니다.

설계 원칙

  • 최고를 표시. 소매 데모 환경을 사용하여 앱의 뛰어난 점을 보여 줍니다. 고객이 앱을 처음 접하는 것일 수 있으므로 최고를 보여 주는 것이 중요합니다.

  • 빠르게 표시. 고객은 인내심이 없을 수 있습니다. 사용자가 앱의 실제 가치를 더 빠르게 경험할수록 좋습니다.

  • 스토리를 간단하게 유지. 소매 데모 환경은 앱의 가치를 요약해서 보여 줍니다.

  • 경험에 집중. 사용자에게 콘텐츠를 이해할 시간을 줍니다. 가장 좋은 부분을 빠르게 파악하는 것이 중요하지만 적절한 일시 중지를 디자인하면 경험을 완전히 즐기는 데 도움이 될 수 있습니다.

기술 요구 사항

RDX 인식 앱은 소매 고객에게 앱의 최고 기능을 선보이려는 것이므로 다음과 같은 기술 요구 사항을 충족하는 것은 물론 Microsoft Store의 모든 소매 데모 환경 앱에 관한 개인 정보 보호 규정을 준수해야 합니다.

이러한 요구 사항은 유효성 검사 프로세스를 준비하고 테스트 프로세스에서 명확성을 제공할 수 있도록 검사 목록으로서 사용될 수 있습니다. 앱이 소매 데모 디바이스에서 계속 실행되는 한 이러한 요구 사항은 유효성 검사 프로세스뿐만 아니라 소매 데모 환경 앱의 전체 수명 동안 유지되어야 합니다.

중요 요구 사항

이러한 중요한 요구 사항을 충족하지 않는 RDX 인식 앱은 모든 소매 데모 디바이스에서 가능한 한 빨리 제거됩니다.

  • PII(개인 식별이 가능한 정보)를 요청하지 않음. 여기에는 로그인 정보, Microsoft 계정 정보 또는 연락처 정보가 포함됩니다.

  • 오류 없는 환경. 앱은 오류 없이 실행되어야 합니다. 또한 소매 데모 디바이스를 사용하는 고객에게 오류 팝업이나 알림을 표시해서는 안 됩니다. 오류는 앱 자체, 브랜드, 디바이스의 브랜드, 디바이스의 제조업체 브랜드 및 Microsoft 브랜드에 부정적인 영향을 주지 않습니다.

  • 유료 앱에는 평가판 모드가 있어야 함. 앱은 무료이거나 평가판 모드를 포함해야 합니다. 고객은 소매점에서 경험 비용을 지불하려고 하지 않습니다.

높은 우선 순위 요구 사항

이러한 우선 순위가 높은 요구 사항을 충족하지 않는 RDX 인식 앱은 즉시 조사하여 수정해야 합니다. 즉각적인 수정 사항이 없는 경우 이 앱은 모든 소매 데모 디바이스에서 제거될 수 있습니다.

  • 기억에 남는 오프라인 경험. 소매점에서는 디바이스의 50% 정도가 오프라인 상태이므로 앱은 뛰어난 오프라인 경험을 제공해야 합니다. 이는 오프라인에서 앱과 상호 작용하는 고객이 여전히 의미 있고 긍정적인 환경을 가질 수 있도록 하기 위한 것입니다.

  • 업데이트된 콘텐츠 환경. 온라인 상태에서 앱을 업데이트하라는 메시지를 표시해서는 안 됩니다. 업데이트가 필요한 경우 자동으로 수행되어야 합니다.

  • 익명 통신 없음. 소매 데모 디바이스를 사용하는 고객은 익명의 사용자이므로 메시지를 주고받거나 디바이스의 콘텐츠를 공유할 수 없습니다.

  • 정리 프로세스를 사용하여 일관된 환경 제공. 모든 고객은 소매 데모 디바이스까지 걸어갈 때 동일한 환경을 가져야 합니다. 앱은 사용이 끝날 때마다 정리 프로세스를 사용하여 동일한 기본 상태로 돌아가야 합니다. 다음 고객이 남은 최종 고객을 확인하도록 하는 것은 바람직하지 않습니다. 여기에는 점수판, 도전 과제 및 잠금 해제가 포함됩니다.

  • 연령에 맞는 콘텐츠. 모든 앱 콘텐츠에는 청소년 이하의 평점 범주를 할당해야 합니다. 자세히 알아보려면 IARC에서 앱 평점 받기ESRB 평점을 참조하세요.

보통 우선 순위 요구 사항

Windows Retail Store 팀은 개발자에게 직접 연락하여 이러한 문제를 해결하는 방법에 대한 논의를 설정할 수 있습니다.

  • 다양한 디바이스에서 성공적으로 실행하는 기능. 앱은 저급 사양의 디바이스를 비롯한 모든 디바이스에서 잘 실행되어야 합니다. 최소 사양이 충족되지 않는 디바이스에 앱을 설치하는 경우, 앱에서 이 문제에 대해 사용자에게 분명히 알려야 합니다. 앱이 항상 고성능으로 실행될 수 있도록 최소 디바이스 요구 사항을 알려야 합니다.

  • 소매 스토어 앱 크기 요구 사항 충족. 앱은 800MB보다 작아야 합니다. RDX 인식 앱이 크기 요건을 충족하지 않는 경우 추가 설명이 필요하면 Windows Retail Store 팀에 직접 문의하세요.

RetailInfo API: 데모 모드를 위한 코드 준비

IsDemoModeEnabled

The Windows 10 및 Windows 11 SDK의 Windows.System.Profile 네임스페이스의 일부인 RetailInfo 유틸리티 클래스에 있는 IsDemoModeEnabled 속성이 앱이 실행되는 코드 경로를 지정하는 부울 표시기로 사용됩니다(일반 모드 또는 소매 모드).

using Windows.Storage;

StorageFolder folder = ApplicationData.Current.LocalFolder;

if (Windows.System.Profile.RetailInfo.IsDemoModeEnabled) 
{
    // Use the demo specific directory
    folder = await folder.GetFolderAsync("demo");
}

StorageFile file = await folder.GetFileAsync("hello.txt");
// Now read from file
using namespace Windows::Storage;

StorageFolder^ localFolder = ApplicationData::Current->LocalFolder;

if (Windows::System::Profile::RetailInfo::IsDemoModeEnabled) 
{
    // Use the demo specific directory
    create_task(localFolder->GetFolderAsync("demo").then([this](StorageFolder^ demoFolder)
    {
        return demoFolder->GetFileAsync("hello.txt");
    }).then([this](task<StorageFile^> fileTask)
    {
        StorageFile^ file = fileTask.get();
    });
    // Do something with file
}
else
{
    create_task(localFolder->GetFileAsync("hello.txt").then([this](StorageFile^ file)
    {
        // Do something with file
    });
}
if (Windows.System.Profile.retailInfo.isDemoModeEnabled) {
    console.log("Retail mode is enabled.");
} else {
    Console.log("Retail mode is not enabled.");
}

RetailInfo.Properties

IsDemoModeEnabled가 true를 반환하면, 보다 사용자 지정 가능한 소매 데모 환경을 구축하기 위해 RetailInfo.Properties를 사용하여 디바이스에 대한 속성 집합을 쿼리할 수 있습니다. 이러한 속성에는 ManufacturerName, Screensize, Memory 등이 포함됩니다.

using Windows.UI.Xaml.Controls;
using Windows.System.Profile

TextBlock priceText = new TextBlock();
priceText.Text = RetailInfo.Properties[KnownRetailInfo.Price];
// Assume infoPanel is a StackPanel declared in XAML
this.infoPanel.Children.Add(priceText);
using namespace Windows::UI::Xaml::Controls;
using namespace Windows::System::Profile;

TextBlock ^manufacturerText = ref new TextBlock();
manufacturerText.set_Text(RetailInfo::Properties[KnownRetailInfoProperties::Price]);
// Assume infoPanel is a StackPanel declared in XAML
this->infoPanel->Children->Add(manufacturerText);
var pro = Windows.System.Profile;
console.log(pro.retailInfo.properties[pro.KnownRetailInfoProperties.price);

IDL

//  Copyright (c) Microsoft Corporation. All rights reserved.
//
//  WindowsRuntimeAPISet

import "oaidl.idl";
import "inspectable.idl";
import "Windows.Foundation.idl";
#include <sdkddkver.h>

namespace Windows.System.Profile
{
    runtimeclass RetailInfo;
    runtimeclass KnownRetailInfoProperties;

    [version(NTDDI_WINTHRESHOLD), uuid(0712C6B8-8B92-4F2A-8499-031F1798D6EF), exclusiveto(RetailInfo)]
    [version(NTDDI_WINTHRESHOLD, Platform.WindowsPhone)]
    interface IRetailInfoStatics : IInspectable
    {
        [propget] HRESULT IsDemoModeEnabled([out, retval] boolean *value);
        [propget] HRESULT Properties([out, retval, hasvariant] Windows.Foundation.Collections.IMapView<HSTRING, IInspectable *> **value);
    }

    [version(NTDDI_WINTHRESHOLD), uuid(50BA207B-33C4-4A5C-AD8A-CD39F0A9C2E9), exclusiveto(KnownRetailInfoProperties)]
    [version(NTDDI_WINTHRESHOLD, Platform.WindowsPhone)]
    interface IKnownRetailInfoPropertiesStatics : IInspectable
    {
        [propget] HRESULT RetailAccessCode([out, retval] HSTRING *value);
        [propget] HRESULT ManufacturerName([out, retval] HSTRING *value);
        [propget] HRESULT ModelName([out, retval] HSTRING *value);
        [propget] HRESULT DisplayModelName([out, retval] HSTRING *value);
        [propget] HRESULT Price([out, retval] HSTRING *value);
        [propget] HRESULT IsFeatured([out, retval] HSTRING *value);
        [propget] HRESULT FormFactor([out, retval] HSTRING *value);
        [propget] HRESULT ScreenSize([out, retval] HSTRING *value);
        [propget] HRESULT Weight([out, retval] HSTRING *value);
        [propget] HRESULT DisplayDescription([out, retval] HSTRING *value);
        [propget] HRESULT BatteryLifeDescription([out, retval] HSTRING *value);
        [propget] HRESULT ProcessorDescription([out, retval] HSTRING *value);
        [propget] HRESULT Memory([out, retval] HSTRING *value);
        [propget] HRESULT StorageDescription([out, retval] HSTRING *value);
        [propget] HRESULT GraphicsDescription([out, retval] HSTRING *value);
        [propget] HRESULT FrontCameraDescription([out, retval] HSTRING *value);
        [propget] HRESULT RearCameraDescription([out, retval] HSTRING *value);
        [propget] HRESULT HasNfc([out, retval] HSTRING *value);
        [propget] HRESULT HasSdSlot([out, retval] HSTRING *value);
        [propget] HRESULT HasOpticalDrive([out, retval] HSTRING *value);
        [propget] HRESULT IsOfficeInstalled([out, retval] HSTRING *value);
        [propget] HRESULT WindowsVersion([out, retval] HSTRING *value);
    }

    [version(NTDDI_WINTHRESHOLD), static(IRetailInfoStatics, NTDDI_WINTHRESHOLD)]
    [version(NTDDI_WINTHRESHOLD, Platform.WindowsPhone), static(IRetailInfoStatics, NTDDI_WINTHRESHOLD, Platform.WindowsPhone)]
    [threading(both)]
    [marshaling_behavior(agile)]
    runtimeclass RetailInfo
    {
    }

    [version(NTDDI_WINTHRESHOLD), static(IKnownRetailInfoPropertiesStatics, NTDDI_WINTHRESHOLD)]
    [version(NTDDI_WINTHRESHOLD, Platform.WindowsPhone), static(IKnownRetailInfoPropertiesStatics, NTDDI_WINTHRESHOLD, Platform.WindowsPhone)]
    [threading(both)]
    [marshaling_behavior(agile)]
    runtimeclass KnownRetailInfoProperties
    {
    }
}

정리 프로세스

쇼핑객이 디바이스와의 상호 작용을 멈춘 후 2분 후에 정리가 시작됩니다. 소매 데모가 재생되고 Windows가 연락처, 사진 및 기타 앱에서 샘플 데이터를 다시 설정하기 시작합니다. 디바이스에 따라 모든 것을 정상으로 완전히 다시 설정하는 데 1~5분 정도 걸릴 수 있습니다. 이렇게 하면 소매점의 모든 고객이 디바이스로 와서 디바이스와 상호 작용할 때 동일한 환경을 사용할 수 있습니다.

1단계: 정리

  • 모든 Win32 및 스토어 앱이 닫힙니다.
  • 사진, 비디오, 음악, 문서, SavedPictures, CameraRoll, 데스크톱다운로드와 같은 알려진 폴더의 모든 파일이 삭제됩니다.
  • 구조화되지 않은 로밍 상태와 구조화된 로밍 상태가 삭제됨
  • 구조적 로컬 상태가 삭제됨

2단계: 설정

  • 오프라인 디바이스의 경우: 폴더가 빈 상태로 유지됨
  • 온라인 디바이스: Microsoft Store에서 디바이스로 소매 데모 자산이 푸시될 수 있습니다.

사용자 세션 간에 데이터 저장

사용자 세션 전체에서 데이터를 저장하려면 ApplicationData.Current.TemporaryFolder에 정보를 저장할 수 있습니다. 기본 정리 프로세스에서 이 폴더의 데이터는 자동으로 삭제되지 않기 때문입니다. LocalState를 사용하여 저장한 정보는 정리 프로세스 중에 삭제됩니다.

정리 프로세스 사용자 지정

정리 프로세스를 사용자 지정하려면 Microsoft-RetailDemo-Cleanup 앱 서비스를 앱에 구현합니다.

사용자 지정 정리 논리가 필요한 시나리오에는 설정에 비용이 많이 드는 경우, 데이터를 다운로드하여 캐시하는 경우 또는 LocalState 데이터의 삭제를 원하는 않는 경우가 포함됩니다.

1단계: 앱 매니페스트에서 Microsoft-RetailDemo-Cleanup 서비스를 선언합니다.

  <Applications>
      <Extensions>
        <uap:Extension Category="windows.appService" EntryPoint="MyCompany.MyApp.RDXCustomCleanupTask">
          <uap:AppService Name="Microsoft-RetailDemo-Cleanup" />
        </uap:Extension>
      </Extensions>
   </Application>
  </Applications>

2단계: 아래의 샘플 템플릿을 사용하여 AppdataCleanup 사례 함수 아래에 사용자 지정 정리 논리를 구현합니다.

using System;
using System.IO;
using System.Runtime.Serialization.Json;
using System.Threading;
using System.Threading.Tasks;
using Windows.ApplicationModel.AppService;
using Windows.ApplicationModel.Background;
using Windows.Foundation.Collections;
using Windows.Storage;

namespace MyCompany.MyApp
{
    public sealed class RDXCustomCleanupTask : IBackgroundTask
    {
        BackgroundTaskCancellationReason _cancelReason = BackgroundTaskCancellationReason.Abort;
        BackgroundTaskDeferral _deferral = null;
        IBackgroundTaskInstance _taskInstance = null;
        AppServiceConnection _appServiceConnection = null;

        const string MessageCommand = "Command";

        public void Run(IBackgroundTaskInstance taskInstance)
        {
            // Get the deferral object from the task instance, and take a reference to the taskInstance;
            _deferral = taskInstance.GetDeferral();
            _taskInstance = taskInstance;
            _taskInstance.Canceled += new BackgroundTaskCanceledEventHandler(OnCanceled);

            AppServiceTriggerDetails appService = _taskInstance.TriggerDetails as AppServiceTriggerDetails;
            if ((appService != null) && (appService.Name == "Microsoft-RetailDemo-Cleanup"))
            {
                _appServiceConnection = appService.AppServiceConnection;
                _appServiceConnection.RequestReceived += _appServiceConnection_RequestReceived;
                _appServiceConnection.ServiceClosed += _appServiceConnection_ServiceClosed;
            }
            else
            {
                _deferral.Complete();
            }
        }

        void _appServiceConnection_ServiceClosed(AppServiceConnection sender, AppServiceClosedEventArgs args)
        {
        }

        async void _appServiceConnection_RequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
        {
            //Get a deferral because we will be calling async code
            AppServiceDeferral requestDeferral = args.GetDeferral();
            string command = null;
            var returnData = new ValueSet();

            try
            {
                ValueSet message = args.Request.Message;
                if (message.ContainsKey(MessageCommand))
                {
                    command = message[MessageCommand] as string;
                }

                if (command != null)
                {
                    switch (command)
                    {
                        case "AppdataCleanup":
                            {
                                // Do custom clean up logic here
                                break;
                            }
                    }
                }
            }
            catch (Exception e)
            {
            }
            finally
            {
                requestDeferral.Complete();
                // Also release the task deferral since we only process one request per instance.
                _deferral.Complete();
            }
        }

        private void OnCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason)
        {
            _cancelReason = reason;
        }
    }
}