All Products
Search
Document Center

SuperApp:Kustomisasi UI Kontainer MiniApp

Last Updated:Mar 31, 2026

Kustomisasi UI Kontainer MiniApp

1 Latar Belakang & Tujuan

Kontainer MiniApp telah mengimplementasikan UI default, seperti tampilan otorisasi perangkat pengguna. Namun, beberapa klien mengharapkan penyesuaian sebagian atau seluruh UI default tersebut.

image.pngimage.pngimage.png

Dukungan bagi klien untuk menyesuaikan sebagian atau seluruh UI default tersedia melalui solusi berikut.

2 Arsitektur & Alur Secara Keseluruhan

image.jpeg

image.jpeg

3 Cara Mengembangkan

3.1 Skenario bawaan

3.1.1 Impor paket terkait

// in build.gradle

implementation "com.aliyun.emas.suite.foundation:mini-app-adapter:1.8.14"
implementation "com.aliyun.emas.suite.foundation:windvane-mini-app:1.8.14"

implementation "com.aliyun.emas.suite.foundation:mini-app-plugin-base:1.8.14"

// in podfile
pod 'EMASMiniAppAdapter', '1.1.4'
pod 'EMASWindVaneMiniApp', '1.2.5'
//if use bluetooth feature please pod WindVaneBluetooth
pod 'WindVaneBluetooth', '1.0.2'

3.1.2 Antarmuka UI dalam SDK

3.1.2.1 Di Android
public interface UIExtension {

   
    // Digunakan untuk membuat dialog 'More', lihat ‘figure 1’ pada catatan untuk contoh gaya
    public default Fragment createMoreDialogFragment(UIComponentParam uiComponentParam) {
        return  NoExistFragment.newInstance();
    }

    // Digunakan untuk membuat dialog 'Favorite', lihat ‘figure 2’ pada catatan untuk contoh gaya
    public default Fragment createFavoriteDialogFragment(UIComponentParam uiComponentParam) {
        return NoExistFragment.newInstance();
    }

    // Digunakan untuk membuat dialog 'Auth', lihat ‘figure 3’ pada catatan untuk contoh gaya
    public default Fragment createUserDeviceInfoAuthDialogFragment(UIComponentParam uiComponentParam) {
        return NoExistFragment.newInstance();
    }

    // Digunakan untuk membuat sub halaman 'Error', lihat ‘figure 5’ pada catatan untuk contoh gaya
    public default Fragment createErrorPageFragment(UIComponentParam uiComponentParam) {
        return NoExistFragment.newInstance();
    }

    // Digunakan untuk membuat sub halaman 'Slash', lihat ‘figure 6’ pada catatan untuk contoh gaya
    public default Fragment createSplashPageFragment(UIComponentParam uiComponentParam) {
        return NoExistFragment.newInstance();
    }

    // Digunakan untuk membuat dialog 'About US', lihat ‘figure 8’ pada catatan untuk contoh gaya
    public default Fragment createAboutUsDialogFragment(UIComponentParam uiComponentParam) {
        return NoExistFragment.newInstance();
    }
        // Digunakan untuk membuat sub halaman 'User Info Auth', lihat ‘figure 7’ pada catatan untuk contoh gaya
    public default Fragment createUserInfoAuthPageFragment(UIComponentParam uiComponentParam) {
        return NoExistFragment.newInstance();
    }

}
public class UIComponentParam {
    private UIResultCallBack uiResultCallBack;
    private Context context;
    private HashMap<String, Object> params;
  /**
  * uiResultCallBack: Data yang dikembalikan
  * data: Data yang dikirim
  * **/
    public UIComponentParam(UIResultCallBack uiResultCallBack, HashMap<String, Object> data, Context context) {
        this.uiResultCallBack = uiResultCallBack;
        this.data = data;
        this.context = context;
    }

    public UIResultCallBack getUiResultCallBack() {
        return uiResultCallBack;
    }

    public HashMap<String, Object> getParams() {
        return params;
    }

    public Context getContext() {
        return context;
    }
}
public interface UIResultCallBack{
    void onComplete(String data);
}
public class UIResultDataUtil {
private static JSONObject buildResultData(Object data, String msg, String code) {
        JSONObject returnData = new JSONObject();
        try {
            returnData.put("data", data != null ? data : EMPTY_DATA);
            returnData.put("msg", msg != null ? msg : EMPTY_DATA);
            returnData.put("code", code);
        } catch (JSONException e) {
            throw new IllegalArgumentException("JSON build failed", e);
        }
        return returnData;
    }

    public static String getResultData(JSONObject jsonObject, String msg, String code) {
        return buildResultData(jsonObject, msg, code).toString();
    }

    public static String getResultData(String data, String msg, String code) {
        return buildResultData(data, msg, code).toString();
    }

    public static String getResultData(String msg, String code) {
        return buildResultData(null, msg, code).toString();
    }

    public static String getResultData(String code) {
        return buildResultData(null, null, code).toString();
    }
}
public class UIResultCodeConstants {
    public static final String CANCEL = "CANCEL";
    public static final String CONFIRM = "CONFIRM";
    public static final String ERROR = "ERROR";
    public static final String RESTART_MINIAPP = "RESTART_MINIAPP";
    public static final String EXIT_MINIAPP = "EXIT_MINIAPP";
    public static final String OPEN_ABOUT="OPEN_ABOUT";
    public static final String OPEN_SETTING="OPEN_SETTING";
}
Cara menggunakan konstanta dalam format data Auth
  • CANCEL: Ketika pengguna mengklik tombol cancel pada halaman, atur code ke konstanta ini. SDK akan menutup jendela pop-up.

  • CONFIRM: Ketika pengguna mengklik tombol confirm pada halaman, atur code ke konstanta ini dan kembalikan informasi relevan di data. SDK akan menutup jendela pop-up.

  • ERROR: Ketika pemrosesan komponen gagal, atur code ke konstanta ini dan berikan pesan kesalahan di msg. SDK akan menutup jendela pop-up dan meneruskan pesan kesalahan ke JavaScript untuk dikembalikan ke miniapp.

  • RESTART_MINIAPP: Ketika pengguna mengklik tombol refresh/restart pada halaman miniapp, atur code ke konstanta ini. SDK akan membuka ulang dan memuat kembali miniapp.

  • EXIT_MINIAPP: Ketika pengguna mengklik tombol exit pada halaman miniapp, atur code ke konstanta ini. SDK akan menutup miniapp saat ini.

Cara menggunakan onComplete dan kode UIResultCodeConstants:

a. Di ErrorPageFragment:

Ketika pengguna mengklik tombol exit pada halaman miniapp, atur code ke EXIT_MINIAPP.

uiComponentParam.getUiResultCallBack().onComplete(UIResultDataUtil.getResultData(UIResultCodeConstants.EXIT_MINIAPP));

Ketika pengguna mengklik tombol refresh/restart pada halaman miniapp, atur code ke RESTART_MINIAPP.

uiComponentParam.getUiResultCallBack().onComplete(UIResultDataUtil.getResultData(UIResultCodeConstants.RESTART_MINIAPP));

b. Di UserDeviceInfoAuthDialogFragment:

uiComponentParam.params adalah sebagai berikut:
authorize: Parameter ini diteruskan ketika miniapp memanggil wv.authorize. Jika parameter metode wv.authorize berisi "scope" dan "scopes", "scopes" digunakan terlebih dahulu; jika tidak, parameter "scope" yang digunakan. SDK akan memeriksa status otorisasi sebelumnya yang diberikan oleh miniapp dan mengatur status otorisasi untuk scope yang diminta.

appId: ID miniapp

userId: userId

miniAppInfo: informasi meta miniapp.

/*Ini sesuai dengan struktur yang diperoleh dengan kunci ’authorize‘ dalam param di UIComponentParam
{
    "authorize": {"location": true/false, "camera": true/false},
    "appId": appId,
    "userId": userInfo.userId,
    "miniAppInfo": miniAppInfo
}
Ini sesuai dengan struktur yang diperoleh sesuai dengan kunci 'miniAppInfo' dalam param di UIComponentParam.
miniAppInfo: 
{
    "miniAppId": appId,
    "miniAppName": appName,
    "miniAppIcon": iconUrl,
    "miniAppVersion": version,
    "miniAppSlogan": slogan,
    "miniAppDescription": description
}
*/

    HashMap<String, Object> params = uiComponentParam.getParam();
    String authorizeData = (String) params.get("authorize");
    String appId = (String) params.get(MiniAppInfoConstants.MINI_APP_ID);
    String userId = (String) params.get(MiniAppInfoConstants.USER_ID);
    String miniAppInfo = (String) params.get("miniAppInfo");
     if (miniAppInfo != null) {
            try {
                JSONObject jsonObject = new JSONObject(miniAppInfo);
                String miniAppIcon = jsonObject.optString("miniAppIcon");
                String miniAppName = jsonObject.optString("miniAppName");
            } catch (JSONException e) {
                throw new RuntimeException(e);
            }
        }

Jika pengguna membatalkan otorisasi, data dalam onComplete harus diatur sebagai berikut:

uiComponentParam.getUiResultCallBack().onComplete(UIResultDataUtil.getResultData( UIResultCodeConstants.CANCEL));

Jika pengguna menolak otorisasi, data dalam onComplete harus diatur sebagai berikut:

uiComponentParam.getUiResultCallBack().onComplete(UIResultDataUtil.getResultData( "User refuses authorization request", UIResultCodeConstants.ERROR));

Jika pengguna menyetujui otorisasi, hasil otorisasi akan dikembalikan ke callback wv.authorize (menandai scope input sebagai true/false). Data dalam onComplete harus diatur sebagai berikut:

/*
finalJsObj:Format informasi sebagai berikut JsonObject:
{"location": true,"camera": false}
*Contoh:JSONObject finalJsObj=new JSONObject
*finalJsObj.put("file","true/false")
*/


uiComponentParam.getUiResultCallBack().onComplete(UIResultDataUtil.getResultData(finalJsObj, "", UIResultCodeConstants.CONFIRM));
c. Di UserMoreDialogFragment

uiComponentParam.params adalah sebagai berikut:

miniAppInfo: informasi meta miniapp.

appId: ID miniapp

userId: userId

miniAppUrl: URL halaman saat ini dari miniapp

/*Ini sesuai dengan struktur yang diperoleh dari param di UIComponentParam

{
    "miniAppUrl":"current page url" 
    "appId": appId,
    "userId": userInfo.userId,
    "miniAppInfo": miniAppInfo
}

Ini sesuai dengan struktur yang diperoleh sesuai dengan kunci 'miniAppInfo' 
dalam param di UIComponentParam.

miniAppInfo: 
{
    "miniAppId": appId,
    "miniAppName": appName,
    "miniAppIcon": iconUrl,
    "miniAppVersion": version,
    "miniAppSlogan": slogan,
    "miniAppDescription": description
}
*/

    HashMap<String, Object> params = uiComponentParam.getParam();
    String authorizeData = (String) params.get("miniAppUrl");
    String appId = (String) params.get(MiniAppInfoConstants.MINI_APP_ID);
    String userId = (String) params.get(MiniAppInfoConstants.USER_ID);
    String miniAppInfo = (String) params.get("miniAppInfo");
     if (miniAppInfo != null) {
            try {
                JSONObject jsonObject = new JSONObject(miniAppInfo);
                String miniAppIcon = jsonObject.optString("miniAppIcon");
                String miniAppName = jsonObject.optString("miniAppName");
            } catch (JSONException e) {
                throw new RuntimeException(e);
            }
        }

  1. Tombol "add/remove" Favorite: Jika fungsi “Mini App Favorite” diperlukan, harap implementasikan antarmuka "IMiniAppFavoriteService".

Kode contoh sebagai berikut:

public class MiniAppFavoriteServiceImpl implements IMiniAppFavoriteService {
    public static List<MiniAppInfo> mFavorites = new ArrayList<>();

    static {
        getFav();
    }

    @Override
    public void addToFavorites(String subAppCode, ServiceCallback callback) {
        if (!isFavorite(subAppCode)) {
            if (mFavorites.size() >= 7) {
                if (callback != null) {
                    Result result = new Result();
                    result.success = false;
                    result.msg = "Collect up to 7 mini programs";
                    callback.onFinish(result);

                    return;
                }
            }

            List<MiniAppInfo> miniApps = MiniAppInfoManager.getInstance().getCache();
            if (miniApps == null || miniApps.isEmpty()) {
                if (FlavorUtil.isVSCodeIDE()) {
                    Result result = new Result();
                    result.success = false;
    
                    callback.onFinish(result);
                    return;
                }
                if (callback != null) {
                    Result result = new Result();
                    result.success = false;
                    result.msg = "Collecting mini program failed";
                    callback.onFinish(result);

                    return;
                }
            }

            for (MiniAppInfo data : miniApps) {
                if (TextUtils.equals(subAppCode, data.getAppId())) {
                    mFavorites.add(data);
                    saveFav();
                    break;
                }
            }
        }



        if (callback != null) {
            callback.onFinish(new Result());
        }
    }

    @Override
    public void removeFromFavorites(String subAppCode, ServiceCallback callback) {
        if (isFavorite(subAppCode)) {
            for (int i = 0; i != mFavorites.size(); ++i) {
                if (TextUtils.equals(subAppCode, mFavorites.get(i).getAppId())) {
                    mFavorites.remove(i);
                    saveFav();
                    break;
                }
            }
        }

        if (callback != null) {
            callback.onFinish(new Result());
        }
    }

    @Override
    public boolean isFavorite(String subAppCode) {
        for (MiniAppInfo data : mFavorites) {
            if (TextUtils.equals(subAppCode, data.getAppId())) {
                return true;
            }
        }
        return false;
    }

    private void saveFav() {
        JSONArray jsonArray = new JSONArray();
        for (MiniAppInfo data : mFavorites) {
            JSONObject jsonObject = new JSONObject();
            try {
                jsonObject.put("appId", data.getAppId());
                jsonObject.put("name", data.getAppName());
                jsonObject.put("icon", data.getAppIcon());
                jsonArray.put(jsonObject);
            } catch (JSONException e) {

            }
        }
        Context context = MainModuleService.context;
        if (context != null) {
            SharedPreferences sp = context.getSharedPreferences("mini_app", 0);
            SharedPreferences.Editor editor = sp.edit();
            editor.putString("mini_app_fav", jsonArray.toString());
            editor.commit();
        }
    }

    private static void getFav() {
        Context context = MainModuleService.context;
        if (context != null) {
            String fav = sp.getString("mini_app_fav", null);
            if (!TextUtils.isEmpty(fav)) {
                try {
                    JSONArray jsonArray = new JSONArray(fav);
                    for (int i = 0; i != jsonArray.length(); ++i) {
                        JSONObject json = jsonArray.getJSONObject(i);
                        MiniAppInfo miniAppData = new MiniAppInfo();
                        miniAppData.setAppId(json.getString("appId"));
                        miniAppData.setAppName(json.getString("name"));
                        miniAppData.setAppIcon(json.getString("icon"));

                        mFavorites.add(miniAppData);
                    }
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
        }
    }

  1. Tombol "setting" di UserMoreDialog digunakan untuk mengelola "User Device Info Scope", di mana pengguna dapat "approve/revoke" scope tersebut.

Halaman pengelolaan scope (UserInfoAuthPageFragment) akan dibuka ketika pengguna mengklik tombol "setting".

Catatan: Jika Anda ingin menggunakan scope kustom, Anda perlu menyesuaikan halaman tersebut. Anda dapat merujuk ke bagian 3.1.3.1.6 (AuthorizeSettingsFragment).

UserInfoAuthPageFragment:

Parameter awal untuk membuka halaman UserInfoAuthPageFragment adalah sebagai berikut:

uiComponentParam.params

miniAppInfo:informasi meta miniapp.

appId: ID miniapp

userId: userId

  1. Tombol "copy" digunakan untuk menyalin URL halaman saat ini:

  1. Tombol "share": Jika fungsi “share” diperlukan, harap implementasikan antarmuka "IMiniAppMoreService". Kode contoh sebagai berikut:

public class MyShare implements IMiniAppMoreService {
   /**
     * @param miniAppInfo
     *             miniAppInfoMap.put("miniAppId","miniAppId");
     *             miniAppInfoMap.put("miniAppName", "miniAppName");
     *             miniAppInfoMap.put("miniAppIcon", "miniAppIcon");
     *             miniAppInfoMap.put("miniAppSlogan", "miniAppSlogan");
     *             miniAppInfoMap.put("miniAppDescription", "miniAppDescription");
     *             miniAppInfoMap.put("miniAppVersion", "miniAppVersion");
     */
    
    @Override
    public String share(HashMap<String, String> miniAppInfo) {
        return "emassuperapp://miniapp/startapp?appId=mini app ID";
    }
}

// register the "share" function
ServiceManager.getInstance().registerService(IMiniAppMoreService.class.getName(), MyShare.class);

  1. Tombol "refresh" digunakan untuk me-restart miniapp.

Jika Anda ingin membuka halaman "about us" dan "scope setting" di halaman more kustom, kode contoh sebagai berikut:

     holder.itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                 int id= v.getId();
                    if(id==about_us){
                        if (uiResultCallBack != null) {
    uiComponentParam.getUiResultCallBack().onComplete(UIResultDataUtil.getResultData(UIResultCodeConstants.OPEN_ABOUT));
}
                    }else if(id==auth_setting){
                     if (uiResultCallBack != null) {
    uiComponentParam.getUiResultCallBack().onComplete(UIResultDataUtil.getResultData(UIResultCodeConstants.OPEN_SETTING));
}   
                    }
                    
                }

d. Di user AboutUsDialogfragment

Kode contoh merujuk ke bagian 3.1.3.1.8.

uiComponentParam.params adalah sebagai berikut:

miniAppInfo:informasi meta miniapp.

e. Di user FavoriteDialogFragment

Kode contoh merujuk ke bagian 3.1.3.1.7.

uiComponentParam.params adalah sebagai berikut:

miniAppInfo:informasi meta miniapp.

appId: ID miniapp

userId: userId

3.1.2.2 Di iOS
@protocol EMASIUIExtension <NSObject>

@optional

// Digunakan untuk membuat dialog 'More', lihat ‘figure 1’ pada catatan untuk contoh gaya
- (nullable UIViewController *)createMoreDialogViewController:(EMASUIComponentParam *)uiComponentParam;

// Digunakan untuk membuat dialog 'Favorite', lihat ‘figure 2’ pada catatan untuk contoh gaya
- (nullable UIViewController *)createFavoriteDialogViewController:(EMASUIComponentParam *)uiComponentParam;

// Digunakan untuk membuat dialog 'Auth', lihat ‘figure 3’ pada catatan untuk contoh gaya
- (nullable UIViewController *)createUserDeviceInfoAuthDialogViewController:(EMASUIComponentParam *)uiComponentParam;

// Digunakan untuk membuat sub halaman 'Error', lihat ‘figure 5’ pada catatan untuk contoh gaya
- (nullable UIViewController *)createErrorPageViewController:(EMASUIComponentParam *)uiComponentParam;

// Digunakan untuk membuat sub halaman 'Slash', lihat ‘figure 6’ pada catatan untuk contoh gaya
- (nullable UIViewController *)createSplashPageViewController:(EMASUIComponentParam *)uiComponentParam;

// Digunakan untuk membuat dialog 'About US', lihat ‘figure 8’ pada catatan untuk contoh gaya
- (nullable UIViewController *)createAboutUsDialogViewController:(EMASUIComponentParam *)uiComponentParam;

- (nullable UIViewController *)createSettingsDialogViewController:(EMASUIComponentParam *)uiComponentParam;


@end
@interface EMASUIComponentParam : NSObject

@property (nonatomic, strong) NSDictionary * params;
@property (nonatomic, copy) void(^completionBlock)(NSDictionary *resultDict);

@end
FOUNDATION_EXPORT NSString * _Nonnull const EMAS_CANCEL;

FOUNDATION_EXPORT NSString * _Nonnull const EMAS_CONFIRM;

FOUNDATION_EXPORT NSString * _Nonnull const EMAS_RESTART_MINIAPP;

FOUNDATION_EXPORT NSString * _Nonnull const EMAS_EXIT_MINIAPP;

FOUNDATION_EXPORT NSString * _Nonnull const EMAS_ERROR;

FOUNDATION_EXPORT NSString * _Nonnull const EMAS_OPEN_ABOUT;

FOUNDATION_EXPORT NSString * _Nonnull const EMAS_OPEN_SETTING;
Cara menggunakan konstanta dalam format data Auth
  • EMAS_CANCEL: Ketika pengguna mengklik tombol cancel pada halaman, atur code ke konstanta ini. SDK akan menutup jendela pop-up.

  • EMAS_CONFIRM: Ketika pengguna mengklik tombol confirm pada halaman, atur code ke konstanta ini dan kembalikan informasi relevan di data. SDK akan menutup jendela pop-up.

  • EMAS_ERROR: Ketika pemrosesan komponen gagal, atur code ke konstanta ini dan berikan pesan kesalahan di msg. SDK akan menutup jendela pop-up dan meneruskan pesan kesalahan ke JavaScript untuk dikembalikan ke miniapp.

  • EMAS_RESTART_MINIAPP: Ketika pengguna mengklik tombol refresh/restart pada halaman miniapp, atur code ke konstanta ini. SDK akan membuka ulang dan memuat kembali miniapp.

  • EMAS_EXIT_MINIAPP: Ketika pengguna mengklik tombol exit pada halaman miniapp, atur code ke konstanta ini. SDK akan menutup miniapp saat ini.

Cara menggunakan completionBlock dan kode Constant:

a. Di ErrorPageViewController:

Ketika pengguna mengklik tombol exit pada halaman miniapp, atur code ke EMAS_EXIT_MINIAPP.

uiComponentParam.completionBlock(["code": EMAS_EXIT_MINIAPP])

Ketika pengguna mengklik tombol refresh/restart pada halaman miniapp, atur code ke EMAS_RESTART_MINIAPP.

uiComponentParam.completionBlock(["code": EMAS_RESTART_MINIAPP])

b. Di UserDeviceInfoAuthDialogViewController:

uiComponentParam.params adalah sebagai berikut:

authorize: Parameter ini diteruskan ketika miniapp memanggil wv.authorize. Jika parameter metode wv.authorize berisi "scope" dan "scopes", "scopes" digunakan terlebih dahulu; jika tidak, parameter "scope" yang digunakan. SDK akan memeriksa status otorisasi sebelumnya yang diberikan oleh miniapp dan mengatur status otorisasi untuk scope yang diminta.

appId: ID miniapp

userId: userId

miniAppInfo: informasi meta miniapp

/*
[
    "authorize": ["location": true/false, "camera": true/false],
    "appId": appId,
    "userId": userInfo.userId,
    "miniAppInfo": miniAppInfo
]

miniAppInfo: 
[
    "miniAppId": appId,
    "miniAppName": appName,
    "miniAppIcon": iconUrl,
    "miniAppVersion": version,
    "miniAppSlogan": slogan,
    "miniAppDescription": description
]
*/

Jika pengguna membatalkan otorisasi, data dalam completionBlock harus diatur sebagai berikut:

uiComponentParam.completionBlock(["code": EMAS_CANCEL])

Jika pengguna menolak otorisasi, data dalam completionBlock harus diatur sebagai berikut:

uiComponentParam.completionBlock(["code": EMAS_CANCEL, "msg":"User cancels authorization"])

Jika pengguna menyetujui otorisasi, hasil otorisasi akan dikembalikan ke callback wv.authorize (menandai scope input sebagai true/false). Data dalam completionBlock harus diatur sebagai berikut:

/*
authDic:Format informasi sebagai berikut:
["location": true,"camera": false]
*/
uiComponentParam.completionBlock(["code": EMAS_CONFIRM, "msg":"", "data":authDic])



c. Di UserMoreDialogFragment

uiComponentParam.params adalah sebagai berikut:

miniAppId: ID miniapp

userId: userId

miniAppUrl: URL halaman saat ini dari miniapp

miniAppName: appName

miniAppIcon: iconUrl

miniAppSlogan: slogan

miniAppDescription: description

miniAppVersion: version

/*
Ini sesuai dengan struktur yang diperoleh dalam param di UIComponentParam.
{
    "miniAppId": appId,
    "miniAppName": appName,
    "miniAppIcon": iconUrl,
    "miniAppVersion": version,
    "miniAppSlogan": slogan,
    "miniAppDescription": description,
    "miniAppUrl": "current page url",
    "userId": userId
}
*/

  1. Tombol "add/remove" Favorite: Jika fungsi “Mini App Favorite” diperlukan, harap implementasikan "EMASMiniAppFavoriteService".

jika fungsi koleksi pengguna ada di more

#import "EMASMiniAppFavoriteServiceImpl.h"

@implementation EMASMiniAppFavoriteServiceImpl


/**
 * add to favorites
 * @param subAppCode ID miniapp
 * @return int 0, add success | not 0, error code
 */
- (void)addToFavorites:(NSString*)subAppCode completion:(nonnull EMASFavoriteCompletionBlock)completionBlock{

    [[NSUserDefaults standardUserDefaults] setObject:@(YES) forKey:subAppCode];
    [[NSUserDefaults standardUserDefaults] synchronize];


    if (completionBlock) {
        completionBlock(@{});
    }

}

/**
 * remove from favorites
 * @param subAppCode ID miniapp
 * @return int 0, add success | not 0, error code
 */
- (void)removeFromFavorites:(NSString*)subAppCode completion:(nonnull EMASFavoriteCompletionBlock)completionBlock{

    [[NSUserDefaults standardUserDefaults] removeObjectForKey:subAppCode];
    [[NSUserDefaults standardUserDefaults] synchronize];

    if (completionBlock) {
        completionBlock(@{});
    }

}

/**
 * is favorite
 * @param subAppCode ID miniapp
 * @return int 0, add success | not 0, error code
 */
- (BOOL)isFavorite:(NSString*)subAppCode {

    BOOL isFavorite = [[NSUserDefaults standardUserDefaults] objectForKey:subAppCode];

    return isFavorite;
}

@end

  1. Tombol "setting" di UserMoreDialog digunakan untuk mengelola "User Device Info Scope", di mana pengguna dapat "approve/revoke" scope tersebut.

Halaman pengelolaan scope (SettingViewController) akan dibuka ketika pengguna mengklik tombol "setting".

Catatan: Jika Anda ingin menggunakan scope kustom, Anda perlu menyesuaikan halaman tersebut. Anda dapat merujuk ke bagian 3.1.3.2.6 (CPSettingPage).

Parameter awal untuk membuka halaman SettingViewController adalah sebagai berikut:

uiComponentParam.params adalah sebagai berikut:

miniAppId: ID miniapp

userId: userId

miniAppUrl: URL halaman saat ini dari miniapp

miniAppName: appName

miniAppIcon: iconUrl

miniAppSlogan: slogan

miniAppDescription: description

miniAppVersion: version

  1. Tombol "copy" digunakan untuk menyalin URL halaman saat ini:

  1. Tombol "share": Jika fungsi “share” diperlukan, harap implementasikan protokol "EMASMiniAppMoreService". Kode contoh sebagai berikut:

import Foundation

@objc(EMASMiniAppMoreServiceImpl)
class EMASMiniAppMoreServiceImpl: NSObject, EMASMiniAppMoreService {
    /*
    *@param miniAppInfo
    {
    "miniAppId": appId,
    "miniAppName": appName,
    "miniAppIcon": iconUrl,
    "miniAppVersion": version,
    "miniAppSlogan": slogan,
    "miniAppDescription": description,
    "userId": userId
}
    */
    @objc func share(_ miniAppInfo: [AnyHashable : Any]!) -> String! {
        let appid: String = miniAppInfo["miniAppId"] as? String ?? ""
        return String(format: "%@%@", "superapp://miniapp/startapp?appId=", appid)
    }
}

  1. Tombol "refresh" digunakan untuk me-restart miniapp.

Harap implementasikan protokol "EMASMiniAppMoreService" jika MiniApp saat ini berada di jendela lain.

import Foundation
import EMASMiniApp
import EMASServiceManager

@objc(EMASMiniAppMoreServiceImpl)
class EMASMiniAppMoreServiceImpl: NSObject, EMASMiniAppMoreService {

    @objc func restartMiniApp(_ miniAppId: String!, openConfiguration: EMASMiniAppOpenConfiguration!) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            if let miniAppService = EMASServiceManager.sharedInstance().service(forProtocol: "EMASMiniAppService") as? EMASMiniAppService {
                //TODO:switch window visible,and call openMiniApp method, such as:
                let appDelegate = UIApplication.shared.delegate as? AppDelegate
                appDelegate?.window?.isHidden = true;
                appDelegate?.windvaneWindow?.isHidden = false
                appDelegate?.windvaneWindow?.makeKeyAndVisible()
                miniAppService.openMiniApp(miniAppId, openConfiguration: openConfiguration) { code, resultDic in

                                                                                            }
            }
        }

    }
}

Jika Anda ingin membuka halaman "about us" dan "scope setting" di halaman more kustom, kode contoh sebagai berikut:

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {

    NSString *appId = self.miniAppInfo[@"miniAppId"] ?: @"";
    NSString * code = @"";
    switch (item.key) {
            //收藏
        case MenuItemKeyAddToFav:
            {
                code = EMAS_CANCEL;
                id<EMASMiniAppFavoriteService> favService = [[EMASServiceManager sharedInstance] serviceForProtocol:@"EMASMiniAppFavoriteService"];

                if (favService){
                    BOOL isFavorite = [favService isFavorite:appId];
                    __weak typeof(self) weakSelf = self;
                    if (!isFavorite) {

                        [favService addToFavorites:appId completion:^(NSDictionary * _Nonnull result) {
                            [CPCommonTool showToast:kLocalStringWithValue(@"miniapp_more_favorite_success", @"Added to favorites")];//miniapp_more_favorite_success
                        }];
                    }else{
                        if(favService){
                            [favService removeFromFavorites:appId completion:^(NSDictionary * _Nonnull result) {
                                [CPCommonTool showToast:kLocalStringWithValue(@"miniapp_more_remove_favorite_success", @"Removed from my favorites")];
                            }];
                        }
                    }
                };
                if (self.completionBlock && code && code .length != 0) {
                    self.completionBlock(@{@"code":code});
                }
            }
            break;
            //设置
        case MenuItemKeySettings:
            {
                //跳转设置页面
                code = EMAS_OPEN_SETTING;
                if (self.completionBlock && code && code .length != 0) {
                    self.completionBlock(@{@"code":code});
                }

            }
            break;

            //复制链接
        case MenuItemKeyCopyLink:
            {

                NSString *urlStr =  self.miniAppInfo[@"miniAppUrl"];
                if (urlStr && ![urlStr isEqualToString:@""]) {
                    UIPasteboard * pasteboard = [UIPasteboard generalPasteboard];
                    pasteboard.string = urlStr;
                    [CPCommonTool showToast:kLocalStringWithValue(@"miniapp_more_copy_success", @"Copied")];
                }

            }
            break;

            //分享
        case MenuItemKeyShare:
            {
                __weak typeof(self) weakself = self;
                UIViewController* vc = self.presentingViewController;
                NSString* url = self.miniAppInfo[@"miniAppUrl"];
                [self dismissViewControllerWithCompletion:^{
                    __strong typeof(weakself) strongSelf = weakself;
                    strongSelf.completionBlock(@{@"code":EMAS_CANCEL});
                    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                        [strongSelf showShareWithString:url showIn:vc];
                    });
                }];
            }
            break;


            //关于我们
        case MenuItemKeyAboutUs:
            {
                code = EMAS_OPEN_ABOUT;
                if (self.completionBlock && code && code .length != 0) {
                    self.completionBlock(@{@"code":code});
                }
            }
            break;

            //刷新
        case MenuItemKeyRefresh:
            {
                code = EMAS_RESTART_MINIAPP;
                if (self.completionBlock && code && code .length != 0) {
                    self.completionBlock(@{@"code":code});
                }

            }

        default:
            break;
    }
}
d. Di user AboutUsDialogfragment

uiComponentParam.params adalah sebagai berikut:

miniAppId: ID miniapp

userId: userId

miniAppUrl: URL halaman saat ini program mini

miniAppName: appName

miniAppIcon: iconUrl

miniAppSlogan: slogan

miniAppDescription: description

miniAppVersion: version.

e. Di user FavoriteDialogFragment

uiComponentParam.params adalah sebagai berikut:

miniAppId: ID miniapp

userId: userId

miniAppUrl: URL halaman saat ini dari miniapp

miniAppName: appName

miniAppIcon: iconUrl

miniAppSlogan: slogan

miniAppDescription: description

miniAppVersion: version

3.1.3 Implementasikan antarmuka melalui plugin

3.1.3.1 Implementasi Android
1. buat UIPlugin kustom client
public class CustomUIPlugin extends BasePlugin implements UIExtension {


    public CustomUIPlugin(Context context) {
        super(context);
    }
       @Override
    public Fragment createSplashPageFragment(UIComponentParam uiComponentParam) {
        return SplashPageFragment.newInstance();
    }
    @Override
    public Fragment createUserDeviceInfoAuthDialogFragment(UIComponentParam uiComponentParam) {
            
        /* Ini adalah data yang kami paket
            HashMap<String, Object> hashMap = new HashMap();
            hashMap.put("authorize", new JSONObject(authorize).toString());
            hashMap.put("miniAppInfo", getMiniAppInfo().toString());
            hashMap.put(MiniAppInfoConstants.MINI_APP_ID, appId);
            hashMap.put(MiniAppInfoConstants.USER_ID, userInfo.getUserId()); */
        HashMap<String, Object> params = uiComponentParam.getData();
        //Di sini Anda dapat menerapkan logika otorisasi Anda untuk memverifikasi kapan jendela pop-up diperlukan. Jika tidak diperlukan jendela pop-up,
        //Anda dapat mengembalikan nilai null.
     if (uiComponentParam == null) {
            return null;
        }
        if (uiComponentParam.getData() == null) return null;
        HashMap<String, Object> params = uiComponentParam.getData();
//        boolean permissionValid = MiniAppAuthorizations.isPermissionValid(innerScope);
        String authorizeData = (String) params.get("authorize");
        String appId = (String) params.get(MiniAppInfoConstants.MINI_APP_ID);
        String userId = (String) params.get(MiniAppInfoConstants.USER_ID);
        JSONObject jsObj = null;
        boolean permissionValid = true;
        List<String> authorize = new ArrayList<>();
        JSONObject returnData = new JSONObject();
        JSONObject authorizedData = new JSONObject();
        String authorizeValid = "";
        List<String> authorizedValid = new ArrayList<>();
        try {
            jsObj = new JSONObject(authorizeData);
            if (jsObj != null) {
                Iterator<String> keys = jsObj.keys();
                for (Iterator<String> it = keys; it.hasNext(); ) {
                    String key = it.next();
                    Boolean value = (Boolean) jsObj.get(key);
                    permissionValid = MiniAppAuthorizations.isPermissionValid(key);
                    if (permissionValid) {
                        if (!value) {
                            authorize.add(key);
                        } else {
                            authorizedData.put(key, true);
                        }
                    } else {
                        authorizedValid.add(key);
                        authorizeValid += "'" + key + "',";
                    }

                }
               

            }
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }

        if (authorize.size() == 0 && authorizedData.length() == 0) {

            if (uiComponentParam.getUiResultCallBack() != null) {
                uiComponentParam.getUiResultCallBack().onComplete(UIResultDataUtil.getResultData(null, "Kesalahan: Izin tidak diketahui " + authorizeValid.substring(0, authorizeValid.length() - 1) + " ", UIResultCallBackConstants.CANCEL_BTN));
            }
            return null;
        } else if (authorize.size() == 0 && authorizedData.length() != 0) {


            if (uiComponentParam.getUiResultCallBack() != null) {
                uiComponentParam.getUiResultCallBack().onComplete(UIResultDataUtil.getResultData(authorizedData, "", UIResultCallBackConstants.SURE_BTN));

            }

            return null;
        }


        params.put("authorize", authorize);
        JSONObject finalJsObj = jsObj;
        String finalAuthorizeValid = authorizeValid;
          return  return AuthorizeFragment.newInstance(params, new OnDialogButtonClickListener() {
            @Override
            public void onPositiveClick(String data) {
                //Data ini dapat berupa data yang dipilih pengguna untuk mengizinkan. Tentu saja, Anda juga dapat memilih untuk menyimpan pemrosesan logika saat tombol OK diklik,
                //atau mengembalikan data tersebut untuk diproses.
                IAuthService authService = PluginEnv.getInstance().getPluginContext().getContainer().getAuthService();
                JSONArray restoredList = null;
                if (!TextUtils.isEmpty(data)) {
                    try {
                        restoredList = new JSONArray(data);
                        JSONObject jsonObject = new JSONObject();
                        if (restoredList != null && restoredList.length() > 0) {
                            for (int i = 0; i < restoredList.length(); i++) {
                               
                                String innerScope = restoredList.getString(i);
//
                                if (authService == null) {
                                   
                                } else {
                                    authService.saveAuth(uiComponentParam.getContext(), innerScope, userId, appId);
                                    finalJsObj.put(innerScope, true);
                                }
                               
                            }
                            if (authorizedValid.size() != 0) {
                                for (String authorize : authorizedValid) {
                                    finalJsObj.remove(authorize);
                                }

                            }
                        }
                        //finalJsObj:Ini adalah data yang kami butuhkan dari Anda untuk mengonfirmasi keberhasilan otorisasi.
                        //  Format   :  new JSONObject ,finalJsObj.put("file", true);        {"file":true,"location":true}
                        if (uiComponentParam.getUiResultCallBack() != null) {
                            uiComponentParam.getUiResultCallBack().onComplete(UIResultDataUtil.getResultData(finalJsObj, "", UIResultCallBackConstants.SURE_BTN));

//   
                        }

                    } catch (JSONException e) {
                        throw new RuntimeException(e);
                    }
                }
            }

            @Override
            public void onNegativeClick(String data) {
                if (uiComponentParam.getUiResultCallBack() != null) {
                    uiComponentParam.getUiResultCallBack().onComplete(UIResultDataUtil.getResultData(null, "Pengguna menolak permintaan otorisasi", UIResultCallBackConstants.CANCEL_BTN));
                }
            }
        }, uiComponentParam.getContext());  
        }
     HashMap<String, Object> params = uiComponentParam.getParams();
        String authorizeData = (String) params.get("authorize");
        String appId = (String) params.get(MiniAppInfoConstants.MINI_APP_ID);
        String userId = (String) params.get(MiniAppInfoConstants.USER_ID);
        JSONObject jsObj = null;
        boolean permissionValid = true;
        List<String> authorize = new ArrayList<>();
        JSONObject authorizedData = new JSONObject();
        String authorizeValid = "";
        List<String> authorizedValid = new ArrayList<>();
        try {
            jsObj = new JSONObject(authorizeData);
            if (jsObj != null) {
                Iterator<String> keys = jsObj.keys();
                for (Iterator<String> it = keys; it.hasNext(); ) {
                    String key = it.next();
                    Boolean value = (Boolean) jsObj.get(key);
                    permissionValid = MiniAppAuthorizations.isPermissionValid(key);
                    if (permissionValid) {
                        if (!value) {
                            authorize.add(key);
                        } else {
                            authorizedData.put(key, true);
                        }
                    } else {
                        authorizedValid.add(key);
                        authorizeValid += "'" + key + "',";
                    }

                }

            }
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
  
        if (authorize.size() == 0 && authorizedData.length() == 0) {
            if (uiComponentParam.getUiResultCallBack() != null) {
                uiComponentParam.getUiResultCallBack().onComplete(UIResultDataUtil.getResultData("Kesalahan: Izin tidak diketahui " + authorizeValid.substring(0, authorizeValid.length() - 1) + " ", UIResultCodeConstants.ERROR));
            }
            return null;
        } else if (authorize.size() == 0 && authorizedData.length() != 0) {
  
            if (uiComponentParam.getUiResultCallBack() != null) {
                uiComponentParam.getUiResultCallBack().onComplete(UIResultDataUtil.getResultData(authorizedData, "", UIResultCodeConstants.CONFIRM));
            }
            return null;
        }
        params.put("authorize", authorize);
        JSONObject finalJsObj = jsObj;
        return AuthorizeFragment.newInstance(params, new OnDialogButtonClickListener() {
            @Override
            public void onPositiveClick(String data) {
                IAuthService authService = PluginEnv.getInstance().getPluginContext().getContainer().getAuthService();
                JSONArray restoredList = null;
                if (!TextUtils.isEmpty(data)) {
                    try {
                        restoredList = new JSONArray(data);
                        if (restoredList != null && restoredList.length() > 0) {
                            for (int i = 0; i < restoredList.length(); i++) {
                                String innerScope = restoredList.getString(i);
                                if (authService != null) {
                                    authService.saveAuth(uiComponentParam.getContext(), innerScope, userId, appId);
                                    finalJsObj.put(innerScope, true);
                                }
                            }
                            if (authorizedValid.size() != 0) {
                                for (String authorize : authorizedValid) {
                                    finalJsObj.remove(authorize);
                                }

                            }
                        }

                        if (uiComponentParam.getUiResultCallBack() != null) {
                            uiComponentParam.getUiResultCallBack().onComplete(UIResultDataUtil.getResultData(finalJsObj, "", UIResultCodeConstants.CONFIRM));
                        }
                    } catch (JSONException e) {
                        throw new RuntimeException(e);
                    }
                }
            }

            @Override
            public void onNegativeClick(String data) {
                if (uiComponentParam.getUiResultCallBack() != null) {
                    uiComponentParam.getUiResultCallBack().onComplete(UIResultDataUtil.getResultData("Pengguna menolak permintaan otorisasi", UIResultCodeConstants.ERROR));
                }
            }
        }, uiComponentParam.getContext());
    }
       @Override
    public Fragment createErrorPageFragment(UIComponentParam uiComponentParam) {
        /*  Ini adalah data yang kami paket
        *   HashMap<String, Object> map = new HashMap<>();
         *   map.put("errorCode", 1);
        */
        return ErrorFragment.newInstance(uiComponentParam);
    }

    ...
}
2. Implementasi UserDeviceInfoAuthDialogFragment
package com.alibaba.plugin.ui.fragment;

import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import com.alibaba.emas.android.windvane.mini.app.pluginsdk.ui.base.AbstractDialogFragment;
import com.alibaba.plugin.ui.R;
//Jika Anda adalah developer Android native dan menggunakan jendela pop-up, 
//Anda dapat extends DialogFragment. 
//Jika Anda adalah developer flutter,
//Anda dapat extends Fragment.

public class AuthorizeFragment extends Fragment  {
  private FragmentAuthorizeBinding binding;
    private TextView mCloseBtn;
    private String miniAppInfo = null;
   private List<String> itemList = new ArrayList<>();
    private OnDialogButtonClickListener listener;
    public AuthorizeFragment() {
        // Required empty public constructor
    }
//Ini adalah informasi miniinfo yang kami teruskan. 
  //  Anda dapat mengurai sesuai dengan kunci yang kami teruskan.
//           JSONObject miniInfoJson = new JSONObject();
            // MiniAppInfo appInfo = updateData.miniAppInfo;
            // miniInfoJson.put("miniAppName", appInfo.getAppName());
            // miniInfoJson.put("miniAppId", updateData.appId);
            // miniInfoJson.put("miniAppSlogan", appInfo.getAppSlogan());
            // miniInfoJson.put("miniAppDescription", appInfo.getAppDesc());
            // miniInfoJson.put("miniAppIcon", appInfo.getAppIcon());
            // miniInfoJson.put("miniAppVersion", updateData.version);
            // miniInfoJson.toString()
  public static AuthorizeFragment newInstance(HashMap<String, Object> data, OnDialogButtonClickListener buttonClickListener, Context context) {
        AuthorizeFragment fragment = new AuthorizeFragment();
        fragment.itemList = (List<String>) data.get("authorize");
        fragment.miniAppInfo = (String) data.get("miniAppInfo");
        fragment.listener = buttonClickListener;
        fragment.context = context;
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
                binding = FragmentAuthorizeBinding.inflate(inflater, container, false);

        return binding.getRoot();
    }


    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        //Di sini Anda dapat melakukan operasi logika Anda dan mengembalikan data kembali
             if (miniAppInfo != null) {
            try {
                JSONObject jsonObject = new JSONObject(miniAppInfo);
                String miniAppIcon = jsonObject.optString("miniAppIcon");
                String miniAppName = jsonObject.optString("miniAppName");
                Picasso.get().load(miniAppIcon).placeholder(R.drawable.splash_logo).error(R.drawable.splash_logo).into(binding.headerIcon);
                binding.headerTitle.setText(miniAppName + " " + getString(R.string.mini_app_authorize_apply));
            } catch (JSONException e) {
                throw new RuntimeException(e);
            }
        }
        binding.cancelButton.setOnClickListener(v -> {
            listener.onNegativeClick(null);
            dismiss();
        });
        binding.confirmButton.setOnClickListener(v -> {
            if (itemList.size() == 1) {
                listener.onPositiveClick(itemList.toString());
            } else {
                List<String> checkedAuthorized = new ArrayList<>();
                for (int i = 0; i < binding.recyclerView.getChildCount(); i++) {
                    View child = binding.recyclerView.getChildAt(i);
                    CheckBox checkBox = child.findViewById(R.id.checkBox);
                    if (checkBox.isChecked()) {
                        int position = binding.recyclerView.getChildAdapterPosition(child);
                        String selectedItem = itemList.get(position);
                        checkedAuthorized.add(selectedItem);
                    }
                }
                if (checkedAuthorized.size() != 0) {
                    listener.onPositiveClick(checkedAuthorized.toString());
                } else {
                    listener.onNegativeClick(null);
                }
            }
            dismiss();
        });
    }



   




}
3. Implementasi ErrorPageFragment
public class ErrorFragment extends Fragment {
    
    private FragmentErrorBinding binding;
    private static int errorCode = 0;
    private static UIResultCallBack uiResultCallBack;

    public static ErrorFragment newInstance(UIComponentParam uiComponentParam) {
        if (uiComponentParam != null && uiComponentParam.getParams() != null) {
            HashMap<String, Object> data = uiComponentParam.getParams();
            errorCode = (int) data.get("errorCode");
            uiResultCallBack = uiComponentParam.getUiResultCallBack();
        }
        return new ErrorFragment();
    }

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        binding = FragmentErrorBinding.inflate(inflater, container, false);
        return binding.getRoot();
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        setText(binding.tvErrorMessage, ErrorConstants.getErrorMessage(getContext(), errorCode));
        setText(binding.tvErrorCode, getString(R.string.mini_app_error_code) + ":00000" + ErrorConstants.getErrorCode(errorCode));
        setLocalImage(binding.ivMiniAppLogo, ErrorConstants.getErrorIcon(errorCode));
        setViewsClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                int id = view.getId();
                if (id == R.id.btn_exit) {
                uiResultCallBack.onComplete(UIResultDataUtil.getResultData(UIResultCodeConstants.EXIT_MINIAPP));
                } else if (id == R.id.btn_close) {
             uiResultCallBack.onComplete(UIResultDataUtil.getResultData(UIResultCodeConstants.EXIT_MINIAPP));

                } else if (id == R.id.btn_refresh) {
                    if (errorCode != MiniAppConstants.ERROR_PAGE_NOT_EXITS) {
                        if (uiResultCallBack != null) {
                            uiResultCallBack.onComplete(UIResultDataUtil.getResultData(UIResultCodeConstants.RESTART_MINIAPP));
                        }
                    }

                }
            }
        }, binding.btnExit, binding.btnClose, binding.btnRefresh);
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

    }

    @Override
    protected void renderState(MiniAppState state) {
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        binding = null;
    }
4. Implementasi SplashPageFragment
//Jika Anda ingin menyesuaikan layar splash saat applet dibuka, 
//Anda dapat merujuk ke
public class SplashPageFragment extends Fragment implements ISplashExtension {
    private FragmentLoadingBinding binding;

    public static SplashPageFragment newInstance() {
        return new SplashPageFragment();
    }

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        binding = FragmentLoadingBinding.inflate(inflater, container, false);
        return binding.getRoot();
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
               setImage(binding.ivLoadingIcon, appUrl);
               setText(binding.tvLoadingName, appName);
    }


       

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        binding = null;
    }

    @Override
    public void setProgress(int progress) {
        //Di sini kami dapat mengembalikan kepada Anda kemajuan pengunduhan program mini
        binding.progressBar.setProgress(progress);
        binding.statusText.setText(progress+"%" );
    }

    @Override
    public void setMiniAppInfo(HashMap<String, Object> params) {
     //Di sini Anda dapat memperbarui antarmuka Anda untuk menampilkan informasi program mini jika diperlukan
        String miniAppInfo = (String) params.get("miniAppInfo");
    
        try {
            JSONObject jsonObject = new JSONObject(miniAppInfo);
            String miniAppName = jsonObject.optString("miniAppName");
            String miniAppIcon = jsonObject.optString("miniAppIcon");
            if (TextUtils.isEmpty(appName)) {
                setText(binding.tvLoadingName, miniAppName);
            }
            if (TextUtils.isEmpty(appUrl)) {
                setImage(binding.ivLoadingIcon, miniAppIcon);
            }
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }


    }
}
5. Implementasi MoreFragment

import android.annotation.SuppressLint;
import android.app.Dialog;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import com.alibaba.emas.android.mini.app.common.log.MiniAppLog;
import com.alibaba.emas.android.windvane.mini.app.plugin.ui.R;
import com.alibaba.emas.android.windvane.mini.app.plugin.ui.data.MoreMenuItem;
import com.alibaba.emas.android.windvane.mini.app.plugin.ui.databinding.FragmentMoreBinding;
import com.alibaba.emas.android.windvane.mini.app.plugin.ui.databinding.MoreItemGridBinding;
import com.alibaba.emas.android.windvane.mini.app.pluginsdk.environment.PluginEnv;
import com.alibaba.emas.android.windvane.mini.app.pluginsdk.service.IMiniAppMoreService;
import com.alibaba.emas.android.windvane.mini.app.pluginsdk.service.IMoreService;
import com.alibaba.emas.android.windvane.mini.app.pluginsdk.ui.misc.UIComponentParam;
import com.alibaba.emas.android.windvane.mini.app.pluginsdk.ui.misc.UIResultCodeConstants;
import com.alibaba.emas.android.windvane.mini.app.pluginsdk.ui.misc.UIResultDataUtil;
import com.alibaba.emas.android.windvane.mini.app.pluginsdk.ui.protocol.UIResultCallBack;
import com.alibaba.module.android.core.servicebus.service.ServiceManager;
import com.alibaba.module.android.mini.app.api.IMiniAppFavoriteService;
import com.alibaba.module.android.mini.app.api.IToastService;
import com.squareup.picasso.Picasso;

import org.json.JSONException;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public class MoreFragment extends BaseDialogFragment {
    private static final String TAG = "MoreFragment";
    private FragmentMoreBinding binding;
    private UIComponentParam uiComponentParam;
    private JSONObject miniAppInfo = null;
    private UIResultCallBack uiResultCallBack = null;
    private String currentUrl = null;
    private List<MoreMenuItem> moreMenuItems = new ArrayList<>();

    public static MoreFragment newInstance(UIComponentParam uiComponentParam) {
        MoreFragment fragment = new MoreFragment();
        fragment.uiComponentParam = uiComponentParam;
        return fragment;
    }

    @NonNull
    @Override
    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
        Dialog dialog = new Dialog(requireContext(), R.style.BottomDialogTheme);
        binding = FragmentMoreBinding.inflate(getLayoutInflater());
        View contentView = binding.getRoot();
        dialog.setContentView(contentView);
        Window window = dialog.getWindow();
        if (window != null) {
            window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
            window.setGravity(Gravity.BOTTOM);
            window.getAttributes().windowAnimations = R.style.mini_app_bottom_dialog_anim;
        }
        dialog.setCanceledOnTouchOutside(false);
        dialog.setCancelable(false);
        return dialog;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        dealData();
    }

    private void dealData() {
        if (uiComponentParam != null) {
            HashMap<String, Object> params = uiComponentParam.getParams();
            if (params != null) {
                try {
                    miniAppInfo = TextUtils.isEmpty((String) params.get("miniAppInfo")) ? null : new JSONObject((String) params.get("miniAppInfo"));
                    currentUrl = (String) params.get("miniAppUrl");
                } catch (JSONException e) {
                    MiniAppLog.d(TAG, "JSONException in dealData ");
                }
            }
            uiResultCallBack = uiComponentParam.getUiResultCallBack();
        }
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        binding = FragmentMoreBinding.inflate(inflater, container, false);
        return binding.getRoot();
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        setView();
        setData();
        GridLayoutManager layoutManager = new GridLayoutManager(getContext(), 4);
        binding.rlMore.setLayoutManager(layoutManager);
        GridAdapter gridAdapter = new GridAdapter(moreMenuItems);
        binding.rlMore.setAdapter(gridAdapter);
    }

    private void setView() {
        if (miniAppInfo != null) {
            binding.headerTitle.setText(miniAppInfo.optString("miniAppName"));
            setImage(binding.headerIcon, miniAppInfo.optString("miniAppIcon"));
        }
        binding.btnMoreCancel.setOnClickListener(view -> {
            if (uiResultCallBack != null) {
                uiResultCallBack.onComplete(UIResultDataUtil.getResultData(UIResultCodeConstants.CANCEL));
            } else {
                dismiss();
            }
        });
    }

    private void handleCallBackResultData(String resultData) {
        if (uiResultCallBack != null) {
            uiResultCallBack.onComplete(UIResultDataUtil.getResultData(resultData));
        } else {
            dismiss();
        }
    }

    private void favorite(boolean isFavorite, IMoreService moreService) {
        final IToastService toastService = ServiceManager.getInstance().getService(IToastService.class.getName());

        if (isFavorite) {
            moreService.removeFromFavorites(miniAppInfo.optString("miniAppId"), getActivity());
            if (toastService == null) {
                getActivity().runOnUiThread(() -> {
                    Toast.makeText(getActivity(), getString(R.string.mini_app_favorite_remove_success), Toast.LENGTH_SHORT).show();
                });
            }
        } else {
            moreService.addToFavorites(miniAppInfo.optString("miniAppId"), getActivity());
            if (toastService == null) {
                getActivity().runOnUiThread(() -> {
                    Toast.makeText(getActivity(), getString(R.string.mini_app_favorite_success), Toast.LENGTH_SHORT).show();
                });
            }
        }
        handleCallBackResultData(UIResultCodeConstants.CANCEL);
    }

    private void copy() {
        ClipboardManager clipboardManager = (ClipboardManager) getContext()
                .getSystemService(Context.CLIPBOARD_SERVICE);
        ClipData clipData = ClipData.newPlainText(currentUrl, currentUrl);
        clipboardManager.setPrimaryClip(clipData);
        getActivity().runOnUiThread(() -> {
            if (getActivity() != null && !getActivity().isFinishing()) {
                Toast toast = Toast.makeText(getActivity(), getString(R.string.mini_app_copy), Toast.LENGTH_SHORT);
                toast.setGravity(Gravity.BOTTOM, 0, 0);
                toast.show();
            }
        });
        handleCallBackResultData(UIResultCodeConstants.CANCEL);

    }

    private void share(String share) {
        Intent shareIntent = new Intent(Intent.ACTION_SEND);
        shareIntent.setType("text/plain");
        shareIntent.putExtra(Intent.EXTRA_TEXT, share);
        shareIntent.putExtra(Intent.EXTRA_SUBJECT, "");
        startActivity(Intent.createChooser(shareIntent, ""));
        handleCallBackResultData(UIResultCodeConstants.CANCEL);
    }

    private void setData() {
        moreMenuItems.clear();
        IMoreService moreService = PluginEnv.getInstance().getPluginContext().getContainer().getMoreService();
        IMiniAppFavoriteService miniAppFavoriteService = ServiceManager.getInstance().getService(IMiniAppFavoriteService.class.getName());
        if (moreService != null && miniAppFavoriteService != null) {
            boolean isFavorite = moreService.isFavorite(miniAppInfo.optString("miniAppId"));
            String title = isFavorite ? getString(R.string.mini_app_more_remove_favorite)
                    : getString(R.string.mini_app_more_favorite);
            int icon = isFavorite ? R.drawable.mini_app_remove_favorite
                    : R.drawable.mini_app_add_favorite;
            moreMenuItems.add(new MoreMenuItem(title, icon, () -> {
                favorite(isFavorite, moreService);
            }));
        }
        if (!TextUtils.isEmpty(currentUrl)) {
            moreMenuItems.add(new MoreMenuItem(getString(R.string.mini_app_more_settings),
                    R.drawable.mini_app_setting,
                    () -> handleCallBackResultData(UIResultCodeConstants.OPEN_SETTING)));
        }
        if (!TextUtils.isEmpty(currentUrl)) {
            moreMenuItems.add(new MoreMenuItem(getString(R.string.mini_app_more_copy),
                    R.drawable.mini_app_copy,
                    () -> {
                        copy();
                    }));
        }
        IMiniAppMoreService service = ServiceManager.getInstance().getService(IMiniAppMoreService.class.getName());
        HashMap<String, String> miniAppInfoMap = new HashMap<>();
        if (miniAppInfo != null) {
            miniAppInfoMap.put("miniAppId", miniAppInfo.optString("miniAppId"));
            miniAppInfoMap.put("miniAppName", miniAppInfo.optString("miniAppName"));
            miniAppInfoMap.put("miniAppIcon", miniAppInfo.optString("miniAppIcon"));
            miniAppInfoMap.put("miniAppSlogan", miniAppInfo.optString("miniAppSlogan"));
            miniAppInfoMap.put("miniAppDescription", miniAppInfo.optString("miniAppDescription"));
            miniAppInfoMap.put("miniAppVersion", miniAppInfo.optString("miniAppVersion"));
        }

        if (service != null) {
            String share = service.share(miniAppInfoMap);
            moreMenuItems.add(new MoreMenuItem(getString(R.string.mini_app_more_Share),
                    R.drawable.mini_app_share, () -> {
                share(share);
            }));
        }
        moreMenuItems.add(new MoreMenuItem(getString(R.string.mini_app_more_refresh),
                R.drawable.mini_app_refresh,
                () -> handleCallBackResultData(UIResultCodeConstants.RESTART_MINIAPP)));

        moreMenuItems.add(new MoreMenuItem(getString(R.string.mini_app_more_about_us),
                R.drawable.mini_app_more_about_us,
                () -> handleCallBackResultData(UIResultCodeConstants.OPEN_ABOUT)));
    }

    @Override
    public void onStart() {
        super.onStart();
        Dialog dialog = getDialog();
        if (dialog != null && dialog.getWindow() != null) {
            Window window = dialog.getWindow();
            window.setGravity(Gravity.BOTTOM); 
            window.getAttributes().windowAnimations = R.style.mini_app_bottom_dialog_anim;
        }
    }

    @Override
    public void onResume() {
        super.onResume();
    }


    @Override
    public void onDestroyView() {
        super.onDestroyView();
        binding = null;
    }

    class GridAdapter extends RecyclerView.Adapter<GridAdapter.ViewHolder> {
        private List<MoreMenuItem> itemList;

        public GridAdapter(List<MoreMenuItem> itemList) {
            this.itemList = itemList;
        }

        @NonNull
        @Override
        public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            MoreItemGridBinding moreItemGridBinding = MoreItemGridBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
            return new ViewHolder(moreItemGridBinding);
        }

        @Override
        public void onBindViewHolder(@NonNull ViewHolder holder, @SuppressLint("RecyclerView") int position) {
            MoreMenuItem item = itemList.get(position);
            holder.itemGridBinding.tvMoreName.setText(item.title);
//            Picasso.get().load(item.iconResId).placeholder(R.drawable.mini_app_default_logo).error(R.drawable.mini_app_default_logo).into(holder.itemGridBinding.tvMoreIcon);
            setImage(holder.itemGridBinding.tvMoreIcon, item.iconResId);
            holder.itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    item.action.run();
                }
            });
        }

        @Override
        public int getItemCount() {
            return itemList.size();
        }

        public class ViewHolder extends RecyclerView.ViewHolder {
            MoreItemGridBinding itemGridBinding;

            public ViewHolder(@NonNull MoreItemGridBinding binding) {
                super(binding.getRoot());
                this.itemGridBinding = binding;
            }
        }
    }

}
6. Implementasi UserInfoPageFragment
import android.content.Context;
import android.graphics.Color;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import com.alibaba.emas.android.mini.app.common.log.MiniAppLog;
import com.alibaba.emas.android.windvane.mini.app.plugin.ui.R;
import com.alibaba.emas.android.windvane.mini.app.plugin.ui.databinding.FragmentAuthorizeSettingItemBinding;
import com.alibaba.emas.android.windvane.mini.app.plugin.ui.databinding.FragmentAuthorizeSettingsBinding;
import com.alibaba.emas.android.windvane.mini.app.plugin.ui.util.AuthorizeMessageUtil;
import com.alibaba.emas.android.windvane.mini.app.plugin.ui.util.DividerItemDecoration;
import com.alibaba.emas.android.windvane.mini.app.pluginsdk.environment.PluginEnv;
import com.alibaba.emas.android.windvane.mini.app.pluginsdk.service.IAuthService;
import com.alibaba.emas.android.windvane.mini.app.pluginsdk.ui.misc.UIComponentParam;
import com.alibaba.emas.android.windvane.mini.app.pluginsdk.ui.misc.UIResultCodeConstants;
import com.alibaba.emas.android.windvane.mini.app.pluginsdk.ui.misc.UIResultDataUtil;
import com.alibaba.emas.android.windvane.mini.app.pluginsdk.ui.protocol.UIResultCallBack;
import com.alibaba.module.android.mini.app.api.MiniAppInfoConstants;

import org.json.JSONException;
import org.json.JSONObject;

import java.util.HashMap;
import java.util.List;

public class AuthorizeSettingsFragment extends Fragment {
    private static final String TAG = "AuthorizeSettingsFragment";
    private FragmentAuthorizeSettingsBinding binding;
    private ItemAdapter itemAdapter;
    private String authorizeData = null;
    private UIComponentParam uiComponentParam = null;
    private Context context = null;
    private String userId = null;
    private String appId = null;
    private List<String> authorizeTotalData = null;
    private String miniAppName = "MiniApp Demo";

    public static AuthorizeSettingsFragment newInstance(UIComponentParam uiComponentParam) {
        AuthorizeSettingsFragment fragment = new AuthorizeSettingsFragment();
        fragment.uiComponentParam = uiComponentParam;
        return fragment;
    }


    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (uiComponentParam != null) {
            HashMap<String, Object> params = uiComponentParam.getParams();
            userId = (String) params.get(MiniAppInfoConstants.USER_ID);
            appId = (String) params.get(MiniAppInfoConstants.MINI_APP_ID);
            context = uiComponentParam.getContext();
            miniAppName = (String) params.get(MiniAppInfoConstants.MINI_APP_NAME);
            IAuthService authService = PluginEnv.getInstance().getContainerContext().getAuthService();
            authorizeData = authService.queryAuthInfo(context, userId, appId);
        }
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        binding = FragmentAuthorizeSettingsBinding.inflate(inflater, container, false);
        return binding.getRoot();
    }


    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        binding.tvAuthorizeSetting.setText(getString(R.string.mini_app_more_authorize_setting_des) + miniAppName + getString(R.string.mini_app_more_authorize_setting_des_end));
        setupRecyclerView();
        binding.flMiniAppNavigationBarBack.setOnClickListener(view1 -> {
            UIResultCallBack uiResultCallBack = uiComponentParam.getUiResultCallBack();
            if (uiResultCallBack != null) {
                uiResultCallBack.onComplete(UIResultDataUtil.getResultData(UIResultCodeConstants.CANCEL));
            }
        });
    }

    private void setupRecyclerView() {
        authorizeTotalData = AuthorizeMessageUtil.getAuthorizeData();
        itemAdapter = new AuthorizeSettingsFragment.ItemAdapter(authorizeTotalData);
        binding.rlAuthorizeSetting.setLayoutManager(new LinearLayoutManager(getContext()));
        binding.rlAuthorizeSetting.addItemDecoration(new DividerItemDecoration(2, Color.parseColor("#EEEEEE")));
        binding.rlAuthorizeSetting.setAdapter(itemAdapter);
    }

    @Override
    public void onStart() {
        super.onStart();

    }

    @Override
    public void onResume() {
        super.onResume();
    }


    @Override
    public void onDestroyView() {
        super.onDestroyView();
        binding = null;
    }

    private class ItemAdapter extends RecyclerView.Adapter<AuthorizeSettingsFragment.ItemAdapter.ViewHolder> {
        private List<String> items;

        public ItemAdapter(List<String> items) {
            this.items = items;
        }

        @NonNull
        @Override
        public AuthorizeSettingsFragment.ItemAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            FragmentAuthorizeSettingItemBinding itemBinding = FragmentAuthorizeSettingItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
            return new AuthorizeSettingsFragment.ItemAdapter.ViewHolder(itemBinding);
        }

        @Override
        public void onBindViewHolder(@NonNull AuthorizeSettingsFragment.ItemAdapter.ViewHolder holder, int position) {
            String authorize = items.get(position);
            holder.binding.tvAuthorizeName.setText(AuthorizeMessageUtil.getAuthorizeName(authorize, getContext()));
            try {
                JSONObject jsonAuthorize = new JSONObject(authorizeData);
                holder.binding.cbAuthorize.setChecked(jsonAuthorize.optBoolean(authorize, false));
            } catch (JSONException e) {
                MiniAppLog.d(TAG,"json pares error ");
            }
            holder.binding.cbAuthorize.setOnCheckedChangeListener((compoundButton, isChecked) -> {
                IAuthService authService = PluginEnv.getInstance().getContainerContext().getAuthService();
                if (isChecked) {
                    if (authorize != null) {
                        boolean b = authService.saveAuth(context, authorizeTotalData.get(position), userId, appId);
                        if (!b) {
                            holder.binding.cbAuthorize.setChecked(false);
                        }

                    }
                } else {
                    if (authorize != null) {
                        boolean b = authService.revokeAuth(context, authorizeTotalData.get(position), userId, appId);
                        if (!b) {
                            holder.binding.cbAuthorize.setChecked(true);
                        }
                    }
                }

            });
           

        }

        @Override
        public int getItemCount() {
            return items.size();
        }

        class ViewHolder extends RecyclerView.ViewHolder {
            FragmentAuthorizeSettingItemBinding binding;

            public ViewHolder(FragmentAuthorizeSettingItemBinding binding) {
                super(binding.getRoot());
                this.binding = binding;
            }
        }
    }
}
7. Implementasi FavoriteFragment
import android.app.Dialog;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;

import com.alibaba.emas.android.windvane.mini.app.plugin.ui.R;
import com.alibaba.emas.android.windvane.mini.app.plugin.ui.databinding.FragmentAboutUsBinding;
import com.alibaba.emas.android.windvane.mini.app.plugin.ui.databinding.FragmentAddFavoriteBinding;
import com.alibaba.emas.android.windvane.mini.app.pluginsdk.environment.PluginEnv;
import com.alibaba.emas.android.windvane.mini.app.pluginsdk.service.IMoreService;
import com.alibaba.emas.android.windvane.mini.app.pluginsdk.ui.misc.UIComponentParam;
import com.alibaba.emas.android.windvane.mini.app.pluginsdk.ui.misc.UIResultCodeConstants;
import com.alibaba.emas.android.windvane.mini.app.pluginsdk.ui.misc.UIResultDataUtil;
import com.alibaba.emas.android.windvane.mini.app.pluginsdk.ui.protocol.UIResultCallBack;
import com.squareup.picasso.Picasso;

import org.json.JSONException;
import org.json.JSONObject;

import java.util.HashMap;

public class AddFavoriteFragment extends BaseDialogFragment {
    private FragmentAddFavoriteBinding binding;
    private JSONObject miniAppInfo = null;
    private UIComponentParam uiComponentParam = null;

    public static AddFavoriteFragment newInstance(UIComponentParam uiComponentParam) {
        AddFavoriteFragment fragment = new AddFavoriteFragment();
        fragment.uiComponentParam = uiComponentParam;
        return fragment;
    }

    @NonNull
    @Override
    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
        Dialog dialog = new Dialog(requireContext(), R.style.BottomDialogTheme);
        binding = FragmentAddFavoriteBinding.inflate(getLayoutInflater());
        View contentView = binding.getRoot();
        dialog.setContentView(contentView);
        Window window = dialog.getWindow();
        if (window != null) {
            window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
            window.setGravity(Gravity.BOTTOM);
            window.getAttributes().windowAnimations = R.style.mini_app_bottom_dialog_anim;
        }
        dialog.setCanceledOnTouchOutside(false);
        dialog.setCancelable(false);
        return dialog;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        HashMap<String, Object> params = uiComponentParam.getParams();
        String miniInfo = (String) params.get("miniAppInfo");
        if (!TextUtils.isEmpty(miniInfo)) {
            try {
                miniAppInfo = new JSONObject(miniInfo);
            } catch (JSONException e) {
                throw new RuntimeException(e);
            }
        }
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        binding = FragmentAddFavoriteBinding.inflate(inflater, container, false);
        return binding.getRoot();
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        View parent = (View) view.getParent();
        ViewGroup.LayoutParams params = parent.getLayoutParams();
        params.height = (int) (getResources().getDisplayMetrics().heightPixels * 0.32);
        parent.setLayoutParams(params);
        if (miniAppInfo != null) {
            String miniAppIcon = miniAppInfo.optString("miniAppIcon");
            String miniAppName = miniAppInfo.optString("miniAppName");
            setImage(binding.headerIcon, miniAppIcon);
            binding.headerTitle.setText(miniAppName + " " + getString(R.string.mini_app_authorize_new_apply));
        }
        binding.ivAboutUs.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (uiComponentParam != null) {
                    UIResultCallBack uiResultCallBack = uiComponentParam.getUiResultCallBack();
                    if (uiResultCallBack != null) {
                        uiResultCallBack.onComplete(UIResultDataUtil.getResultData(UIResultCodeConstants.OPEN_ABOUT));
                    }
                }
            }
        });
        binding.confirmButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                IMoreService moreService = PluginEnv.getInstance().getPluginContext().getContainer().getMoreService();
                if (moreService != null) {
                    if (!moreService.isFavorite(miniAppInfo.optString("miniAppId"))) {
                        moreService.addToFavorites(miniAppInfo.optString("miniAppId"), getActivity());
                    }
                }
                dismiss();
                UIResultCallBack uiResultCallBack = uiComponentParam.getUiResultCallBack();
                if (uiResultCallBack != null) {
                    uiResultCallBack.onComplete(UIResultDataUtil.getResultData(UIResultCodeConstants.EXIT_MINIAPP));
                }

            }
        });
        binding.cancelButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                dismiss();
                UIResultCallBack uiResultCallBack = uiComponentParam.getUiResultCallBack();
                if (uiResultCallBack != null) {
                    uiResultCallBack.onComplete(UIResultDataUtil.getResultData(UIResultCodeConstants.EXIT_MINIAPP));
                }

            }
        });
    }

    @Override
    public void onStart() {
        super.onStart();

    }

    @Override
    public void onResume() {
        super.onResume();
    }


    @Override
    public void onDestroyView() {
        super.onDestroyView();
        binding = null;
    }
}
8. Implementasi AboutUsFragment
import android.app.Dialog;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;

import com.alibaba.emas.android.mini.app.common.log.MiniAppLog;
import com.alibaba.emas.android.windvane.mini.app.plugin.ui.R;
import com.alibaba.emas.android.windvane.mini.app.plugin.ui.databinding.FragmentAboutUsBinding;
import com.alibaba.emas.android.windvane.mini.app.plugin.ui.databinding.FragmentAuthorizeBinding;
import com.alibaba.emas.android.windvane.mini.app.pluginsdk.ui.misc.UIComponentParam;
import com.alibaba.emas.android.windvane.mini.app.pluginsdk.ui.misc.UIResultCodeConstants;
import com.alibaba.emas.android.windvane.mini.app.pluginsdk.ui.misc.UIResultDataUtil;
import com.alibaba.emas.android.windvane.mini.app.pluginsdk.ui.protocol.UIResultCallBack;

import org.json.JSONException;
import org.json.JSONObject;

import java.util.HashMap;

public class AboutUsFragment extends DialogFragment {

    private FragmentAboutUsBinding binding;
    private JSONObject miniAppInfo = null;
    private UIComponentParam uiComponentParam = null;
    private UIResultCallBack uiResultCallBack = null;
    private static final String TAG = "AboutUsFragment";

    public static AboutUsFragment newInstance(UIComponentParam uiComponentParam) {
        AboutUsFragment fragment = new AboutUsFragment();
        fragment.uiComponentParam = uiComponentParam;
        return fragment;
    }


    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getData();
    }

    private void getData() {
        if (uiComponentParam != null) {
            HashMap<String, Object> params = uiComponentParam.getParams();
            uiResultCallBack = uiComponentParam.getUiResultCallBack();
            if (params != null) {
                String miniAppInfo = (String) params.get("miniAppInfo");
                if (!TextUtils.isEmpty(miniAppInfo)) {
                    try {
                        this.miniAppInfo = new JSONObject(miniAppInfo);
                    } catch (JSONException e) {
                        MiniAppLog.d(TAG, "miniAppInfo is null");
                    }
                }
            }
        }
    }

    @NonNull
    @Override
    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
        Dialog dialog = new Dialog(requireContext(), R.style.BottomDialogTheme);
        binding = FragmentAboutUsBinding.inflate(getLayoutInflater());
        View contentView = binding.getRoot();
        dialog.setContentView(contentView);
        Window window = dialog.getWindow();
        if (window != null) {
            window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
            window.setGravity(Gravity.BOTTOM);
            window.getAttributes().windowAnimations = R.style.mini_app_bottom_dialog_anim;
        }
        dialog.setCanceledOnTouchOutside(false);
        dialog.setCancelable(false);
        return dialog;
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        binding = FragmentAboutUsBinding.inflate(inflater, container, false);
        return binding.getRoot();
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        if (miniAppInfo != null) {
            binding.tvNameDes.setText(miniAppInfo.optString("miniAppName"));
            binding.tvSloganDes.setText(miniAppInfo.optString("miniAppSlogan"));
            binding.tvVersionDes.setText(miniAppInfo.optString("miniAppVersion"));
            binding.tvDescription.setText(miniAppInfo.optString("miniAppDescription"));
        }
        binding.ivBack.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (uiResultCallBack != null) {
                    uiResultCallBack.onComplete(UIResultDataUtil.getResultData(UIResultCodeConstants.CANCEL));
                }
            }
        });
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        binding = null;
    }

}

3.1.3.2 Implementasi iOS
1. buat UIPlugin kustom client
#import "EMASClientCustomUIPlugin.h"
#import <EMASMiniAppService/EMASIUIExtension.h>
#import <EMASMiniAppService/EMASUIComponentParam.h>
#import <EMASMiniAppService/EMASResultCodeConstants.h>
#import "EMASAuthUIViewController.h"
#import "CPErrorPage.h"
#import "CPLoadPage.h"
#import "CPMenuController.h"
#import "CPAboutUsViewController.h"
#import "CPComfirmViewController.h"
#import "CPSettingPage.h"

@interface EMASClientCustomUIPlugin()<EMASIUIExtension>

@end

@implementation EMASClientCustomUIPlugin

- (UIViewController *)createUserDeviceInfoAuthDialogViewController:(EMASUIComponentParam *)uiComponentParam {

    NSDictionary *authParams = uiComponentParam.params;

    NSDictionary *authDic = authParams[@"authorize"];
    //scope yang sudah diotorisasi
    NSMutableDictionary *authedScopeDic = [[NSMutableDictionary alloc] init];
    //scope yang perlu diotorisasi (menyaring scope yang tidak dikenal)
    NSMutableArray * filterArray = [[NSMutableArray alloc] init];
    for (NSString *key in authDic.allKeys) {
        if ([[authDic objectForKey:key] boolValue]) {
            [authedScopeDic setObject:[authDic objectForKey:key] forKey:key];
        }else {
            [filterArray addObject:key];
        }
    }

    NSDictionary *filterDic = @{@"authorize": filterArray, @"appId": authParams[@"appId"], @"userId": authParams[@"userId"], @"miniAppInfo": authParams[@"miniAppInfo"], @"authed": authedScopeDic};

    EMASAuthUIViewController *vc = [[EMASAuthUIViewController alloc] init];
    [vc setMiniAppAuth:filterDic];
    vc.completionBlock = uiComponentParam.completionBlock;
    return vc;
}

- (UIViewController *)createErrorPageViewController:(EMASUIComponentParam *)uiComponentParam {
    CPErrorPage* page = [CPErrorPage new];
    [page miniAppErrorInfo:uiComponentParam.params];
    [page setComplete:^(int index) {
        if (index == 0) {
            if (uiComponentParam.completionBlock) {
                uiComponentParam.completionBlock(@{@"code": EMAS_EXIT_MINIAPP});
            }
        }else if(index == 1){
            if (uiComponentParam.completionBlock) {
                uiComponentParam.completionBlock(@{@"code": EMAS_RESTART_MINIAPP});
            }
        }
    }];
    return page;


}

- (UIViewController *)createSplashPageViewController:(EMASUIComponentParam *)uiComponentParam {
    CPLoadPage* page =  [CPLoadPage new];
    [page miniAppInfo:uiComponentParam.params];
    return page;
}

-(UIViewController*)createMoreDialogViewController:(EMASUIComponentParam *)uiComponentParam{

    CPMenuController* moreVC = [CPMenuController new];
    [moreVC miniAppInfo:uiComponentParam.params];
    moreVC.completionBlock = uiComponentParam.completionBlock;
    return moreVC;
}

- (UIViewController *)createAboutUsDialogViewController:(EMASUIComponentParam *)uiComponentParam {
    CPAboutUsViewController *aboutUsVC = [[CPAboutUsViewController alloc] init];
    [aboutUsVC miniAppInfo:uiComponentParam.params];
    aboutUsVC.completionBlock = uiComponentParam.completionBlock;
    return aboutUsVC;
}

- (UIViewController *)createFavoriteDialogViewController:(EMASUIComponentParam *)uiComponentParam {
    CPComfirmViewController *favoriteVC = [[CPComfirmViewController alloc] init];
    CPCfmViewConfig *config = [[CPCfmViewConfig alloc] init];
    config.miniAppInfo = uiComponentParam.params;
    favoriteVC.completionBlock = uiComponentParam.completionBlock;
    favoriteVC.config = config;
    return favoriteVC;
}

- (UIViewController *)createSettingsDialogViewController:(EMASUIComponentParam *)uiComponentParam {
    CPSettingPage* settingsVC = [CPSettingPage new];
    [settingsVC miniAppInfo:uiComponentParam.params];
    settingsVC.completionBlock = uiComponentParam.completionBlock;
    return settingsVC;
}

@end
2. Implementasi UserDeviceInfoAuthDialogViewController
#import "EMASAuthUIViewController.h"
#import "EMASAuthUITopInfoView.h"
#import "UIColor+SAHexString.h"
#import "EMASDefaultAuthTabCell.h"
#import "EMASAuthInfoTabCell.h"
#import "EMASAuthorizeInfoModel.h"
#import "UIColor+SAHexString.h"
#import "EMASUIUtils.h"
#import <EMASMiniAppService/EMASPluginEnv.h>
#import <EMASMiniAppService/EMASIAuthService.h>
#import <EMASMiniAppService/EMASUIComponentParam.h>
#import <EMASMiniAppService/EMASResultCodeConstants.h>

static NSString * const defaultIdentifier = @"defalultIdentifier";
static NSString * const authIdentifier = @"authIdentifier";

@interface EMASAuthUIViewController ()<UITableViewDelegate, UITableViewDataSource>

@property (nonatomic, strong) UIView *containerView;
@property (nonatomic, strong) EMASAuthUITopInfoView *topView;
@property (nonatomic, strong) UILabel *authTipLabel;
@property (nonatomic, strong) UIButton *cancelBtn;
@property (nonatomic, strong) UIButton *sureBtn;
@property (nonatomic, strong) UITableView *authTabView;

@property (nonatomic, strong) NSMutableArray *filterScopes;

@property (nonatomic, strong) NSMutableArray *scopeAuthArray;

@property (nonatomic, assign) BOOL isMultiple;

@property (nonatomic, strong) NSDictionary *scopeDic;
@property (nonatomic, strong)NSDictionary *scopeTipDic;
@property (nonatomic, strong) NSDictionary *permissionDic;
//informasi miniapp
@property (nonatomic, strong) NSDictionary *miniAppInfo;
//appid miniapp
@property (nonatomic, strong) NSString *appId;
//userid
@property (nonatomic, strong)NSString *userId;

//memperbarui tata letak tinggi tableview
@property (nonatomic, strong)NSLayoutConstraint *authTabHeightConstraint;
@property (nonatomic, strong)NSLayoutConstraint *cancelBottomConstraint;

@property (nonatomic, strong) NSDictionary *unknownAuthDic;
@property (nonatomic, strong) NSDictionary *authedDic;

@end

@implementation EMASAuthUIViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Lakukan setup tambahan setelah memuat tampilan.
    self.view.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.5];
    [self initDefaultAuthInfo];
    [self initSubViewUI];
    self.containerView.layer.cornerRadius = 12.0;
    self.containerView.layer.masksToBounds = YES;
    [self.topView setMiniAppInfo:self.miniAppInfo];
    [self updateSubViewLayout];
    [self getScopeAuthInfo];
    [self updateAuthTipInfo];
    [self.authTabView reloadData];

}

- (void)initSubViewUI {
    self.containerView.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:self.containerView];
    self.topView.translatesAutoresizingMaskIntoConstraints = NO;
    [self.containerView addSubview:self.topView];
    self.authTipLabel.translatesAutoresizingMaskIntoConstraints = NO;
    [self.containerView addSubview:self.authTipLabel];
    self.authTabView.translatesAutoresizingMaskIntoConstraints = NO;
    [self.containerView addSubview:self.authTabView];
    self.authTabView.delegate = self;
    self.authTabView.dataSource = self;
    self.authTabView.rowHeight = UITableViewAutomaticDimension;
    self.authTabView.estimatedRowHeight = 70;
    self.authTabView.estimatedSectionHeaderHeight = 0;
    self.authTabView.estimatedSectionFooterHeight = 0;
    if (@available(iOS 11.0, *)) {
        self.authTabView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
    }
    self.authTabView.scrollEnabled = NO;
    self.cancelBtn.translatesAutoresizingMaskIntoConstraints = NO;
    [self.containerView addSubview:self.cancelBtn];
    self.sureBtn.translatesAutoresizingMaskIntoConstraints = NO;
    [self.containerView addSubview:self.sureBtn];
    [self.cancelBtn addTarget:self action:@selector(clickToCancelAuth:) forControlEvents:UIControlEventTouchUpInside];
    [self.sureBtn addTarget:self action:@selector(clickToAllowAuth:) forControlEvents:UIControlEventTouchUpInside];
    [self addSubViewLayout];
    [self setDefaultStyle];
}

- (void)clickToCancelAuth:(UIButton *)btn {
    
    if (self.completionBlock) {
        NSDictionary *dic = @{
            @"code": EMAS_CANCEL,
            @"msg": @"User cancels authorization"
        };
        self.completionBlock(dic);
    }
    
}

- (void)clickToAllowAuth:(UIButton *)btn {
    
    NSMutableArray *array = [[NSMutableArray alloc] init];
    for (EMASAuthorizeInfoModel *model in self.scopeAuthArray) {
        if (model.isSelect) {
            [array addObject:model.scope];
        }
    }
    //tidak ada nilai yang perlu diotorisasi sementara
    if (array.count == 0) {
        //informasi teks dan tampilan--logika pemrosesan
        return;
    }
    id<EMASIAuthService> authService = EMASPluginEnv.shareInstance.getContainerContext.getAuthService;
    if (authService && [authService respondsToSelector:@selector(saveAuthList:userId:miniAppId:)]) {
        BOOL isSuccess = [authService saveAuthList:array userId:self.userId miniAppId:self.appId];
        
        NSMutableDictionary *scopDic = [[NSMutableDictionary alloc] init];
        for (EMASAuthorizeInfoModel *model in self.scopeAuthArray) {
            BOOL isAuth = isSuccess ? model.isSelect : isSuccess;
            [scopDic setObject:@(isAuth) forKey:model.scope];
        }
        
        if (self.authedDic && self.authedDic.allKeys.count != 0) {
            [scopDic addEntriesFromDictionary:self.authedDic];
        }
        
        if (self.unknownAuthDic && self.unknownAuthDic.allKeys.count != 0) {
            [scopDic addEntriesFromDictionary:self.unknownAuthDic];
        }
        if (self.completionBlock) {
            NSDictionary *dic = @{
                @"code": EMAS_CONFIRM,
                @"msg": @"",
                @"data": scopDic
            };
            self.completionBlock(dic);
        }
       
    }
    
}

- (void)addSubViewLayout {
    
    // Tinggi tabel default diatur menjadi 60
    self.authTabHeightConstraint = [self.authTabView.heightAnchor constraintEqualToConstant:60];
    
    [NSLayoutConstraint activateConstraints:@[
        [self.containerView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:0],
        [self.containerView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-0],
        [self.containerView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor constant:0],
        
        [self.topView.leadingAnchor constraintEqualToAnchor:self.containerView.leadingAnchor constant:24],
        [self.topView.trailingAnchor constraintEqualToAnchor:self.containerView.trailingAnchor constant:-24],
        [self.topView.topAnchor constraintEqualToAnchor:self.containerView.topAnchor constant:0],
        [self.topView.heightAnchor constraintEqualToConstant:56],
        
        [self.authTipLabel.leadingAnchor constraintEqualToAnchor:self.containerView.leadingAnchor constant:24],
        [self.authTipLabel.trailingAnchor constraintEqualToAnchor:self.containerView.trailingAnchor constant:-24],
        [self.authTipLabel.topAnchor constraintEqualToAnchor:self.topView.bottomAnchor constant:16],
        [self.authTipLabel.heightAnchor constraintEqualToConstant:22.0],
        
        [self.authTabView.leadingAnchor constraintEqualToAnchor:self.containerView.leadingAnchor constant:0],
        [self.authTabView.trailingAnchor constraintEqualToAnchor:self.containerView.trailingAnchor constant:0],
        [self.authTabView.topAnchor constraintEqualToAnchor:self.authTipLabel.bottomAnchor constant:0],
        self.authTabHeightConstraint,
        
        [self.cancelBtn.topAnchor constraintEqualToAnchor:self.authTabView.bottomAnchor constant:20],
        [self.cancelBtn.leadingAnchor constraintEqualToAnchor:self.containerView.leadingAnchor constant:57.5],
        [self.cancelBtn.widthAnchor constraintEqualToAnchor:self.sureBtn.widthAnchor],
        [self.cancelBtn.heightAnchor constraintEqualToConstant:40],
        [self.cancelBtn.bottomAnchor constraintEqualToAnchor:self.containerView.bottomAnchor constant:-54],
        
        [self.sureBtn.trailingAnchor constraintEqualToAnchor:self.containerView.trailingAnchor constant:-57.5],
        [self.sureBtn.heightAnchor constraintEqualToAnchor:self.cancelBtn.heightAnchor],
        [self.sureBtn.leadingAnchor constraintEqualToAnchor:self.cancelBtn.trailingAnchor constant:20],
        [self.sureBtn.centerYAnchor constraintEqualToAnchor:self.cancelBtn.centerYAnchor constant:0]
        
    ]];
}

- (void)updateSubViewLayout {
    CGFloat h = 56 + 16 + 22 + 20 + 43 + 54;
    CGFloat max_tab_h = self.view.frame.size.height/5.0 * 4 - h;
    if (self.isMultiple) {
        CGFloat tab_h = 93 * self.filterScopes.count;
        
        self.authTabHeightConstraint.constant = tab_h < max_tab_h ? tab_h : max_tab_h;
        if (tab_h > max_tab_h) {
            self.authTabView.scrollEnabled = YES;
        }
        [UIView animateWithDuration:0.3 animations:^{
            [self.containerView layoutIfNeeded];
        }];
    }
    
}

- (void)updateAuthTipInfo {
    NSString *multipleTopInfo = [EMASUIUtils localizedStringForKey:@"miniapp_auth_tip" value:@"Access your following information"];
    NSString *AuthTipString = self.isMultiple ? multipleTopInfo : self.scopeTipDic[self.filterScopes.firstObject];
    self.authTipLabel.text = AuthTipString;
}

- (void)initDefaultAuthInfo {
    self.scopeTipDic = @{
        @"location": @"Access your location information",
        @"contacts": @"Access your contact list information",
        @"bluetooth": @"Access your bluetooth information",
        @"camera": @"Access your camera information",
        @"microphone": @"Access your microphone information",
        @"album":@"Access your photo album information",
        @"file": @"Access your file information",
        @"call": @"Access your call information",
        @"vibrate": @"Access your device vibration information",
        @"screen":@"Access your screen information"
    };
    
    self.permissionDic = @{
        @"location": @"Location",
        @"contacts": @"Contacts",
        @"bluetooth": @"Bluetooth",
        @"camera":@"Camera",
        @"microphone": @"Microphone",
        @"album":@"Album",
        @"file": "Files",
        @"call": @"Calls",
        @"vibrate": @"Vibration",
        @"screen":@"Screen"
    };
    
    self.scopeDic = @{
        @"location": @"This miniapp will access your location to provide address recommendations and localized services",
        @"contacts": @"This miniapp will access your contacts to quickly find contacts or invite friends",
        @"bluetooth": @"This miniapp will access bluetooth to connect to smart devices and enable data interaction",
        @"camera": @"This miniapp will access your camera for taking photos or scanning QR codes",
        @"microphone": @"This miniapp will access your microphone for voice input and voice-based features",
        @"album": @"This miniapp will access your photo album to select and upload images for sharing or editing",
        @"file": @"This miniapp will access files to read and manage documents or resource files required by the application",
        @"call": @"This miniapp will access call logs to identify incoming calls or enhance communication-related features",
        @"vibrate": @"This miniapp will access device vibration controls to provide tactile feedback and enhance operational experience",
        @"screen": @"This miniapp will access screen information to adapt interface display, support screenshot and screen recording features, and improve user interaction"
    };
    
}

- (void)setDefaultStyle {
    self.cancelBtn.backgroundColor = [UIColor colorWithHexString:@"#F1F2F3" alpha:1.0];
    self.sureBtn.backgroundColor = [UIColor colorWithHexString:@"#2561F5" alpha:1.0];
    self.cancelBtn.layer.cornerRadius = 4.0;
    self.cancelBtn.layer.masksToBounds = YES;
    self.sureBtn.layer.cornerRadius = 4.0;
    self.sureBtn.layer.masksToBounds = YES;
    self.cancelBtn.titleLabel.font = [UIFont systemFontOfSize:17.0];
    self.sureBtn.titleLabel.font = [UIFont systemFontOfSize:17.0];
    [self.cancelBtn setTitleColor:[UIColor colorWithHexString:@"#2561F5" alpha:1.0] forState:UIControlStateNormal];
    [self.sureBtn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
    NSString *cancelTitle = @"Reject";
    [self.cancelBtn setTitle:cancelTitle forState:UIControlStateNormal];
    NSString *sureTitle = @"Allow";
    [self.sureBtn setTitle:sureTitle forState:UIControlStateNormal];
    
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.scopeAuthArray.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    if (_isMultiple) {
        EMASAuthInfoTabCell *cell = [tableView dequeueReusableCellWithIdentifier:authIdentifier];
        if (!cell) {
            cell = [[EMASAuthInfoTabCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:authIdentifier];
            cell.selectionStyle = UITableViewCellSelectionStyleNone;
            
        }
        cell.authModel = self.scopeAuthArray[indexPath.row];
        return cell;
    }
    EMASDefaultAuthTabCell *cell = [tableView dequeueReusableCellWithIdentifier:defaultIdentifier];
    if (!cell) {
        cell = [[EMASDefaultAuthTabCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:defaultIdentifier];
        cell.selectionStyle = UITableViewCellSelectionStyleNone;
        
    }
    EMASAuthorizeInfoModel *model = self.scopeAuthArray[indexPath.row];
    cell.authInfo = model.authInfo;
    return cell;
    
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [tableView deselectRowAtIndexPath:indexPath animated:true];
    if (self.isMultiple) {
        EMASAuthorizeInfoModel *model = self.scopeAuthArray[indexPath.row];
        model.isSelect = !model.isSelect;
        [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
        [self updateAuthButtonState];
    }
}

- (void)updateAuthButtonState {
    BOOL isContainerAuth = NO;
    for (EMASAuthorizeInfoModel *model in self.scopeAuthArray) {
        if (model.isSelect) {
            isContainerAuth = YES;
            break;
        }
    }
    if (isContainerAuth) {
        self.sureBtn.alpha = 1.0;
        self.sureBtn.userInteractionEnabled = YES;
        
    }else {
        self.sureBtn.alpha = 0.3;
        self.sureBtn.userInteractionEnabled = NO;
    }
}


- (void)setMiniAppAuth:(NSDictionary *)params {
    NSArray *scopes = params[@"authorize"];
    self.miniAppInfo = params[@"miniAppInfo"];
    self.appId = params[@"appId"];
    self.userId = params[@"userId"];
    self.unknownAuthDic = params[@"unknownAuth"];
    self.authedDic = params[@"authed"];
    [self.filterScopes removeAllObjects];
    for (NSString *scope in scopes) {
        [self.filterScopes addObject:scope];
    }
    _isMultiple = self.filterScopes.count != 1;
    
    
}

- (void)getScopeAuthInfo {
    
    [self.scopeAuthArray removeAllObjects];
    for (NSString *scope in self.filterScopes) {
        EMASAuthorizeInfoModel *model = [[EMASAuthorizeInfoModel alloc] init];
        model.isSelect = YES;
        model.captialScope = self.permissionDic[scope];
        model.authInfo = self.scopeDic[scope];
        model.scopeTips = self.scopeTipDic[scope];
        model.scope = scope;
        [self.scopeAuthArray addObject:model];
    }
}



- (NSMutableArray *)filterScopes {
    if (!_filterScopes) {
        _filterScopes = [[NSMutableArray alloc] init];
    }
    return _filterScopes;
}

- (NSMutableArray *)scopeAuthArray {
    if (!_scopeAuthArray) {
        _scopeAuthArray = [[NSMutableArray alloc] init];
    }
    return _scopeAuthArray;
}

- (UIView *)containerView {
    if (!_containerView) {
        _containerView = [[UIView alloc] init];
        _containerView.backgroundColor = [UIColor whiteColor];
    }
    return _containerView;
}

- (EMASAuthUITopInfoView *)topView {
    if (!_topView) {
        _topView = [[EMASAuthUITopInfoView alloc] init];
        
    }
    return _topView;
}

- (UILabel *)authTipLabel {
    if (!_authTipLabel) {
        _authTipLabel = [[UILabel alloc] init];
        _authTipLabel.font = [UIFont systemFontOfSize:17.0];
        _authTipLabel.textColor = [UIColor colorWithHexString:@"#1F2024" alpha:1.0];
    }
    return _authTipLabel;
}

- (UITableView *)authTabView {
    if (!_authTabView) {
        _authTabView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
        _authTabView.backgroundColor = [UIColor whiteColor];
        _authTabView.separatorStyle = UITableViewCellSeparatorStyleNone;
    }
    return _authTabView;
}

- (UIButton *)cancelBtn {
    if (!_cancelBtn) {
        _cancelBtn = [UIButton buttonWithType:UIButtonTypeCustom];
    }
    return _cancelBtn;
}

- (UIButton *)sureBtn {
    if (!_sureBtn) {
        _sureBtn = [UIButton buttonWithType:UIButtonTypeCustom];
    }
    return _sureBtn;
}

@end
3. Implementasi ErrorPageViewController
#import "CPErrorPage.h"
#import "CPCommonTool.h"
#import <EMASMiniAppApi/EMASMiniAppConstants.h>

@interface CPErrorPage ()
@property (nonatomic,strong) UILabel *titleLabel;
@property (nonatomic,strong) UILabel *errorLabel;
@property (nonatomic,strong) UIButton *refreshBtn;
@property (nonatomic,strong) UIButton *quitBtn;
@property (nonatomic,strong) UIImageView *mainImg;

@end

@implementation CPErrorPage

- (void)viewDidLoad {
    [super viewDidLoad];
    // Lakukan setup tambahan setelah memuat tampilan.
    self.view.backgroundColor = UIColor.whiteColor;
    [self setupErrorView];
    [self loadData];
}
-(void)loadData{

    if (self.config) {
        CPErrorViewConfig* cnf = self.config;
        if(kStrExist(cnf.mainIconStr)){
            self.mainImg.image =[CPCommonTool getImageByName:cnf.mainIconStr];
        }
        if(kStrExist(cnf.errorTxt)){
            [self.titleLabel setText:cnf.errorTxt];
        }

        if(kStrExist(cnf.errorCodeTxt)){
            [self.errorLabel setText:cnf.errorCodeTxt];
        }

        if(kStrExist(cnf.refreshTxt)){
            [self.refreshBtn setTitle:cnf.refreshTxt forState:(UIControlStateNormal)];
        }

        if(kStrExist(cnf.quitTxt)){
            [self.quitBtn setTitle:cnf.quitTxt forState:(UIControlStateNormal)];
        }
        self.quitBtn.hidden = !cnf.showQuitBtn;
        self.refreshBtn.hidden = !cnf.showRefreshBtn;
    }else{
        CPErrorViewConfig* cnf = [CPErrorViewConfig new];
        self.config = cnf;
    }
    //sesuaikan constraint berdasarkan kondisi
    [self updateConstraints];



}


-(void)setupErrorView{


    // 1. Buat tampilan kontainer
    UIView* container = [UIView new];
    container.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:container];

    // 2. Buat gambar kesalahan dan tambahkan ke kontainer
    UIImageView *errorImage = [[UIImageView alloc] initWithImage:[CPCommonTool getImageByName:@"app_load_failure"]];
    errorImage.contentMode = UIViewContentModeScaleAspectFit;
    errorImage.translatesAutoresizingMaskIntoConstraints = NO;
    self.mainImg = errorImage;
    [container addSubview:errorImage]; // Tambahkan ke kontainer

    // 3. Buat label petunjuk kesalahan dan tambahkan ke kontainer
    UILabel *errorLabel = self.titleLabel;
    errorLabel.text = kLocalString(@"app_failed_to_load");
    errorLabel.textAlignment = NSTextAlignmentCenter;
    errorLabel.textColor = kColorWithHex(@"#474A52");
    errorLabel.font = kFont(16);
    errorLabel.translatesAutoresizingMaskIntoConstraints = NO;
    [container addSubview:errorLabel]; // Tambahkan ke kontainer


    // constraint label kode kesalahan
    UILabel *codeLabel = self.errorLabel;
    codeLabel.text = [NSString stringWithFormat:@"%@:0000",kLocalString(@"error_code")];
    codeLabel.textAlignment = NSTextAlignmentCenter;
    codeLabel.textColor = kColorWithHex(@"#81848F");
    codeLabel.font = kFont(13);
    codeLabel.translatesAutoresizingMaskIntoConstraints = NO;
    [container addSubview:codeLabel];


    // Buat tombol refresh
    UIButton *refreshButton = self.refreshBtn;
        [refreshButton setTitle:kLocalString(@"miniapp_error_refresh_btn_title") forState:UIControlStateNormal];
        [refreshButton addTarget:self action:@selector(refreshAction) forControlEvents:UIControlEventTouchUpInside];
        [container addSubview:refreshButton];

        // Buat tombol keluar
        UIButton *exitButton = self.quitBtn;
        [exitButton setTitle:kLocalString(@"miniapp_error_exit_btn_title") forState:UIControlStateNormal];
        [exitButton addTarget:self action:@selector(exitAction) forControlEvents:UIControlEventTouchUpInside];
        [container addSubview:exitButton];

   
        
        // 4. Atur constraint
        [NSLayoutConstraint activateConstraints:@[
            // constraint kontainer
            [container.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
            [container.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor constant:-hPix(100)],
            [container.widthAnchor constraintEqualToAnchor:self.view.widthAnchor multiplier:0.8],
            
            // constraint gambar kesalahan
            [errorImage.topAnchor constraintEqualToAnchor:container.topAnchor],
            [errorImage.centerXAnchor constraintEqualToAnchor:container.centerXAnchor],
            [errorImage.widthAnchor constraintEqualToConstant:wPix(120)],
            [errorImage.heightAnchor constraintEqualToConstant:wPix(120)],
            
            // constraint label kesalahan
            [errorLabel.topAnchor constraintEqualToAnchor:errorImage.bottomAnchor constant:hPix(16)],
            [errorLabel.leadingAnchor constraintEqualToAnchor:container.leadingAnchor],
            [errorLabel.trailingAnchor constraintEqualToAnchor:container.trailingAnchor],
            
            
            // constraint label kode kesalahan
            [codeLabel.topAnchor constraintEqualToAnchor:errorLabel.bottomAnchor constant:hPix(8)],
            [codeLabel.centerXAnchor constraintEqualToAnchor:container.centerXAnchor],
            
            
        ]];
    
    
}

-(void)updateConstraints{
    
    CPErrorViewConfig* cnf = self.config;
    UIButton* refreshButton = self.refreshBtn;
    UIButton* exitButton = self.quitBtn;
    UILabel* codeLabel = self.errorLabel;
    UIView* container = self.refreshBtn.superview;
    NSMutableArray* arr = @[].mutableCopy;
    CGFloat btn_wid = wPix(120);
    CGFloat btn_hei = hPix(40);
    if (cnf.showRefreshBtn) {//showQuitBtn
        if (cnf.showQuitBtn) {//showRefreshBtn
            [arr addObjectsFromArray:@[
                [exitButton.topAnchor constraintEqualToAnchor:codeLabel.bottomAnchor constant:hPix(40)],
                [exitButton.trailingAnchor constraintEqualToAnchor:container.centerXAnchor constant:-10],
                [exitButton.widthAnchor constraintEqualToConstant:btn_wid],
                [exitButton.heightAnchor constraintEqualToConstant:btn_hei],
                [refreshButton.leadingAnchor constraintEqualToAnchor:container.centerXAnchor constant:10],
                [container.bottomAnchor constraintEqualToAnchor:exitButton.bottomAnchor constant:0]
            ]];
        }else{
            [arr addObjectsFromArray:@[
                [refreshButton.centerXAnchor constraintEqualToAnchor:container.centerXAnchor constant:0],
                [container.bottomAnchor constraintEqualToAnchor:refreshButton.bottomAnchor constant:0]
            ]];
        }
        [arr addObjectsFromArray:@[
            [refreshButton.topAnchor constraintEqualToAnchor:codeLabel.bottomAnchor constant:hPix(40)],
            [refreshButton.widthAnchor constraintEqualToConstant:btn_wid],
            [refreshButton.heightAnchor constraintEqualToConstant:btn_hei],
        ]];
    }else{
        if (cnf.showQuitBtn) {
            [arr addObjectsFromArray:@[
                [exitButton.topAnchor constraintEqualToAnchor:codeLabel.bottomAnchor constant:hPix(40)],
                [exitButton.centerXAnchor constraintEqualToAnchor:container.centerXAnchor constant:0],
                [exitButton.widthAnchor constraintEqualToConstant:btn_wid],
                [exitButton.heightAnchor constraintEqualToConstant:btn_hei],
                [container.bottomAnchor constraintEqualToAnchor:exitButton.bottomAnchor constant:0]
            ]];
        }else{
            [arr addObject: [container.bottomAnchor constraintEqualToAnchor:codeLabel.bottomAnchor constant:0]];
        }
    }
    [NSLayoutConstraint activateConstraints:arr];
}


- (void)refreshAction {
    // Implementasi logika refresh
    if (self.complete) {
        self.complete(1);
    }
}

- (void)exitAction {
    // Implementasi logika keluar
    if (self.complete) {
        self.complete(0);
    }
}

#pragma mark -

- (void)miniAppErrorInfo:(NSDictionary *)params{
    CPErrorViewConfig* cfn = [CPErrorViewConfig new];
    if ([params objectForKey:@"ErrorCode"] == nil) {
        self.config = cfn;
        return;
    }
    int code =  [params[@"ErrorCode"] intValue];
    switch (code) {
        case EMAS_ERROR_CODE_NETWORK_ERROR://network exception
        case EMAS_ERROR_CODE_NETWORK_RESPONSE_EMPTY://network exception
        case EMAS_ERROR_CODE_NETWORK_RESPONSE_SUCCESS_FALSE://network exception
        case EMAS_ERROR_CODE_URL_VERSION_EMPTY://network exception
            {
                [cfn setErrorType:(CPErrorTypeConnectionError) errorCode:code];
            }
            break;
        case EMAS_ERROR_CODE_NETWORK_RESPONSE_FORMAT_ERROR://cannot open this page
            {
                [cfn setErrorType:(CPErrorTypeOpenError) errorCode:code];
            }
            break;
        case EMAS_ERROR_CODE_MINI_APP_DOWNLOAD_ERROR://miniapp load failed
        case EMAS_ERROR_CODE_MINI_APP_UNZIP_ERROR://miniapp load failed
        case EMAS_ERROR_CODE_MINI_APP_CACHE_ERROR://miniapp load failed
        case EMAS_ERROR_CODE_MINI_APP_TYPE_NOT_SUPPORT://load failed
        case EMAS_ERROR_CODE_WINDVANE_NOT_USED://load failed
        case EMAS_ERROR_CODE_UNIAPP_NOT_USED://miniapp load failed
            {
                [cfn setErrorType:(CPErrorTypeLoadError) errorCode:code];
            }
            break;
        case EMAS_ERROR_CODE_MINI_APP_TAKEN_OFF://miniapp does not exist
            {
                [cfn setErrorType:(CPErrorTypeOfflineError) errorCode:code];
            }
            break;
            
        default:
            break;
    }
    self.config = cfn;
}


#pragma mark - Setter&Getter
-(UILabel*)titleLabel{
    if (!_titleLabel) {
        _titleLabel = [UILabel new];
    }
    return _titleLabel;
}
-(UILabel*)errorLabel{
    if (!_errorLabel) {
        _errorLabel = [UILabel new];
    }
    return _errorLabel;
}
-(UIButton*)refreshBtn{
    if (!_refreshBtn) {
        _refreshBtn = [CPCommonTool getThemeDarkButton];
    }
    return _refreshBtn;
}
-(UIButton*)quitBtn{
    if (!_quitBtn) {
        _quitBtn = [CPCommonTool getThemeLightButton];
    }
    return _quitBtn;
}
@end

4. Implementasi SplashPageViewController:

Untuk mendapatkan kemajuan pemuatan dan informasi miniapp, Anda perlu mengimplementasikan protokol EMASISplashExtension.

#import "CPLoadPage.h"
#import "CPCommonTool.h"

@interface CPLoadPage ()
@property(nonatomic,strong)UIProgressView* progressView;
@property(nonatomic,strong)UILabel*percentLabel;
@property (nonatomic,strong)UILabel *titleLabel;
@property (nonatomic,strong)UIImageView *appIcon;

@end

@implementation CPLoadPage


- (void)viewDidLoad {
    [super viewDidLoad];
    // Lakukan setup tambahan setelah memuat tampilan.
    [self setupViews];
    [self loadData];

}


-(void)loadData{

    if (self.config) {
        CPLoadViewConfig* cnf = self.config;
        self.progressView.hidden = self.percentLabel.hidden = !cnf.showProgress;
        if (cnf.iconUrl) {
            [self.appIcon loadUrl:cnf.iconUrl];
        }
        if(kStrExist(cnf.appName)){
            [self.titleLabel setText:cnf.appName];
        }
        if (cnf.titleColor) {
            [self.titleLabel setTextColor:cnf.titleColor];
        }
        if (cnf.progressColor) {
            [self.progressView setTintColor:cnf.progressColor];
        }
    }

}

-(void)setupViews{

    self.view.backgroundColor = [UIColor whiteColor];

    // Buat tampilan kontainer
    UIView *loadContainer = [UIView new];
    loadContainer.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:loadContainer];

    // Tambahkan gambar
    UIImageView *imageView = [[UIImageView alloc] initWithImage:[CPCommonTool getImageByName:(@"miniapp_default_icon")]];
    self.appIcon = imageView;
    imageView.contentMode = UIViewContentModeScaleAspectFit;
    imageView.translatesAutoresizingMaskIntoConstraints = NO;
    [loadContainer addSubview:imageView];

    // Tambahkan label nama aplikasi
    UILabel *label = self.titleLabel;
    label.text = @"Miniapp Demo";
    label.font = kFontWithWeight(16,500);
    label.textAlignment = NSTextAlignmentCenter;
    label.translatesAutoresizingMaskIntoConstraints = NO;
    [loadContainer addSubview:label];

    // Tambahkan progress bar
    self.progressView.progress = 0;
    self.progressView.translatesAutoresizingMaskIntoConstraints = NO;
    self.progressView.tintColor = kColorWithHex(CP_THEME_COLOR);
    [loadContainer addSubview:self.progressView];

    // Tambahkan label persentase
    self.percentLabel.text = @"0%";
    self.percentLabel.textColor = [UIColor grayColor];
    self.percentLabel.font = kFontWithWeight(12,400);
    self.percentLabel.translatesAutoresizingMaskIntoConstraints = NO;
    [loadContainer addSubview:self.percentLabel];



    NSArray* arr = @[
        // constraint kontainer
        [loadContainer.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
        [loadContainer.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor constant:-64],
        [loadContainer.widthAnchor constraintEqualToAnchor:self.view.widthAnchor multiplier:1],

        // constraint gambar
        [imageView.topAnchor constraintEqualToAnchor:loadContainer.topAnchor],
        [imageView.centerXAnchor constraintEqualToAnchor:loadContainer.centerXAnchor],
        [imageView.widthAnchor constraintEqualToConstant:wPix(56)],
        [imageView.heightAnchor constraintEqualToConstant:wPix(56)],
        
        // constraint label nama aplikasi
        [label.topAnchor constraintEqualToAnchor:imageView.bottomAnchor constant:hPix(20)],
        [label.leadingAnchor constraintEqualToAnchor:loadContainer.leadingAnchor],
        [label.trailingAnchor constraintEqualToAnchor:loadContainer.trailingAnchor],
        
        // constraint progress bar
        [self.progressView.topAnchor constraintEqualToAnchor:label.bottomAnchor constant:hPix(22)],
        [self.progressView.centerXAnchor constraintEqualToAnchor:loadContainer.centerXAnchor],
        [self.progressView.widthAnchor constraintEqualToConstant:wPix(160)],
        
        // constraint label persentase
        [self.percentLabel.topAnchor constraintEqualToAnchor:self.progressView.bottomAnchor constant:hPix(8)],
        [self.percentLabel.centerXAnchor constraintEqualToAnchor:self.progressView.centerXAnchor],
        
        // constraint bawah kontainer
        [loadContainer.bottomAnchor constraintEqualToAnchor:self.progressView.bottomAnchor constant:0]
    ];
        // Atur constraint
        [NSLayoutConstraint activateConstraints:arr];
    
    
}

-(void)updateProgress:(CGFloat)progress{
    dispatch_async(dispatch_get_main_queue(), ^{
        [self.progressView setProgress:progress animated:YES];
        self.percentLabel.text = [NSString stringWithFormat:@"%0.0f%%",progress*100];
    });
}

#pragma mark - Setter&Getter

-(void)setConfig:(CPLoadViewConfig *)config{
    _config = config;
}

-(UIProgressView*)progressView{
    if (!_progressView) {
        _progressView = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleDefault];
    }
    return _progressView;
}

-(UILabel*)titleLabel{
    if (!_titleLabel) {
        _titleLabel = [UILabel new];
    }
    return _titleLabel;
}

-(UILabel*)percentLabel{
    if (!_percentLabel) {
        _percentLabel = [UILabel new];
    }
    return _percentLabel;
}

#pragma mark -

- (void)setMiniAppDownloadProgress:(CGFloat)progress{
    
    [self updateProgress:progress];
}
- (void)miniAppInfo:(NSDictionary *)params{
    
    dispatch_async(dispatch_get_main_queue(), ^{
        CPLoadViewConfig* cnf = [CPLoadViewConfig new];
        self.config = cnf;
        self.config.appName = params[@"miniAppName"];
        self.config.iconUrl = params[@"miniAppIcon"];
        
        [self loadData];
    });
}

@end
5. Implementasi MoreViewController
#import "CPMenuController.h"
#import "CPCommonTool.h"
#import "CPSettingPage.h"
#import "CPAboutUsViewController.h"
#import "CPComfirmViewController.h"
#import <EMASMiniAppService/EMASPluginEnv.h>
#import <EMASMiniAppService/EMASResultCodeConstants.h>
#import <EMASServiceManager/EMASServiceManager.h>
#import <EMASMiniAppApi/EMASMiniAppFavoriteService.h>
#import <EMASMiniAppApi/EMASMiniAppMoreService.h>

#define kPopupHeight hPix(355.0)
#define kCornerRadius 10.0
#define kIconSize 24.0
#define kItemSpacing 12.0
#define kItemWid wPix(72)
#define kItemHei hPix(94)
#define kCollectionHeight hPix(185.0)
#define kCancelButtonHeight hPix(56.0)
#define kCancelButtonBottomMargin hPix(34.0)

#define CPMENU_CELL_ICON_TAG 1122
#define CPMENU_CELL_ICON_CONTAINER_TAG 1234
#define CPMENU_CELL_TITLE_TAG 1123

typedef NS_ENUM(NSInteger, MenuItemKey) {
    MenuItemKeyAddToFav = 0,
    MenuItemKeySettings,
    MenuItemKeyCopyLink,
    MenuItemKeyShare,
    MenuItemKeyRefresh,
    MenuItemKeyAboutUs
    };


@interface CPMenuItem : NSObject
@property (nonatomic,copy) NSString *title;
@property (nonatomic,assign) MenuItemKey key;
@property (nonatomic,copy) NSString *imgName;
@property (nonatomic,assign)NSInteger status;
@property (nonatomic,copy) NSString *vice_title;
@property (nonatomic,copy) NSString *vice_imgName;
@property (nonatomic, assign) BOOL isAvailable;
@end

@implementation CPMenuItem

- (instancetype)init
{
    self = [super init];
    if (self) {
        self.status = 1;
    }
    return self;
}

@end


@interface CPMenuController () <UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>

@property (nonatomic, strong) UIView *containerView;
@property (nonatomic, strong) UICollectionView *collectionView;
@property (nonatomic, strong) NSArray<CPMenuItem*> *items;
@property (nonatomic, strong) UIButton *cancelButton;
@property (nonatomic,strong) UIImageView *appIcon;

@property (nonatomic, strong)NSDictionary *miniAppInfo;

@end

@implementation CPMenuController

- (void)viewDidLoad {
    [super viewDidLoad];

    [self setupData];
    [self setupUI];
    [self setupConstraints];


}

- (void)miniAppInfo:(NSDictionary *)info {
    self.miniAppInfo = info;
}


-(void)setupData{
    NSString *miniAppId = self.miniAppInfo[@"miniAppId"] ?: @"";
    BOOL favoriteAvailable = NO;
    BOOL isFavorite = NO;
    BOOL copyLinkAvailable = NO;
    BOOL shareAvailable = NO;
    BOOL settingsAvailable = NO;
    BOOL isCustomWindow = NO;
    BOOL refreshMiniAppAvailable = YES;
    id<EMASMiniAppFavoriteService> favService = [[EMASServiceManager sharedInstance] serviceForProtocol:@"EMASMiniAppFavoriteService"];
    if (favService) {
        favoriteAvailable = YES;
        if ([favService respondsToSelector:@selector(isFavorite:)]) {
            isFavorite = [favService isFavorite:miniAppId];
        }
    }
    NSString *miniAppUrl = self.miniAppInfo[@"miniAppUrl"] ?: @"";
    if (![miniAppUrl isEqualToString:@""]) {
        copyLinkAvailable = YES;
        settingsAvailable = YES;
    }
    
    id<EMASMiniAppMoreService> moreService = [[EMASServiceManager sharedInstance] serviceForProtocol:@"EMASMiniAppMoreService"];
    if (moreService && [moreService respondsToSelector:@selector(share:)]) {
        shareAvailable = YES;
    }
    
    NSMutableArray *mutableMenuItems = [[NSMutableArray alloc] init];
    if (favoriteAvailable) {
        if (isFavorite) {
            [mutableMenuItems addObject:@{@"title": kLocalString(@"miniapp_more_remove_favorite_info"), @"image": @"miniapp_more_remove_favorite", @"key": @(MenuItemKeyAddToFav), @"isAvailabel":@(favoriteAvailable)}];
        }else {
            [mutableMenuItems addObject:@{@"title": kLocalString(@"miniapp_more_add_favorite_info"), @"image": @"miniapp_more_add_favorite", @"key": @(MenuItemKeyAddToFav), @"isAvailabel":@(favoriteAvailable)}];
        }
    }
    if (settingsAvailable) {
        [mutableMenuItems addObject:@{@"title": kLocalString(@"miniapp_more_settings_info"), @"image": @"miniapp_more_settings", @"key": @(MenuItemKeySettings), @"isAvailabel":@(settingsAvailable)}];
    }
    if (copyLinkAvailable) {
        [mutableMenuItems addObject:@{@"title": kLocalString(@"miniapp_more_copy_link_info"), @"image": @"miniapp_more_copy_link", @"key": @(MenuItemKeyCopyLink),@"isAvailabel":@(copyLinkAvailable)}];
    }
    if (shareAvailable) {
        [mutableMenuItems addObject:@{@"title": kLocalString(@"miniapp_more_share_url_info"), @"image": @"miniapp_more_share_url", @"key": @(MenuItemKeyShare), @"isAvailabel":@(shareAvailable)}];
    }
    if (refreshMiniAppAvailable) {
        [mutableMenuItems addObject:@{@"title": kLocalString(@"miniapp_more_refresh_info"), @"image": @"miniapp_more_refresh", @"key": @(MenuItemKeyRefresh), @"isAvailabel":@(refreshMiniAppAvailable)}];
    }
    
    [mutableMenuItems addObject:@{@"title": kLocalString(@"miniapp_more_about_us_info"), @"image": @"miniapp_more_about_us", @"key": @(MenuItemKeyAboutUs), @"isAvailabel":@(YES)}];
    
    NSMutableArray *itemsArr = [[NSMutableArray alloc] init];
    for (int i = 0; i < mutableMenuItems.count; i++) {
        CPMenuItem* item = [CPMenuItem new];
        item.title = mutableMenuItems[i][@"title"];
        item.imgName = mutableMenuItems[i][@"image"];
        item.key = [mutableMenuItems[i][@"key"] intValue];
        item.isAvailable = [mutableMenuItems[i][@"isAvailabel"] boolValue];
        [itemsArr addObject:item];
    }
   
    self.items = itemsArr;
    
    
}

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    [UIView animateWithDuration:0.3 animations:^{
        self.view.backgroundColor = [UIColor colorWithWhite:0 alpha:0.5];
        self.containerView.transform = CGAffineTransformIdentity;
    }];
}

- (void)setupUI {
    self.view.backgroundColor = [UIColor clearColor];
    
    
    UIView* topView =  [UIView new];
    topView.translatesAutoresizingMaskIntoConstraints = NO;
    topView.backgroundColor = kColorWithHex(CP_BACKGROUD_COLOR);
    
    // Tampilan kontainer
    self.containerView = [[UIView alloc] init];
    self.containerView.backgroundColor = [UIColor whiteColor];
    self.containerView.layer.cornerRadius = kCornerRadius;
    self.containerView.layer.maskedCorners = kCALayerMinXMinYCorner | kCALayerMaxXMinYCorner;
    self.containerView.clipsToBounds = YES;
    self.containerView.transform = CGAffineTransformMakeTranslation(0, kPopupHeight);
    [self.containerView addSubview:topView];
    [self.view addSubview:self.containerView];
    //[self.view addSubview:self.containerView];
    
    // Bilah judul
    UIImageView *iconImageView = self.appIcon;
    iconImageView.image = kGetImg(@"miniapp_default_icon");
    iconImageView.tintColor = [UIColor blackColor];
    //[self.containerView addSubview:iconImageView];
    NSString *iconUrl = self.miniAppInfo[@"miniAppIcon"] ?: @"";
    if (![iconUrl isEqualToString:@""]) {
        [iconImageView loadUrl:iconUrl];
    }
    [topView addSubview:iconImageView];
    
    UILabel *titleLabel = [[UILabel alloc] init];
    NSString *miniAppName = self.miniAppInfo[@"miniAppName"] ?: @"Minapp Demo";
    titleLabel.text = miniAppName;
    titleLabel.font = [UIFont systemFontOfSize:17 weight:UIFontWeightMedium];
    titleLabel.textColor = [UIColor blackColor];
    //[self.containerView addSubview:titleLabel];
    [topView addSubview:titleLabel];
    
    // CollectionView
    UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
    layout.itemSize = CGSizeMake(kItemWid, kItemHei);
    layout.minimumInteritemSpacing = kItemSpacing;
    layout.minimumLineSpacing = kItemSpacing;
    layout.scrollDirection = UICollectionViewScrollDirectionVertical;
    
    self.collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
    self.collectionView.delegate = self;
    self.collectionView.dataSource = self;
    self.collectionView.backgroundColor = [UIColor clearColor];
    self.collectionView.showsHorizontalScrollIndicator = NO;
    [self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"cell"];
    //[self.containerView addSubview:self.collectionView];
    [topView addSubview:self.collectionView];
    
    // Tombol batal
    self.cancelButton = [UIButton buttonWithType:UIButtonTypeSystem];
    [self.cancelButton setTitle:kLocalString(@"mini_app_cancel") forState:UIControlStateNormal];
    [self.cancelButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
    self.cancelButton.titleLabel.font = kFont(17);
    self.cancelButton.backgroundColor = UIColor.whiteColor;
    self.cancelButton.layer.cornerRadius = 8.0;
    [self.cancelButton addTarget:self action:@selector(cancelAction) forControlEvents:UIControlEventTouchUpInside];
    [self.containerView addSubview:self.cancelButton];
    
    // constraint elemen bilah judul
    iconImageView.translatesAutoresizingMaskIntoConstraints = NO;
    titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
    
    [NSLayoutConstraint activateConstraints:@[
        // constraint ikon
        [iconImageView.leadingAnchor constraintEqualToAnchor:topView.leadingAnchor constant:wPix(24)],
        [iconImageView.topAnchor constraintEqualToAnchor:topView.topAnchor constant:hPix(16)],
        [iconImageView.widthAnchor constraintEqualToConstant:kIconSize],
        [iconImageView.heightAnchor constraintEqualToConstant:kIconSize],
        
        // constraint judul
        [titleLabel.leadingAnchor constraintEqualToAnchor:iconImageView.trailingAnchor constant:wPix(8)],
        [titleLabel.centerYAnchor constraintEqualToAnchor:iconImageView.centerYAnchor],
        [titleLabel.trailingAnchor constraintLessThanOrEqualToAnchor:topView.trailingAnchor constant:-wPix(24)]
    ]];
}

- (void)setupConstraints {
    self.containerView.translatesAutoresizingMaskIntoConstraints = NO;
    self.collectionView.translatesAutoresizingMaskIntoConstraints = NO;
    self.cancelButton.translatesAutoresizingMaskIntoConstraints = NO;
    
    UIView* topView  = self.collectionView.superview;
    
    [NSLayoutConstraint activateConstraints:@[
        
        [topView.topAnchor  constraintEqualToAnchor:self.containerView.topAnchor],
        [topView.leftAnchor  constraintEqualToAnchor:self.containerView.leftAnchor],
        [topView.rightAnchor  constraintEqualToAnchor:self.containerView.rightAnchor ],
        
        // constraint tampilan kontainer
        [self.containerView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
        [self.containerView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
        [self.containerView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
        [self.containerView.heightAnchor constraintEqualToConstant:kPopupHeight],
        
        // constraint CollectionView
        [self.collectionView.topAnchor constraintEqualToAnchor:topView.topAnchor constant:hPix(60)],
        [self.collectionView.leadingAnchor constraintEqualToAnchor:topView.leadingAnchor constant:wPix(24)],
        [self.collectionView.trailingAnchor constraintEqualToAnchor:topView.trailingAnchor constant:-wPix(24)],
        [self.collectionView.bottomAnchor constraintEqualToAnchor:topView.bottomAnchor],
        
        // constraint tombol batal
        [self.cancelButton.topAnchor constraintEqualToAnchor:topView.bottomAnchor constant:0],
        [self.cancelButton.leadingAnchor constraintEqualToAnchor:self.containerView.leadingAnchor constant:wPix(16)],
        [self.cancelButton.trailingAnchor constraintEqualToAnchor:self.containerView.trailingAnchor constant:-wPix(16)],
        [self.cancelButton.heightAnchor constraintEqualToConstant:kCancelButtonHeight],
        [self.cancelButton.bottomAnchor constraintEqualToAnchor:self.containerView.bottomAnchor constant:-kCancelButtonBottomMargin]
    ]];
}

#pragma mark - CollectionView DataSource & Delegate

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
    return self.items.count;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"cell" forIndexPath:indexPath];
    UIView* c;
    UIImageView* iconView;
    UILabel* titleLabel;
    CPMenuItem* item = self.items[indexPath.item];
    if (![cell.contentView viewWithTag:CPMENU_CELL_ICON_CONTAINER_TAG]) {
        c = [UIView new];
        c.tag = 1234;
        c.backgroundColor = UIColor.whiteColor;
        c.layer.cornerRadius = 12;
        [c setClipsToBounds:YES];
        [cell.contentView addSubview:c];
        
        // ikon
        iconView = [[UIImageView alloc] init];
        iconView.tag = CPMENU_CELL_ICON_TAG;
        iconView.image = kGetImg(item.imgName);
        iconView.tintColor = [UIColor whiteColor];
        iconView.contentMode = UIViewContentModeScaleAspectFit;
        [c addSubview:iconView];
        
        // judul
        titleLabel = [[UILabel alloc] init];
        titleLabel.tag = CPMENU_CELL_TITLE_TAG;
        titleLabel.text = item.title;
        titleLabel.font = kFont(11);
        titleLabel.textAlignment = NSTextAlignmentCenter;
        titleLabel.textColor = kColorWithHex(@"#81848F");
        titleLabel.numberOfLines = 2;
        [cell.contentView addSubview:titleLabel];
        
        // constraint
        c.translatesAutoresizingMaskIntoConstraints = NO;
        iconView.translatesAutoresizingMaskIntoConstraints = NO;
        titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
        
        [NSLayoutConstraint activateConstraints:@[
            
            [c.topAnchor constraintEqualToAnchor:cell.contentView.topAnchor],
            [c.centerXAnchor constraintEqualToAnchor:cell.contentView.centerXAnchor],
            [c.widthAnchor constraintEqualToConstant:wPix(56)],
            [c.heightAnchor constraintEqualToConstant:hPix(56)],
            
            // constraint ikon
            [iconView.centerYAnchor constraintEqualToAnchor:c.centerYAnchor],
            [iconView.centerXAnchor constraintEqualToAnchor:c.centerXAnchor],
            [iconView.widthAnchor constraintEqualToConstant:wPix(32)],
            [iconView.heightAnchor constraintEqualToConstant:wPix(32)],
            
            // constraint judul
            [titleLabel.topAnchor constraintEqualToAnchor:c.bottomAnchor constant:hPix(8)],
            [titleLabel.leadingAnchor constraintEqualToAnchor:cell.contentView.leadingAnchor],
            [titleLabel.trailingAnchor constraintEqualToAnchor:cell.contentView.trailingAnchor],
            [titleLabel.bottomAnchor constraintLessThanOrEqualToAnchor:cell.contentView.bottomAnchor]
        ]];
        
        // gaya latar belakang
        cell.contentView.backgroundColor = kColorWithHex(CP_BACKGROUD_COLOR);
        cell.contentView.layer.cornerRadius = 8.0;
        cell.contentView.clipsToBounds = YES;
    }else{
        c =  [cell.contentView viewWithTag:CPMENU_CELL_ICON_CONTAINER_TAG];
        iconView = [c viewWithTag:CPMENU_CELL_ICON_TAG];
        titleLabel = [cell.contentView viewWithTag:CPMENU_CELL_TITLE_TAG];
        iconView.image = kGetImg(item.imgName);
        titleLabel.text = item.title;
    }
    
    //status tidak tersedia
    if (item.status == 0 ){
        titleLabel.alpha = iconView.alpha = 0.3;
    }else if (item.status == 2){
        iconView.image = kGetImg(item.vice_imgName);
        titleLabel.text = item.vice_title;
    }
    
    if (!item.isAvailable) {
        titleLabel.alpha = iconView.alpha = 0.3;
    }
    
    return cell;
}

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
    return CGSizeMake(kItemWid, kItemHei);
}

- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {
    return UIEdgeInsetsMake(0, 0, 0, 0);
}

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
    
    CPMenuItem* item = self.items[indexPath.item];
    if (item.status == 0) {
        return;
    }
    if (!item.isAvailable) {
        return;
    }
    
    NSString *appId = self.miniAppInfo[@"miniAppId"] ?: @"";
    NSString * code = @"";
    
    switch (item.key) {
            //收藏
        case MenuItemKeyAddToFav:
        {
            code = EMAS_CANCEL;
            id<EMASMiniAppFavoriteService> favService = [[EMASServiceManager sharedInstance] serviceForProtocol:@"EMASMiniAppFavoriteService"];
            
            if (favService){
                BOOL isFavorite = [favService isFavorite:appId];
                __weak typeof(self) weakSelf = self;
                if (!isFavorite) {
                    
                    [favService addToFavorites:appId completion:^(NSDictionary * _Nonnull result) {
                        [CPCommonTool showToast:kLocalStringWithValue(@"miniapp_more_favorite_success", @"Added to favorites")];//miniapp_more_favorite_success
                    }];
                }else{
                    if(favService){
                        [favService removeFromFavorites:appId completion:^(NSDictionary * _Nonnull result) {
                            [CPCommonTool showToast:kLocalStringWithValue(@"miniapp_more_remove_favorite_success", @"Removed from my favorites")];
                        }];
                    }
                }
            };
        }
            break;
            //设置
        case MenuItemKeySettings:
        {
            //跳转设置页面
            code = EMAS_OPEN_SETTING;
        }
            break;
            
            //复制链接
        case MenuItemKeyCopyLink:
        {
            code = EMAS_CANCEL;
            NSString *urlStr =  self.miniAppInfo[@"miniAppUrl"];
            if (urlStr && ![urlStr isEqualToString:@""]) {
                UIPasteboard * pasteboard = [UIPasteboard generalPasteboard];
                pasteboard.string = urlStr;
                [CPCommonTool showToast:kLocalStringWithValue(@"miniapp_more_copy_success", @"Copied")];
            }
            
        }
            break;
            
            //分享
        case MenuItemKeyShare:
        {
            __weak typeof(self) weakself = self;
            UIViewController* vc = self.presentingViewController;
            NSString* url = self.miniAppInfo[@"miniAppUrl"];
            [self dismissViewControllerWithCompletion:^{
                __strong typeof(weakself) strongSelf = weakself;
                strongSelf.completionBlock(@{@"code":EMAS_CANCEL});
                dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                    [strongSelf showShareWithString:url showIn:vc];
                });
            }];
        }
            break;
            
            
            //关于我们
        case MenuItemKeyAboutUs:
        {
            code = EMAS_OPEN_ABOUT;
        }
            break;
            
            //刷新
        case MenuItemKeyRefresh:
        {
            code = EMAS_RESTART_MINIAPP;
            
        }
            
        default:
            break;
    }
    if (self.completionBlock && code && code .length != 0) {
            self.completionBlock(@{@"code":code});
     }
    
    
}

#pragma mark - Private

-(void)showShareWithString:(NSString*)string showIn:(UIViewController*)showVC{
    
    if (!showVC) {
        return;
    }
    
    NSString* value = nil;
    
    id<EMASMiniAppMoreService> moreService = [[EMASServiceManager sharedInstance] serviceForProtocol:@"EMASMiniAppMoreService"];
    if([moreService conformsToProtocol:@protocol(EMASMiniAppMoreService)]){
        if ([moreService respondsToSelector:@selector(share:)]) {
            value = [moreService share: self.miniAppInfo];
        }
    }
    NSString* _url = value;
    
    if (_url && ![_url isEqualToString:@""]) {
        NSMutableArray *items = [NSMutableArray new];
        [items addObject:_url];
        
        UIActivityViewController *activityVC = [[UIActivityViewController alloc]
                                                initWithActivityItems:items
                                                applicationActivities:nil];
        //kompatibel dengan iPad
        if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
            activityVC.popoverPresentationController.sourceView = self.view;
            activityVC.popoverPresentationController.sourceRect = CGRectMake(CGRectGetMidX(self.view.bounds),
                                                                             CGRectGetMidY(self.view.bounds),
                                                                             0, 0);
        }
        activityVC.completionHandler = ^(UIActivityType  _Nullable activityType, BOOL completed) {
            if (completed) {
                [CPCommonTool showToast:kLocalStringWithValue(@"miniapp_more_share_success", @"Share Successful")];
            }
        };
        UIViewController* vc = showVC;
        while (vc.presentedViewController) {
            vc = vc.presentedViewController;
        }
        
        [vc presentViewController:activityVC animated:YES completion:nil];
    }else {
        NSLog(@"empty share url");
    }
    
}


#pragma mark - Actions

- (void)cancelAction {
    
    __weak typeof(self) weakSelf = self;
    [self dismissViewControllerWithCompletion:^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        if (strongSelf.completionBlock) {
            strongSelf.completionBlock(@{@"code":EMAS_CANCEL});
        }
    }];
    
    
}

- (void)dismissViewControllerWithCompletion:(void (^)(void))finish{
   
    [UIView animateWithDuration:0.3 animations:^{
        self.view.backgroundColor = [UIColor clearColor];
        self.containerView.transform = CGAffineTransformMakeTranslation(0, kPopupHeight);
    } completion:^(BOOL finished) {
        [self dismissViewControllerAnimated:NO completion:^{
            if (finish) {
                finish();
            }
        }];
    }];
    
}

- (void)dismissViewController {
    [self dismissViewControllerWithCompletion:nil];
}
#pragma mark - Setter

-(UIImageView*)appIcon{
    if (!_appIcon) {
        _appIcon = [UIImageView new];
    }
    return _appIcon;
}
@end
6. Implementasi SettingViewController
#import "CPSettingPage.h"
#import "CPSettingTableViewCell.h"
#import "CPCommonTool.h"
#import <EMASMiniAppService/EMASPluginEnv.h>
#import <EMASMiniAppService/EMASIAuthService.h>
#import <EMASMiniAppService/EMASResultCodeConstants.h>


@interface CPSettingPage () <UITableViewDataSource,UITableViewDelegate,CPAuthChangeDelegate>
@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, strong) NSArray *dataArray; // data
@property (nonatomic, strong) NSDictionary *miniAppInfoDict;
@end

@implementation CPSettingPage

- (void)viewDidLoad {
    [super viewDidLoad];
    // Lakukan setup tambahan setelah memuat tampilan.
    [self setupData];
    [self setupTableView];

}

- (void)miniAppInfo:(NSDictionary *)info {
    self.miniAppInfoDict = info;
}


- (void)setupData {
    // Inisialisasi sumber data
    NSString *userId = self.miniAppInfoDict[@"userId"] ?: @"";
    NSString *miniAppId = self.miniAppInfoDict[@"miniAppId"] ?: @"";
    id <EMASIAuthService> authService = EMASPluginEnv.shareInstance.getContainerContext.getAuthService;
    NSDictionary *dic = [authService queryAuthInfo:userId miniAppId:miniAppId];

    NSMutableArray* arr = @[].mutableCopy;

    CPAuthItem* item = [CPAuthItem new];
    item.authType = CPAuthTypeLOC;
    item.title = kLocalString(@"miniapp_location");
    item.isAuth = [[dic objectForKey:@"location"] boolValue];
    item.scope = @"location";
    [arr addObject:item];

    item = [CPAuthItem new];
    item.authType = CPAuthTypeCAM;
    item.title = kLocalString(@"miniapp_camera");
    item.isAuth = [[dic objectForKey:@"camera"] boolValue];
    item.scope = @"camera";
    [arr addObject:item];

    item = [CPAuthItem new];
    item.authType = CPAuthTypeALBUM;
    item.title = kLocalString(@"miniapp_album");
    item.isAuth = [[dic objectForKey:@"album"] boolValue];
    item.scope = @"album";
    [arr addObject:item];

    item = [CPAuthItem new];
    item.authType = CPAuthTypeBLE;
    item.title = kLocalString(@"miniapp_bluetooth");
    item.isAuth = [[dic objectForKey:@"bluetooth"] boolValue];
    item.scope = @"bluetooth";
    [arr addObject:item];

    item = [CPAuthItem new];
    item.authType = CPAuthTypeMIC;
    item.title = kLocalString(@"miniapp_microphone");
    item.isAuth = [[dic objectForKey:@"microphone"] boolValue];
    item.scope = @"microphone";
    [arr addObject:item];

    item = [CPAuthItem new];
    item.authType = CPAuthTypeCONT;
    item.title = kLocalString(@"miniapp_contacts");
    item.isAuth = [[dic objectForKey:@"contacts"] boolValue];
    item.scope = @"contacts";
    [arr addObject:item];

    item = [CPAuthItem new];
    item.authType = CPAuthTypeFILES;
    item.title = kLocalString(@"miniapp_file");
    item.isAuth = [[dic objectForKey:@"file"] boolValue];
    item.scope = @"file";
    [arr addObject:item];

    item = [CPAuthItem new];
    item.authType = CPAuthTypeCALLS;
    item.title = kLocalString(@"miniapp_call");
    item.isAuth = [[dic objectForKey:@"call"] boolValue];
    item.scope = @"call";
    [arr addObject:item];
    
    item = [CPAuthItem new];
    item.authType = CPAuthTypeVIBRATE;
    item.title = kLocalString(@"miniapp_vibrate");
    item.isAuth = [[dic objectForKey:@"vibrate"] boolValue];
    item.scope = @"vibrate";
    [arr addObject:item];
    
    item = [CPAuthItem new];
    item.authType = CPAuthTypeSCREEN;
    item.title = kLocalString(@"miniapp_screen");
    item.isAuth = [[dic objectForKey:@"screen"] boolValue];
    item.scope = @"screen";
    [arr addObject:item];
    
    self.dataArray = arr.copy;
}

- (void)goBack {    
    if (self.completionBlock) {
        self.completionBlock(@{@"code":EMAS_CANCEL});        
    }
    
}

- (void)setupTableView {
    //tampilan header
    UIView *topView = [[UIView alloc] initWithFrame:CGRectZero];
    topView.translatesAutoresizingMaskIntoConstraints = NO;
    //miniapp_back
    topView.backgroundColor = [UIColor whiteColor];
    UIButton *backBtn = [UIButton buttonWithType:UIButtonTypeCustom];
    backBtn.translatesAutoresizingMaskIntoConstraints = NO;
    [backBtn setImage:[CPCommonTool getImageByName:(@"miniapp_back")] forState:UIControlStateNormal];
    [backBtn addTarget:self action:@selector(goBack) forControlEvents:UIControlEventTouchUpInside];
    UILabel *titleLable = [[UILabel alloc] initWithFrame:CGRectZero];
    titleLable.translatesAutoresizingMaskIntoConstraints = NO;
    titleLable.text = kLocalString(@"miniapp_settings_title");
    titleLable.font = [UIFont systemFontOfSize:16.0 weight:UIFontWeightMedium];
    titleLable.textAlignment = NSTextAlignmentCenter;
    titleLable.textColor = [UIColor blackColor];
    [self.view addSubview:topView];
    [topView addSubview:titleLable];
    [topView addSubview:backBtn];
    
    CGFloat statusbar_h = UIApplication.sharedApplication.statusBarFrame.size.height;
    CGFloat h = statusbar_h + 44.0;
    
    
    self.tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
    self.tableView.translatesAutoresizingMaskIntoConstraints = NO;
    self.tableView.dataSource = self;
    self.tableView.delegate = self;
    [self.tableView registerClass:[CPSettingTableViewCell class] forCellReuseIdentifier:@"cell"];
    [self.view addSubview:self.tableView];
    
    self.tableView.separatorInset = UIEdgeInsetsZero;
    self.tableView.layoutMargins = UIEdgeInsetsZero
        
    if (@available(iOS 15.0, *)) {
        self.tableView.sectionHeaderTopPadding = 0;
    }
    
    // Buat tampilan kontainer
    UIView *headerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 0)];
    headerView.translatesAutoresizingMaskIntoConstraints = NO;
    headerView.backgroundColor = kColorWithHex(CP_BACKGROUD_COLOR);

    NSString *miniAppName = self.miniAppInfoDict[@"miniAppName"] ?: @"MiniApp Demo";
    // Buat label teks
    UILabel *headerLabel = [[UILabel alloc] init];
    headerLabel.numberOfLines = 0;
    headerLabel.text = [NSString stringWithFormat:kLocalString(@"miniapp_settings_allow_tip"), miniAppName];
    headerLabel.textColor = kColorWithHex(@"#81848F");
    headerLabel.backgroundColor = [UIColor clearColor];
    [headerLabel setFont:kFont(13)];
    

    headerLabel.translatesAutoresizingMaskIntoConstraints = NO;
    [headerView addSubview:headerLabel];
  
    [self.tableView setTableHeaderView:headerView];
    
    [NSLayoutConstraint activateConstraints:@[
        [headerView.widthAnchor constraintEqualToAnchor:self.view.widthAnchor]
    ]];
    
    [NSLayoutConstraint activateConstraints:@[
        [topView.topAnchor constraintEqualToAnchor:self.view.topAnchor constant:0],
        [topView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:0],
        [topView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:0],
        [topView.heightAnchor constraintEqualToConstant:h],
        
        [titleLable.widthAnchor constraintEqualToConstant:200],
        [titleLable.heightAnchor constraintEqualToConstant:44.0],
        [titleLable.topAnchor constraintEqualToAnchor:topView.topAnchor constant:statusbar_h],
        [titleLable.centerXAnchor constraintEqualToAnchor: topView.centerXAnchor],
        
        [backBtn.leadingAnchor constraintEqualToAnchor:topView.leadingAnchor constant:0],
        [backBtn.widthAnchor constraintEqualToConstant:64],
        [backBtn.heightAnchor constraintEqualToAnchor:titleLable.heightAnchor],
        [backBtn.centerYAnchor constraintEqualToAnchor:titleLable.centerYAnchor],
        
        [headerLabel.topAnchor constraintEqualToAnchor:headerView.topAnchor constant:hPix(16)],
        [headerLabel.leadingAnchor constraintEqualToAnchor:headerView.leadingAnchor constant:wPix(16)],
        [headerLabel.trailingAnchor constraintEqualToAnchor:headerView.trailingAnchor constant:wPix(-16)],
        [headerLabel.bottomAnchor constraintEqualToAnchor:headerView.bottomAnchor constant:hPix(-16)],
        
        [self.tableView.topAnchor constraintEqualToAnchor:topView.bottomAnchor constant:0],
        [self.tableView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:0],
        [self.tableView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:0],
        [self.tableView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor constant:0]
    ]];
    
    [headerView setNeedsLayout];
    [headerView layoutIfNeeded];
    
    
}


#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.dataArray.count;
}
 
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    CPSettingTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
    cell.selectionStyle = UITableViewCellSelectionStyleNone;
    cell.model = self.dataArray[indexPath.row];
    cell.delegate = self;
    return cell;
}

#pragma mark - CPAuthChangeDelegate

-(void)onSwitchValueChange:(CPAuthItem*)authItem On:(BOOL)on{
    
    [self updateMiniAppPermission:authItem isAuth:on];
    [self.tableView reloadData];
}

- (void)updateMiniAppPermission:(CPAuthItem *)authItem isAuth:(BOOL)isAuth {
    id <EMASIAuthService> authService = [EMASPluginEnv.shareInstance.getContainerContext getAuthService];
    NSString *userId = self.miniAppInfoDict[@"userId"] ?: @"";
    NSString *miniAppId = self.miniAppInfoDict[@"miniAppId"] ?: @"";
    if (isAuth) {
        BOOL isSuccess = [authService saveAuth:authItem.scope userId:userId miniAppId:miniAppId];
        BOOL authed = isSuccess ? isAuth : !isAuth;
        authItem.isAuth = authed;
    }else {
        BOOL isSuccess = [authService revokeAuth:authItem.scope userId:userId miniAppId:miniAppId];
        BOOL authed = isSuccess ? isAuth : !isAuth;
        authItem.isAuth = authed;
    }
}

@end
7. Implementasi FavoriteViewController
#import "CPComfirmViewController.h"
#import "CPCommonTool.h"
#import <EMASMiniAppService/EMASPluginEnv.h>
#import <EMASMiniAppService/EMASResultCodeConstants.h>
#import <EMASServiceManager/EMASServiceManager.h>
#import <EMASMiniAppApi/EMASMiniAppFavoriteService.h>

@implementation CPCfmViewConfig

@end


@interface CPComfirmViewController()
@property (nonatomic,strong) UIView *maskView;
@property (nonatomic,strong) NSLayoutConstraint *bottomConstraint;
@property (nonatomic,strong) UIButton *infoButton;
@property (nonatomic,strong) UIImageView *icon;
@property (nonatomic,strong) UILabel *appLabel;
@property (nonatomic,strong) UILabel *titleLabel;
@property (nonatomic,strong) UILabel *descLabel;
@property (nonatomic,strong) UIView *container;

@end
@implementation CPComfirmViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self setupUI];
    [self loadData];
}

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];

    // Paksa pembaruan tata letak
    self.bottomConstraint.constant = 0;
    // Jalankan animasi tampilan
    [UIView animateWithDuration:0.3 animations:^{
        self.maskView.alpha = 1;
        [self.view layoutIfNeeded];

    }];

}


/*
 @"miniAppId": appid,
          @"miniAppName": appName,
          @"miniAppIcon": iconUrl,
          @"miniAppVersion": version,
          @"miniAppSlogan": slogan,
          @"miniAppDescription": descrip
 */
- (void)setupUI {
    self.view.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.5];
    self.maskView.alpha = 0;

    UIView* container = [UIView new];
    container.backgroundColor = UIColor.whiteColor;
    container.layer.cornerRadius = 12;
    container.layer.maskedCorners = kCALayerMinXMinYCorner | kCALayerMaxXMinYCorner;
    container.translatesAutoresizingMaskIntoConstraints = NO;
    self.container = container;
    [self.view addSubview:container];

    // Tombol tanda seru
    _infoButton = [UIButton buttonWithType:UIButtonTypeCustom];
    [_infoButton setImage:kGetImg(@"miniapp_aboutus") forState:UIControlStateNormal];
    _infoButton.translatesAutoresizingMaskIntoConstraints = NO;
    [_infoButton addTarget:self action:@selector(moreMiniAppInfo) forControlEvents:UIControlEventTouchUpInside];
    [container addSubview:_infoButton];

    _infoButton.hidden = YES;

    // Tata letak ikon dan nama aplikasi
    UIImageView *icon = [UIImageView new];
    icon.image = kGetImg(@"miniapp_default_icon");
    icon.translatesAutoresizingMaskIntoConstraints = NO;
    [container addSubview:icon];
    self.icon = icon;
    NSString *iconUrl = self.config.miniAppInfo[@"miniAppIcon"] ?: @"";
    if (![iconUrl isEqualToString:@""]) {
        [self.icon loadUrl:iconUrl];
    }

    UILabel *appLabel = [UILabel new];
    self.appLabel = appLabel;
    NSString *miniAppName = self.config.miniAppInfo[@"miniAppName"] ?: @"Minapp Demo";
    [self setAppLabelTxt:miniAppName];
    appLabel.font = kFontWithWeight(15, 500);
    appLabel.translatesAutoresizingMaskIntoConstraints = NO;
    [container addSubview:appLabel];

    // Kontainer konten (gunakan UIStackView)
    UIStackView *contentStack = [[UIStackView alloc] init];
    contentStack.axis = UILayoutConstraintAxisVertical;
    contentStack.spacing = hPix(16);
    contentStack.translatesAutoresizingMaskIntoConstraints = NO;
    [container addSubview:contentStack];
    
    // Tambahkan elemen konten ke stackView
    UILabel *titleLabel = [UILabel new];
    titleLabel.text = kLocalString(@"miniapp_add_favorite_title");
    titleLabel.font = kFont(17);
    titleLabel.numberOfLines = 0;
    [contentStack addArrangedSubview:titleLabel];
    self.titleLabel = titleLabel;
    
    UILabel *descLabel = [UILabel new];
    descLabel.text = kLocalString(@"miniapp_add_favorite_info");
    descLabel.font = kFont(13);
    descLabel.textColor = [UIColor grayColor];
    descLabel.numberOfLines = 0;
    [contentStack addArrangedSubview:descLabel];
    self.descLabel = descLabel;
    
    // Kontainer tombol
    UIView *buttonContainer = [UIView new];
    buttonContainer.translatesAutoresizingMaskIntoConstraints = NO;
    [container addSubview:buttonContainer];
    
    // Tombol tolak
    UIButton *rejectBtn = [CPCommonTool getThemeLightButton];
    [rejectBtn setTitle:kLocalString(@"mini_app_cancel") forState:UIControlStateNormal];
    [rejectBtn addTarget:self action:@selector(rejectAction) forControlEvents:UIControlEventTouchUpInside];
    [buttonContainer addSubview:rejectBtn];
    
    // Tombol izinkan
    UIButton *allowBtn = [CPCommonTool getThemeDarkButton];
    [allowBtn setTitle:kLocalString(@"mini_app_ok") forState:UIControlStateNormal];
    [allowBtn addTarget:self action:@selector(allowAction) forControlEvents:UIControlEventTouchUpInside];
    [buttonContainer addSubview:allowBtn];
    
    
    // Konfigurasi constraint
    [NSLayoutConstraint activateConstraints:@[
       
        // Tombol tanda seru
        [_infoButton.topAnchor constraintEqualToAnchor:container.topAnchor constant:16],
        [_infoButton.trailingAnchor constraintEqualToAnchor:container.trailingAnchor constant:-wPix(24)],
        [_infoButton.widthAnchor constraintEqualToConstant:24],
        [_infoButton.heightAnchor constraintEqualToConstant:24],
        
        // Ikon dan nama aplikasi
        [icon.topAnchor constraintEqualToAnchor:container.topAnchor constant:16],
        [icon.leadingAnchor constraintEqualToAnchor:container.leadingAnchor constant:wPix(24)],
        [icon.widthAnchor constraintEqualToConstant:wPix(24)],
        [icon.heightAnchor constraintEqualToConstant:wPix(24)],
        
        [appLabel.centerYAnchor constraintEqualToAnchor:icon.centerYAnchor],
        [appLabel.leadingAnchor constraintEqualToAnchor:icon.trailingAnchor constant:wPix(8)],
        [appLabel.trailingAnchor constraintEqualToAnchor:_infoButton.leadingAnchor constant:-wPix(8)],
        
        // Area konten
        [contentStack.topAnchor constraintEqualToAnchor:icon.bottomAnchor constant:hPix(32)],
        [contentStack.leadingAnchor constraintEqualToAnchor:container.leadingAnchor constant:wPix(24)],
        [contentStack.trailingAnchor constraintEqualToAnchor:container.trailingAnchor constant:-wPix(24)],
        
        // Kontainer tombol
        [buttonContainer.topAnchor constraintEqualToAnchor:contentStack.bottomAnchor constant:hPix(20)],
        [buttonContainer.bottomAnchor constraintEqualToAnchor:container.bottomAnchor constant:-hPix(34)],
        [buttonContainer.leadingAnchor constraintEqualToAnchor:container.leadingAnchor],
        [buttonContainer.trailingAnchor constraintEqualToAnchor:container.trailingAnchor],
        //[buttonContainer.heightAnchor constraintEqualToConstant:72],
        
        // Tata letak tombol
        [rejectBtn.leadingAnchor constraintEqualToAnchor:buttonContainer.leadingAnchor constant:wPix(33.5)],
        [rejectBtn.topAnchor constraintEqualToAnchor:buttonContainer.topAnchor constant:hPix(20)],
        [rejectBtn.heightAnchor constraintEqualToConstant:hPix(40)],
        [rejectBtn.widthAnchor constraintEqualToAnchor:allowBtn.widthAnchor],
        [rejectBtn.bottomAnchor constraintEqualToAnchor:buttonContainer.bottomAnchor constant:-hPix(20)],
        
        [allowBtn.leadingAnchor constraintEqualToAnchor:rejectBtn.trailingAnchor constant:wPix(20)],
        [allowBtn.trailingAnchor constraintEqualToAnchor:buttonContainer.trailingAnchor constant:-wPix(33.5)],
        [allowBtn.heightAnchor constraintEqualToConstant:hPix(40)],
        [allowBtn.topAnchor constraintEqualToAnchor:buttonContainer.topAnchor constant:hPix(20)],
    ]];
    
    // Perhitungan tinggi dinamis
    [container setContentHuggingPriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisVertical];
    [container setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical];
}


-(void)setAppLabelTxt:(NSString*)Txt{
    
    self.appLabel.text = [NSString stringWithFormat:@"%@ %@",Txt,kLocalString(@"miniapp_auth_apply_tip")];
}

-(void)loadData{
    
   CPCfmViewConfig* cfn =  self.config;
    if (kStrExist(cfn.appName)) {
        [self setAppLabelTxt:cfn.appName];
    }
    if (kStrExist(cfn.title)) {
        self.titleLabel.text = cfn.title;
    }
    if (kStrExist(cfn.appIconName)) {
        self.icon.image = [CPCommonTool getImageByName:cfn.appIconName];
    }
    if (kStrExist(cfn.content)) {
        self.descLabel.text = cfn.content;
    }
    
    UIView* superView = self.view;
    
    // Buat lapisan masker
    _maskView = self.maskView;
    _maskView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.5];
    //_maskView.alpha = 0;
    [superView insertSubview:_maskView belowSubview:self.container];
    
    // Dapatkan tinggi dinamis
    [self.view layoutIfNeeded];
    
    // Atur constraint (posisi awal di bawah layar)
    _bottomConstraint = [self.container.bottomAnchor constraintEqualToAnchor:superView.bottomAnchor constant:self.container.bounds.size.height];
    [NSLayoutConstraint activateConstraints:@[
        [self.container.widthAnchor constraintEqualToAnchor:superView.widthAnchor],
        [self.container.leadingAnchor constraintEqualToAnchor:superView.leadingAnchor],
        [self.container.trailingAnchor constraintEqualToAnchor:superView.trailingAnchor],
        _bottomConstraint
    ]];
    
    
    
}


#pragma mark -Lihat informasi miniapp
- (void)moreMiniAppInfo {
    if (self.completionBlock) {
        self.completionBlock(@{@"code":EMAS_OPEN_ABOUT});
    }
}

- (void)allowAction {
    
    NSString *appId = self.config.miniAppInfo[@"miniAppId"];
    if (appId && ![appId isEqualToString:@""]) {
        id<EMASMiniAppFavoriteService> favService = [[EMASServiceManager sharedInstance] serviceForProtocol:@"EMASMiniAppFavoriteService"];
        if (favService){
            [favService addToFavorites:appId completion:^(NSDictionary * _Nonnull result) {
                [CPCommonTool showToast:kLocalStringWithValue(@"miniapp_more_favorite_success", @"Added to my favorites")];
            }];
        }
    }

    __weak typeof(self) weakSelf = self;
    [self dismiss:^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        if (strongSelf.completionBlock) {
            strongSelf.completionBlock(@{@"code":EMAS_EXIT_MINIAPP});
        }
    }];

}

- (void)rejectAction {
    
    __weak typeof(self) weakSelf = self;
    [self dismiss:^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        if (strongSelf.completionBlock) {
            strongSelf.completionBlock(@{@"code":EMAS_EXIT_MINIAPP});
        }
    }];
}

- (void)dismiss:(void(^)(void))completion{

    [UIView animateWithDuration:0.3 animations:^{
        self.maskView.alpha = 0;
        self.container.transform = CGAffineTransformMakeTranslation(0, self.container.frame.size.height);
    } completion:^(BOOL finished) {
        if (completion) {
            completion();
        }
    }];
}

- (void)dismiss {
    [self dismiss:nil];
}


#pragma mark - Getter

-(UIView*)maskView{
    if (!_maskView) {
        _maskView = [[UIView alloc]initWithFrame:self.view.bounds];
    }
    return _maskView;
}

@end
8. Implementasi AboutUsViewController
#import "CPAboutUsViewController.h"
#import "CPCommonTool.h"
#import <EMASMiniAppService/EMASPluginEnv.h>
#import <EMASMiniAppService/EMASResultCodeConstants.h>

@interface CPAboutUsViewController ()
@property (strong, nonatomic) UIView *containerView;
@property (strong, nonatomic) UILabel *titleLabel;
@property (strong, nonatomic) UIButton *backButton;
@property (strong, nonatomic) UIStackView *contentStackView;
@property (strong, nonatomic) NSLayoutConstraint *containerLeftConstraint;
@property (nonatomic, strong) NSArray *dataItems;
@end

@implementation CPAboutUsViewController


- (instancetype)init
{
    self = [super init];
    if (self) {
        self.modalPresentationStyle = UIModalPresentationOverFullScreen;
    }
    return self;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    [self setupViews];
    [self setupData];
    [self.backButton addTarget:self action:@selector(dismissVC) forControlEvents:UIControlEventTouchUpInside];
}

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    [self animatePresent];
}

- (void)setupViews {
    self.view.backgroundColor = [UIColor colorWithWhite:0 alpha:0.5];


    // Tampilan Kontainer
    self.containerView = [[UIView alloc] init];
    self.containerView.backgroundColor = UIColor.whiteColor;
    self.containerView.layer.cornerRadius = 12;
    self.containerView.layer.maskedCorners = kCALayerMinXMinYCorner | kCALayerMaxXMinYCorner;
    self.containerView.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:self.containerView];

    // Label Judul
    self.titleLabel = [[UILabel alloc] init];
    self.titleLabel.text = kLocalString(@"miniapp_more_about_us_info");
    self.titleLabel.font = kFontWithWeight(15, 500);
    self.titleLabel.textAlignment = NSTextAlignmentCenter;
    self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
    [self.containerView addSubview:self.titleLabel];

    // Tombol Kembali
    self.backButton = [UIButton buttonWithType:UIButtonTypeSystem];
    [self.backButton setImage:kGetImg(@"miniapp_back") forState:UIControlStateNormal];
    self.backButton.tintColor = UIColor.blackColor;
    self.backButton.translatesAutoresizingMaskIntoConstraints = NO;
    [self.containerView addSubview:self.backButton];

    // Stack Konten
    self.contentStackView = [[UIStackView alloc] init];
    self.contentStackView.axis = UILayoutConstraintAxisVertical;
    self.contentStackView.spacing = hPix(16);
    self.contentStackView.translatesAutoresizingMaskIntoConstraints = NO;
    [self.containerView addSubview:self.contentStackView];

    // Constraint
    self.containerLeftConstraint = [self.containerView.leftAnchor constraintEqualToAnchor:self.view.leftAnchor constant:kScreenWid];

    [NSLayoutConstraint activateConstraints:@[
        [self.containerView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
        [self.containerView.widthAnchor constraintEqualToAnchor:self.view.widthAnchor],
        self.containerLeftConstraint,

        [self.titleLabel.topAnchor constraintEqualToAnchor:self.containerView.topAnchor constant:hPix(16)],
        [self.titleLabel.centerXAnchor constraintEqualToAnchor:self.containerView.centerXAnchor],
       
       [self.backButton.leadingAnchor constraintEqualToAnchor:self.containerView.leadingAnchor constant:wPix(24)],
       [self.backButton.centerYAnchor constraintEqualToAnchor:self.titleLabel.centerYAnchor],
       
       [self.contentStackView.topAnchor constraintEqualToAnchor:self.titleLabel.bottomAnchor constant:wPix(24)],
       [self.contentStackView.leadingAnchor constraintEqualToAnchor:self.containerView.leadingAnchor constant:wPix(24)],
       [self.contentStackView.trailingAnchor constraintEqualToAnchor:self.containerView.trailingAnchor constant:-wPix(24)],
       [self.contentStackView.bottomAnchor constraintEqualToAnchor:self.containerView.bottomAnchor constant:-hPix(34)]
   ]];
}

- (void)miniAppInfo:(NSDictionary *)info {
    
    if (info[@"miniAppInfo"]) {
        info =  info[@"miniAppInfo"];
    }
    NSString *miniAppName = info[@"miniAppName"] ?: @"";
    NSString *slogan = info[@"miniAppSlogan"] ?: @"";
    NSString *miniAppVersion = info[@"miniAppVersion"] ?: @"";
    NSString *description = info[@"miniAppDescription"] ?: @"";
    self.dataItems = @[
        @{@"title": kLocalString(@"miniapp_about_us_miniapp_name_descrip"), @"content": miniAppName},
        @{@"title": kLocalString(@"miniapp_about_us_slogan_descrip"), @"content": slogan},
        @{@"title": kLocalString(@"miniapp_about_us_miniapp_version_descrip"), @"content": miniAppVersion},
        @{@"title": kLocalString(@"miniapp_about_us_miniapp_descrip"), @"content": description}
    ];
    
}

- (void)setupData {
    
    for (NSDictionary *item in self.dataItems) {
       UIView *itemView = [self createItemViewWithTitle:item[@"title"] content:item[@"content"]];
       [self.contentStackView addArrangedSubview:itemView];
   }
}

- (UIView *)createItemViewWithTitle:(NSString *)title content:(NSString *)content {
   UIView *container = [[UIView alloc] init];
   
   UILabel *titleLabel = [[UILabel alloc] init];
   titleLabel.text = title;
   titleLabel.font = kFont(13);
   titleLabel.textColor = kColorWithHex(@"#81848F");
   titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
   [container addSubview:titleLabel];
   
   UILabel *contentLabel = [[UILabel alloc] init];
   contentLabel.text = content;
   contentLabel.font = kFont(16);
   contentLabel.numberOfLines = 0;
   contentLabel.translatesAutoresizingMaskIntoConstraints = NO;
   [container addSubview:contentLabel];
    
    
    UIView* line = [UIView new];
    line.backgroundColor = kColorWithHex(CP_BACKGROUD_COLOR);
    line.translatesAutoresizingMaskIntoConstraints = NO;
    [container addSubview:line];
   
   [NSLayoutConstraint activateConstraints:@[
       [titleLabel.topAnchor constraintEqualToAnchor:container.topAnchor],
       [titleLabel.leadingAnchor constraintEqualToAnchor:container.leadingAnchor],
       [titleLabel.trailingAnchor constraintEqualToAnchor:container.trailingAnchor],
       
       [contentLabel.topAnchor constraintEqualToAnchor:titleLabel.bottomAnchor constant:hPix(4)],
       [contentLabel.leadingAnchor constraintEqualToAnchor:container.leadingAnchor],
       [contentLabel.trailingAnchor constraintEqualToAnchor:container.trailingAnchor],
       [contentLabel.bottomAnchor constraintEqualToAnchor:container.bottomAnchor constant: -hPix(16)],
       
       [line.bottomAnchor constraintEqualToAnchor:container.bottomAnchor],
       [line.widthAnchor constraintEqualToAnchor:container.widthAnchor],
       [line.heightAnchor constraintEqualToConstant:1],
   ]];
   
   return container;
}

- (void)animatePresent {
   self.containerLeftConstraint.constant = 0;
   [UIView animateWithDuration:0.3 animations:^{
       [self.view layoutIfNeeded];
   }];
}

- (void)dismissVC {
    self.containerLeftConstraint.constant = kScreenWid;
    if (self.navigationController) {
        [UIView animateWithDuration:0.3 animations:^{
            [self.view layoutIfNeeded];
        } completion:^(BOOL finished) {
            //[self.navigationController popViewControllerAnimated:YES];
            if (self.completionBlock) {
                self.completionBlock(@{@"code":EMAS_CANCEL});
            }else{
                [self.navigationController popViewControllerAnimated:YES];
            }
        }];
    }else{
        [UIView animateWithDuration:0.3 animations:^{
            [self.view layoutIfNeeded];
        } completion:^(BOOL finished) {
            //[self dismissViewControllerAnimated:NO completion:nil];
            if (self.completionBlock) {
                self.completionBlock(@{@"code":EMAS_CANCEL});
            }else{
                [self dismissViewControllerAnimated:NO completion:nil];
            }
        }];
    }
}

@end

3.1.4 Daftarkan plugin

3.1.4.1 Di Android
    private void initWindVaneMiniApp() {

        // 1. konfigurasi parameter MiniAppService
        MiniAppInitConfig config = new MiniAppInitConfig.Builder()
                .setUseWindVane(true)
                .setUseUniApp(false)
                .setHost("poc.superapp-intl.com")
                .setAppCode("YourAppCode")
                .setAccessKey("YourAccessKey")
                .setSecretKey("YourSecretKey")
                .build();

        // 2. buat objek miniAppService dan inisialisasi
        IMiniAppService miniAppService = new MiniAppService();
        miniAppService.initialize(application, config);

        ...

        // 2. daftarkan plugin
        miniAppService.registerPlugin("CustomUIPlugin", CustomUIPlugin(this));
    }
3.1.4.2 Di iOS
private func configMiniAppContainer() {
    // 1. konfigurasi parameter MiniAppService
    let miniAppInitConfig = EMASMiniAppInitConfig()
    miniAppInitConfig.accessKey = "YourAccessKey";
    miniAppInitConfig.secretKey = "YourSecretKey";
    miniAppInitConfig.host = "poc.superapp-intl.com";
    miniAppInitConfig.appCode = "YourAppCode";
    miniAppInitConfig.useUniApp = false
    miniAppInitConfig.useWindVane = true
    //2. buat objek miniAppService dan inisialisasi
    let miniAppService = EMASMiniAppServiceImpl();
    miniAppService.initialize(miniAppInitConfig)
    EMASServiceManager.sharedInstance().registerServiceProtocol("EMASMiniAppService", impClass: "EMASMiniAppServiceImpl", target: miniAppService)
    // 3. daftarkan plugin
    let uiPlugin = EMASClientCustomUIPlugin()
    miniAppService.registerPlugin("ClientCustomUIPlugin", plugin: uiPlugin)
}

3.2 Skenario Flutter

3.2.0 Bagan alir Interaksi Flutter

image.jpeg

3.2.1 Impor paket terkait

    // in build.gradle
    implementation "com.aliyun.emas.suite.foundation:mini-app-adapter:1.8.14"
    implementation "com.aliyun.emas.suite.foundation:windvane-mini-app:1.8.14"
    implementation "com.aliyun.emas.suite.foundation:mini-app-plugin-base:1.8.14"

// in podfile
pod 'EMASMiniAppAdapter', '1.1.4'
pod 'EMASWindVaneMiniApp', '1.2.5'
//if use bluetooth feature please pod WindVaneBluetooth
pod 'WindVaneBluetooth', '1.0.2'

3.2.2 Implementasikan FlutterAuthDialogFragment

Untuk klien yang ingin menggunakan halaman Flutter untuk menampilkan kotak pop-up, mereka perlu mengimplementasikan kontainer FlutterFragment terlebih dahulu.

public class FlutterAuthDialogFragment extends FlutterFragment {
    private static final String TAG = FlutterAuthDialogFragment.class.getName();


    // TODO: Ganti nama parameter argumen, pilih nama yang sesuai
    // dengan parameter inisialisasi fragment, misalnya ARG_ITEM_NUMBER
    private static final String ARG_ROUTE = "route";
    private static final String ARG_PARAM2 = "param2";

    // TODO: Ganti nama dan ubah jenis parameter
    private String mRoute;
    private String mParam2;

    private MethodChannel mMethodChannel;


    public FlutterAuthDialogFragment(String route) {
        mRoute = route;
    }


    public static FlutterAuthDialogFragment newInstance(String route, String dartEntrypoint) {

        FlutterAuthDialogFragment flutterFragment = new FlutterAuthDialogFragment(route);
        Bundle args = new Bundle();
        args.putString(ARG_FLUTTERVIEW_TRANSPARENCY_MODE, TransparencyMode.transparent.name());
        flutterFragment.setArguments(args);
        return flutterFragment;
    }

    @Nullable
    @Override
    public FlutterEngine provideFlutterEngine(@NonNull Context context) {
        FlutterEngine flutterEngine = new FlutterEngine(context);
        flutterEngine.getNavigationChannel().setInitialRoute(mRoute);
        flutterEngine.getDartExecutor().executeDartEntrypoint(
                DartExecutor.DartEntrypoint.createDefault()
        );
        return flutterEngine;
    }


    @Override
    public void onAttach(@NonNull android.content.Context context) {
        super.onAttach(context);
        initializeMethodChannel();
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {
            mRoute = getArguments().getString(ARG_ROUTE);
            mParam2 = getArguments().getString(ARG_PARAM2);
        }
    }

    private void initializeMethodChannel() {

        FlutterEngine flutterEngine = getFlutterEngine();
        if (flutterEngine == null) {
            Log.e("FlutterFragment", "FlutterEngine belum diinisialisasi");
            return;
        }

        mMethodChannel = new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), "flutter_auth_dialog");
        mMethodChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
            @Override
            public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
                IAuthService authService = PluginEnv.getInstance().getContainerContext().getAuthService();


                if (FlutterUIPluginConstant.FLUTTER_APPROVAL_AUTH_ACTION.equals(call.method)) {

                    Log.i(TAG, "approval auth");

                    Map<String, Object> arguments = (Map<String, Object>)call.arguments;
                    String permission = (String)arguments.get("permission");
                    String userId = (String)arguments.get("userId");
                    String miniAppId = (String)arguments.get("miniAppId");


                    // Tangani tindakan otorisasi persetujuan
                    authService.saveAuth(FlutterAuthDialogFragment.this.getActivity(), permission, userId, miniAppId);

                    // kembalikan 'result' ke kode flutter
                    result.success(true);
                } else if (FlutterUIPluginConstant.FLUTTER_CHECK_AUTH_ACTION.equals(call.method)) {
                    Log.i(TAG, "check auth");

                    Map<String, Object> arguments = (Map<String, Object>)call.arguments;
                    String permission = (String)arguments.get("permission");
                    String userId = (String)arguments.get("userId");
                    String miniAppId = (String)arguments.get("miniAppId");

                    // Tangani tindakan pemeriksaan otorisasi
                    boolean isAuthorized = authService.isAuthorized(FlutterAuthDialogFragment.this.getActivity(), permission, userId, miniAppId);

                    // kembalikan 'result' ke kode flutter
                    result.success(isAuthorized);
                } else {
                    result.notImplemented();
                }
            }
        });
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (mMethodChannel != null) {
            mMethodChannel.setMethodCallHandler(null);
        }
    }

}

iOS

import UIKit
import EMASMiniAppService

class EMASAliAuthUIViewController: FlutterViewController {
    var messageChannel: FlutterMethodChannel?
    var authBlock: (([AnyHashable : Any]) -> Void)?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = UIColor.init(white: 0.0, alpha: 0.5)
        //flutter_auth_dialog
        
    }
    
    func setAuthInfo(_ dic: [AnyHashable : Any]) {
        
        self.messageChannel = FlutterMethodChannel(name: "flutter_auth_dialog", binaryMessenger: self.binaryMessenger)
        
        self.messageChannel?.setMethodCallHandler({ [weak self] call, result in
            print("------\(call.method)")
            if call.method == "flutter_approval_auth_action" {
                self?.selectScope(call: call, result: result)
        
            }
            
        })
        
    }
    
    func selectScope(call: FlutterMethodCall, result: FlutterResult) {
        
        guard let arguments = call.arguments as? NSDictionary, let permission = arguments["permission"] as? String else {
            self.authBlock?(["code": EMAS_ERROR, "msg": "not get permission"])
            result(nil)
            return
        }
        print("------\(arguments)")
        self.authBlock?(["code": EMAS_CONFIRM, "msg": "", "data": [permission: true]])
        result(nil);
        
    }
    
}

3.2.3 Implementasikan halaman Auth flutter



import 'package:flutter/material.dart';
import 'package:fluttertest/windvane.dart';

class AuthListPage extends StatefulWidget {

  const AuthListPage({super.key});

  @override
  State<StatefulWidget> createState() {
    return AuthListPageState();
  }

}

class AuthListPageState extends State<AuthListPage> {

  final List<String> permissionsItem = [
    "location",
    "album",
    "camera",
    "bluetooth",
    "contacts",
    "microphone",
    "file",
    "call",
    "vibrate",
    "screen",
  ];
  final List<bool> approvalItem = [
    false,
    false,
    false,
    false,
    false,
    false,
    false,
    false,
    false,
    false,
  ];


  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 1000,
      child: ListView.builder(
        itemCount: permissionsItem.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(permissionsItem[index]),
            onTap: () {
              print("click item:${permissionsItem[index]}");

              Map<String, String> args = {
                "permission": permissionsItem[index],
                "userId": "123",
                "miniAppId": "17856810363",
              };

              // langkah 7: pengguna mengajukan/menolak otorisasi
              WindVaneMiniAppManager.authChannel.invokeMethod(approvalItem[index] ? "flutter_revoke_auth_action" : "flutter_approval_auth_action", args).then((result) {

                // langkah 9: menerima hasil otorisasi
                approvalItem[index] = result;

                // langkah 10: muat ulang halaman 
                setState(() {});
              });

            },
          );
        },
      ),
    );
  }
}

3.2.4 Implementasikan channel metode flutter

class WindVaneMiniAppManager {
  static const MethodChannel authChannel = const MethodChannel("flutter_auth_dialog");
}

3.2.5 Deklarasikan rute halaman flutter

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),

      onGenerateRoute: (RouteSettings settings) {
        final uri = Uri.parse(settings.name ?? '/');
        Map<String, String> params = uri.queryParameters;
        String path = uri.path;


        // langkah 1: daftarkan rute halaman auth flutter
        if (path == "/ui/auth") {

          return MaterialPageRoute(builder: (BuildContext context) {
            return AuthListPage();
          });

        } else if (path == "/ui/more") {

          return MaterialPageRoute(builder: (BuildContext context) {
            return const MoreDialogPage();
          });

        } else {
          ...
        }
      },
      initialRoute: 'home',
      builder: EasyLoading.init(),
    );
  }
}

3.2.6 Implementasikan antarmuka melalui plugin

public class FlutterUIPlugin extends BasePlugin implements UIExtension {


    public FlutterUIPlugin(Context context) {
        super(context);
    }

    // langkah 2: UIService memanggil createUserDeviceInfoAuthDialogFragment(...) untuk membuat fragment Dialog Auth
    @Override
    public Fragment createUserDeviceInfoAuthDialogFragment(HashMap<String, Object> params) {

        // "/ui/auth" adalah rute halaman flutter
        FlutterFragment fragment = FlutterAuthDialogFragment.newInstance("/ui/auth", params);
        return fragment;
    }

    ...
}

iOS

import UIKit
import EMASMiniAppService

class EMASAliCustomUIPlugin: EMASBasePlugin, EMASIUIExtension {

    func createUserDeviceInfoAuthDialogViewController(_ uiComponentParam: EMASUIComponentParam) -> UIViewController? {
        let vc = EMASAliAuthUIViewController()
        vc.setAuthInfo(uiComponentParam.params)
        vc.authBlock = uiComponentParam.completionBlock
        vc.setInitialRoute("/ui/auth")
        return vc
    }
}

3.2.7 Daftarkan plugin

    private void initWindVaneMiniApp() {

        // 1. konfigurasi parameter MiniAppService
        MiniAppInitConfig config = new MiniAppInitConfig.Builder()
                .setUseWindVane(true)
                .setUseUniApp(false)
                .setHost("poc.superapp-intl.com")
                .setAppCode("YourAppCode")
                .setAccessKey("YourAccessKey")
                .setSecretKey("YourSecretKey")
                .build();

        // 2. buat objek miniAppService dan inisialisasi
        IMiniAppService miniAppService = new MiniAppService();
        miniAppService.initialize(application, config);

        ...

        // 2. daftarkan plugin
        miniAppService.registerPlugin("FlutterUIPlugin", FlutterUIPlugin(this));
    }

iOS

    private func configMiniAppContainer() {
        // 1. konfigurasi parameter MiniAppService
        let miniAppInitConfig = EMASMiniAppInitConfig()
        miniAppInitConfig.accessKey = "YourAccessKey";
        miniAppInitConfig.secretKey = "YourSecretKey";
        miniAppInitConfig.host = "poc.superapp-intl.com";
        miniAppInitConfig.appCode = "YourAppCode";
        miniAppInitConfig.useUniApp = false
        miniAppInitConfig.useWindVane = true
        //2. buat objek miniAppService dan inisialisasi
        let miniAppService = EMASMiniAppServiceImpl();
        miniAppService.initialize(miniAppInitConfig)
        EMASServiceManager.sharedInstance().registerServiceProtocol("EMASMiniAppService", impClass: "EMASMiniAppServiceImpl", target: miniAppService)
        // 3. daftarkan plugin
        let aliUIPlugin = EMASAliCustomUIPlugin()
        miniAppService.registerPlugin("EMASAliCustomUIPlugin", plugin: aliUIPlugin)
    }

3.3 Halaman semi transparan Flutter

3.3.1 Halaman semi transparan

image.png

3.3.2 Di 'main.dart', pastikan latar belakang MaterialApp transparan:

void main() {
  runApp(
    MaterialApp(

      // atur latar belakang transparan
      theme: ThemeData(
        scaffoldBackgroundColor: Colors.transparent,
        canvasColor: Colors.transparent,
      ),

      
      onGenerateRoute: (RouteSettings settings) {
        final uri = Uri.parse(settings.name ?? '/');
        Map<String, String> params = uri.queryParameters;
        String path = uri.path;

        if (path == "/ui/auth") {
          return MaterialPageRoute(builder: (BuildContext context) {
            return AuthListPage();
          });
        } else {
          return MaterialPageRoute(builder: (BuildContext context) {
            return MyHomePage(title: settings.name.toString());
          });
        }
      },
    ),
  );
}

3.3.3 Demo halaman flutter semi transparan

class AuthListPage extends StatefulWidget {

  const AuthListPage({super.key});

  @override
  State<StatefulWidget> createState() {
    return AuthListPageState();
  }

}

class AuthListPageState extends State<AuthListPage> {

  final List<String> permissionsItem = [
    "location",
    "album",
    "camera",
    "bluetooth",
    "contacts",
    "microphone",
    "file",
    "call",
    "vibrate",
    "screen",
  ];
  final List<bool> approvalItem = [
    false,
    false,
    false,
    false,
    false,
    false,
    false,
    false,
    false,
    false,
  ];


  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Expanded(
          child: Container(
            // atur nilai semi transparan
            color: Colors.black.withOpacity(0.65),
          ),
        ),
        Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Container(
              height: 50,
              color: Colors.white,
              alignment: Alignment.center,
              child: const Text("The Auth List on the semi transparent page"),
            ),
            Container(
              height: 300,
              color: Colors.white,
              child: ListView.builder(
                itemCount: permissionsItem.length,
                itemBuilder: (context, index) {
                  return ListTile(
                    title: Text(permissionsItem[index]),
                    onTap: () {
                      print("click item:${permissionsItem[index]}");

                      Map<String, String> args = {
                        "permission": permissionsItem[index],
                        "userId": "123",
                        "miniAppId": "17856810363",
                      };

                      // langkah 7: pengguna mengajukan/menolak otorisasi
                      WindVaneMiniAppManager.authChannel.invokeMethod(approvalItem[index] ? "flutter_revoke_auth_action" : "flutter_approval_auth_action", args).then((result) {

                        // langkah 9: menerima hasil otorisasi
                        approvalItem[index] = result;

                        // langkah 10: muat ulang halaman
                        setState(() {});
                      });

                    },
                  );
                },
              ),
            ),
          ],
        ),
      ],
    );
  }
}

3.3.4 Buat FlutterFragment dan atur transparansi

3.3.4.1 Di Android
  public static FlutterAuthDialogFragment newInstance(String route, String dartEntrypoint) {

        FlutterAuthDialogFragment flutterFragment = new FlutterAuthDialogFragment(route);
        Bundle args = new Bundle();
        args.putString(ARG_FLUTTERVIEW_TRANSPARENCY_MODE, TransparencyMode.transparent.name());
        flutterFragment.setArguments(args);
        return flutterFragment;
    }

3.3.4.2 Di iOS
   override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = UIColor.init(white: 0.0, alpha: 0.5)
        //flutter_auth_dialog
        
    }

3.3.5 Untuk konten lebih detail

silakan merujuk ke branch "feature/flutter_page_in_native" di proyek flutter_windvane_demo.

4 Antarmuka Umum

4.1 Antarmuka Auth

4.1.1 Di Android

4.1.1.1 Antarmuka Auth dalam SDK
public interface IAuthService {

    public boolean saveAuth(Context context, String scope, String userId, String miniAppId);

    public boolean saveAuthList(Context context, List<String> scopes, String userId, String miniAppId);

    public boolean revokeAuth(Context context, String scope, String userId, String miniAppId);

    public boolean revokeAuthList(Context context, List<String> scopes, String userId, String miniAppId);
    
    public String queryAuthInfo(Context context, String userId, String miniAppId);

    public boolean isAuthorized(Context context, String scope, String userId, String miniAppId);
    
}

4.1.1.2 Gunakan AuthService
    IAuthService authService = PluginEnv.getInstance().getContainerContext().getAuthService();
    authService.saveAuth(context, scope, userId, miniAppId);

4.1.2 Di iOS

4.1.2.1 Antarmuka Auth dalam SDK

@protocol EMASIAuthService <NSObject>

@optional

- (BOOL)saveAuth:(NSString *)scope userId:(NSString *)userId miniAppId:(NSString *)miniAppId;
- (BOOL)saveAuthList:(NSArray<NSString *> *)scopes userId:(NSString *)userId miniAppId:(NSString *)miniAppId;

- (BOOL)revokeAuth:(NSString *)scope userId:(NSString *)userId miniAppId:(NSString *)miniAppId;
- (BOOL)revokeAuthList:(NSArray<NSString *> *)scopes userId:(NSString *)userId miniAppId:(NSString *)miniAppId;

- (NSDictionary *)queryAuthInfo:(NSString *)userId miniAppId:(NSString *)miniAppId;
- (BOOL)isAuthorized:(NSString *)scope userId:(NSString *)userId miniAppId:(NSString *)miniAppId;
    
@end
4.1.2.2 Gunakan AuthService
EMASPluginEnv.shareInstance().getContainerContext().getAuthService().saveAuthList!(scopes, userId: userId, miniAppId: miniAppId)

4.2 Antarmuka UI

4.2.1 Di Android

4.2.1.1 Antarmuka UI dalam SDK
public interface UIExtension {

       // Digunakan untuk membuat dialog 'More', lihat ‘gambar 1’ dalam catatan untuk contoh gaya
    public default Fragment createMoreDialogFragment(UIComponentParam uiComponentParam) {
        return  NoExistFragment.newInstance();
    }

    // Digunakan untuk membuat dialog 'Favorite', lihat ‘gambar 2’ dalam catatan untuk contoh gaya
    public default Fragment createFavoriteDialogFragment(UIComponentParam uiComponentParam) {
        return NoExistFragment.newInstance();
    }

    // Digunakan untuk membuat dialog 'Auth', lihat ‘gambar 3’ dalam catatan untuk contoh gaya
    public default Fragment createUserDeviceInfoAuthDialogFragment(UIComponentParam uiComponentParam) {
        return NoExistFragment.newInstance();
    }

    // Digunakan untuk membuat halaman anak 'Error', lihat ‘gambar 5’ dalam catatan untuk contoh gaya
    public default Fragment createErrorPageFragment(UIComponentParam uiComponentParam) {
        return NoExistFragment.newInstance();
    }

    // Digunakan untuk membuat halaman anak 'Slash', lihat ‘gambar 6’ dalam catatan untuk contoh gaya
    public default Fragment createSplashPageFragment(UIComponentParam uiComponentParam) {
        return NoExistFragment.newInstance();
    }

    // Digunakan untuk membuat dialog 'About US', lihat ‘gambar 8’ dalam catatan untuk contoh gaya
    public default Fragment createAboutUsDialogFragment(UIComponentParam uiComponentParam) {
        return NoExistFragment.newInstance();
    }
    // Digunakan untuk membuat halaman anak 'User Info Auth', lihat ‘gambar 7’ dalam catatan untuk contoh gaya
    public default Fragment createUserInfoAuthPageFragment(UIComponentParam uiComponentParam) {
        return NoExistFragment.newInstance();
    }
}
public class UIComponentParam {
    private UIResultCallBack uiResultCallBack;
    private Context context;
    private HashMap<String, Object> params;
  /**
  * uiResultCallBack: Data yang dikembalikan
  * data: Data yang dikirim
  * **/
    public UIComponentParam(UIResultCallBack uiResultCallBack, HashMap<String, Object> data, Context context) {
        this.uiResultCallBack = uiResultCallBack;
        this.params = data;
        this.context = context;
    }

    public UIResultCallBack getUiResultCallBack() {
        return uiResultCallBack;
    }

    public HashMap<String, Object> getParams() {
        return params;
    }

    public Context getContext() {
        return context;
    }
}

4.2.2 Di iOS

@protocol EMASIUIExtension <NSObject>

@optional

// Digunakan untuk membuat dialog 'More', lihat ‘figure 1’ pada catatan untuk contoh gaya
- (nullable UIViewController *)createMoreDialogViewController:(EMASUIComponentParam *)uiComponentParam;

// Digunakan untuk membuat dialog 'Favorite', lihat ‘figure 2’ pada catatan untuk contoh gaya
- (nullable UIViewController *)createFavoriteDialogViewController:(EMASUIComponentParam *)uiComponentParam;

// Digunakan untuk membuat dialog 'Auth', lihat ‘figure 3’ pada catatan untuk contoh gaya
- (nullable UIViewController *)createUserDeviceInfoAuthDialogViewController:(EMASUIComponentParam *)uiComponentParam;

// Digunakan untuk membuat sub halaman 'Error', lihat ‘figure 5’ pada catatan untuk contoh gaya
- (nullable UIViewController *)createErrorPageViewController:(EMASUIComponentParam *)uiComponentParam;

// Digunakan untuk membuat sub halaman 'Slash', lihat ‘figure 6’ pada catatan untuk contoh gaya
- (nullable UIViewController *)createSplashPageViewController:(EMASUIComponentParam *)uiComponentParam;

// Digunakan untuk membuat dialog 'About US', lihat ‘figure 8’ pada catatan untuk contoh gaya
- (nullable UIViewController *)createAboutUsDialogViewController:(EMASUIComponentParam *)uiComponentParam;

- (nullable UIViewController *)createSettingsDialogViewController:(EMASUIComponentParam *)uiComponentParam;

    
@end
@interface EMASUIComponentParam : NSObject

@property (nonatomic, strong) NSDictionary * params;
@property (nonatomic, copy) void(^completionBlock)(NSDictionary *resultDict);

@end
FOUNDATION_EXPORT NSString * _Nonnull const EMAS_CANCEL;

FOUNDATION_EXPORT NSString * _Nonnull const EMAS_CONFIRM;

FOUNDATION_EXPORT NSString * _Nonnull const EMAS_RESTART_MINIAPP;

FOUNDATION_EXPORT NSString * _Nonnull const EMAS_EXIT_MINIAPP;

FOUNDATION_EXPORT NSString * _Nonnull const EMAS_ERROR;

FOUNDATION_EXPORT NSString * _Nonnull const EMAS_OPEN_ABOUT;

FOUNDATION_EXPORT NSString * _Nonnull const EMAS_OPEN_SETTING;

5 UI Default Baru dalam SDK

figure 1

more dialog

image.png

figure 2

favorite dialog

image.png

figure 3

auth dialog

image.png

figure 4

loading dialog

image.png

figure 5

error page

image.png

figure 6

splash page

image.png

figure 7

user info auth page

image.png

figure 8

about us dialog

image.png