← Back to list

How to Implement HCE Payment in Smart Wearables?

Today, we will learn how to use our smart watches to emulate cards and make payments.

Mehmet Karaaslan in Huawei Developers · 2026-02-26 07:20 · 50 claps · 5.7 min read
#huawei #che #nfc #wearables #ark-ts
Open on Medium ↗
Wiki topics: DH · Digital Health & Health Tech FIN · Fintech & Banking 📟 · Gadgets & IoT

How to Implement HCE Payment in Smart Wearables?

Let’s build with ArkTS

HCE Payment

HCE Payment

Introduction

Hello everybody.

Today, we will learn how to use our smart watches to emulate cards and make payments. Let’s roll.

But first, allow me to give you brief domain knowledge:

EMV is a payment method based on a technical standard developed by Europay, Mastercard, and Visa.

Developed for payment cards, Point Of Sale (POS) terminals, and automated teller machines (ATM)

An application identifier (AID) is used to address an application in the card or Host Card Emulation (HCE) device.

It is used to identify the type of product, so that all product issuers (Visa, Mastercard, etc.) must have their own application.

EMV uses predefined protocols for data transmission, and data is exchanged in application protocol data units (APDUs)

For more information, check: https://en.wikipedia.org/wiki/EMV

Configurations

Let’s start with configurations. Modify module.json5.

  • Add permission
"requestPermissions": [
  {
    "name": "ohos.permission.NFC_CARD_EMULATION",
    "reason": "$string:NFC_reason"
  }
]
  • Add card emulation action
"skills": [
  {
    "entities": [
      "entity.system.home"
    ],
    "actions": [
      "ohos.want.action.home",
      "ohos.nfc.cardemulation.action.HOST_APDU_SERVICE"
    ]
  }
],
  • Add payment-aid
"metadata": [
  {
    "name": "payment-aid",
    "value": "325041592E5359532E4444463031" // Visa International
  {
    "name": "payment-aid",
    "value": "A0000000031010" // Visa credit or debit
  },
  {
    "name": "payment-aid",
    "value": "A0000000041010" // Mastercard credit or debit
  }
]

An application identifier (AID) is used to address an application in the card or Host Card Emulation (HCE) if delivered without a card.

https://en.wikipedia.org/wiki/EMV

RDB

We will use the relational database to store the Card and Suks. We will not go into details of the RDB. You can check the project for the implementation.

Let’s define our data models. We will create these under the model directory.

  • Card.ets
export interface Card {
  name: string;
  // TODO: define rest of the parameters if needed
}
  • SUK.ets
export interface SUK {
  // TODO: define necessary parameters
}

export interface DbSUK {
  id: number;
  suk: SUK;
}

In our app, there will be only one card and multiple SUKs. I can see you smiling and trying to understand why we have the DbSUK interface. In real-life scenarios, using the power of JS, you can convert a given JSON data to an object. So we use DbSUK to benefit from this power and store SUK data without defining all the parameters. Also, we need a PK for our database.

HceHandler

Here is the main deal. Let’s see what it is.

import { cardEmulation } from '@kit.ConnectivityKit';

This is the main character. As its name gives away, we use this kit to make our app emulate a bank card.

type rt = 'foreground' | 'background';

We will keep track of whether our app runs in the foreground or background. This is not necessary in many cases, but in our case, we will route the app to the success page after the card read operation is successfull and as you might guess route operation causes the app to fail if it is called in the background.

await HceHandler.hceService!.transmit(response);

This is how we send a response to the POS machine. Basically, payment is just a communication with the POS machine. You just need to know what to send.

import { cardEmulation } from '@kit.ConnectivityKit';
import { bundleManager } from '@kit.AbilityKit';
import { Rdb } from '../rdb/Rdb';
import { CardTable } from '../rdb/tables/CardTable';
import { SUKTable } from '../rdb/tables/SukTable';

type rt = 'foreground' | 'background';

export default class HceHandler {
  private constructor() {
  }

  private static hceService: cardEmulation.HceService | undefined | null = undefined;
  private static runType: rt = 'background';

  static runInBackground() {
    HceHandler.runType = 'background';
  }

  static enable(context: Context, _runType: rt, pageStack?: NavPathStack) {
    console.info('start enable');
    HceHandler.runType = _runType;
    // first check availability
    if (canIUse('SystemCapability.Communication.NFC.Core') && cardEmulation.hasHceCapability()) {
      try {
        const hceElementName: bundleManager.ElementName = {
          bundleName: 'com.hmosdemos.hcepayment', // TODO: this must match with the app's bundle
          abilityName: 'EntryAbility',
          moduleName: 'entry'
        };

        // TODO: this must match with module.json5
        const paymentAid = [
          '325041592E5359532E4444463031',
          'A0000000031010',
          'A0000000041010'
        ];

        HceHandler.hceService?.stop(hceElementName);
        HceHandler.hceService = new cardEmulation.HceService();
        HceHandler.hceService.start(hceElementName, paymentAid);

        HceHandler.hceService.on('hceCmd', async (err, data) => {
          console.info(`hceCmd ${data}`);
          if (err) {
            console.error(`HceHandler callback Error: ${err}`);
            return;
          }

          // get card data from db
          await Rdb.instance.init(context);
          const card = CardTable.get();
          const suk = SUKTable.getLast();

          if (card && suk) {
            // TODO: make payment
            // Basically payment is just a communication with the POS machine.
            // You just need to know what to send.
            // GOOD LUCK :)

            // send a dummy response
            const response = [0x47, 0x4F, 0x4F, 0x44, 0x20, 0x4C, 0x55, 0x43, 0x4B, 0x20, 0x3A, 0x29, 0x90, 0x00];
            await HceHandler.hceService!.transmit(response);

            // simulate success
            // delete last suk
            SUKTable.delete(suk);
            console.info(`payment success remaining suk: ${SUKTable.getCount()}`);

            // route to success page
            if (HceHandler.runType === 'foreground' && pageStack) {
              const lastPage = pageStack.getAllPathName().pop();
              if (lastPage === 'CardTransferSuccess' || lastPage === 'NoSuk') {
                pageStack.replacePath({ name: 'CardTransferSuccess' });
              } else {
                pageStack.pushPath({ name: 'CardTransferSuccess' });
              }
            }

          } else if (card && SUKTable.getCount() === 0) {
            // route to no suk page
            if (HceHandler.runType === 'foreground' && pageStack) {
              const lastPage = pageStack.getAllPathName().pop();
              if (lastPage === 'CardTransferSuccess') {
                pageStack.replacePath({ name: 'NoSuk' });
              } else if (lastPage !== 'NoSuk') {
                pageStack.pushPath({ name: 'NoSuk' });
              }
            }
          }
        });
      } catch (error) {
        console.error(`HceHandler errCode: ${error.code} errMessage: ${error.message}`);
      }
    }
  }
};

User Interface

  • Success Page — CardReadSuccess.ets
import { SUKTable } from '../rdb/tables/SukTable';

@Component
export default struct CardTransferSuccess {
  @Consume('mainStack') pageStack: NavPathStack;
  sukCount: number = SUKTable.getCount();

  build() {
    NavDestination() {
      Column() {
        Column() {
          SymbolGlyph($r('sys.symbol.checkmark_circle_fill'))
            .fontColor([Color.Green])
            .fontSize(24)
            .margin({ bottom: 8 });
          Text(`Card Transfer Success, Remaining: ${this.sukCount}`);
        }
        .size({ width: '70%', height: '70%' })
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.Center);
      }
      .size({ width: '100%', height: '100%' })
      .backgroundColor(Color.Black)
      .alignItems(HorizontalAlign.Center)
      .justifyContent(FlexAlign.Center);
    }
    .hideTitleBar(true);
  }
}
  • NoSuk.ets
@Component
export default struct NoSuk {
  @Consume('mainStack') pageStack: NavPathStack;

  build() {
    NavDestination() {
      Column() {
        Column() {
          SymbolGlyph($r('sys.symbol.xmark_circle_fill'))
            .fontColor([Color.Red])
            .fontSize(24)
            .margin({ bottom: 8 });
          Text('No suk left.');
        }
        .size({ width: '70%', height: '70%' })
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.Center);
      }
      .size({ width: '100%', height: '100%' })
      .backgroundColor(Color.Black)
      .alignItems(HorizontalAlign.Center)
      .justifyContent(FlexAlign.Center);
    }
    .hideTitleBar(true);
  }
}
  • And finally, Index.ets
import CardTransferSuccess from '../components/CardTransferSuccess';
import NoSuk from '../components/NoSuk';
import { SUKTable } from '../rdb/tables/SukTable';
import HceHandler from '../util/HceHandler';
import CardVM from '../viewmodel/CardVM';

@Entry
@Component
struct Index {
  @Provide('mainStack') pageStack: NavPathStack = new NavPathStack();
  @State cardVM: CardVM = CardVM.getInstance();

  onPageShow(): void {
    HceHandler.enable(this.getUIContext().getHostContext()!, 'foreground', this.pageStack);
  }

  build() {
    Navigation(this.pageStack) {
      Stack() {
        Column() {
          Blank().layoutWeight(1);

          Column() {
            Text('Contactless Payment')
              .fontSize(12);

            Text(this.cardVM.card?.name ?? 'Card Not Found')
              .fontSize(10);
          }
          .alignItems(HorizontalAlign.Start)
          .padding(6)
          .backgroundColor(Color.Gray)
          .width('100%')
          .borderRadius(8);

          Blank().layoutWeight(1);

          Row() {
            Button('Add Card')
              .layoutWeight(1)
              .height(30)
              .type(ButtonType.Normal)
              .backgroundColor(Color.Green)
              .onClick(() => {
                CardVM.getInstance().add({ name: 'My Lucky Card' });
                SUKTable.add([{}, {}, {}, {}, {}]);
              });
            Button('Remove')
              .layoutWeight(1)
              .height(30)
              .type(ButtonType.Normal)
              .backgroundColor(Color.Red)
              .onClick(() => {
                CardVM.getInstance().delete();
                SUKTable.deleteAll();
              });
          };
        }
        .height('70%')
        .width('70%');
      }
      .backgroundColor(Color.Black)
      .height('100%')
      .width('100%');
    }
    .navDestination(this.pageMap)
    .hideToolBar(true);
  }

  @Builder
  pageMap(name: string) {
    if (name === 'CardTransferSuccess') {
      CardTransferSuccess();
    } else if (name === 'NoSuk') {
      NoSuk();
    }
  }
}

You may wonder what this is:

@State cardVM: CardVM = CardVM.getInstance();

Don’t worry. I got you.

In normal cases, you might get card data from an API or from a mobile app. Here we will use mock data to simulate the process, and CardVM to handle app state.

import { Card } from '../model/Card';
import { CardTable } from '../rdb/tables/CardTable';

@Observed
export default class CardVM {
  private static instance: CardVM;

  private constructor() {
  }

  public static getInstance(): CardVM {
    if (!CardVM.instance) {
      CardVM.instance = new CardVM();
    }
    return CardVM.instance;
  }

  card: Card | null = CardTable.get();

  add(card: Card) {
    this.delete()
    CardTable.add(card);
    this.card = card;
  }

  delete() {
    CardTable.delete()
    this.card = null;
  }
}

Result

It is good, ain’t it?

It is good, ain’t it?

Conclusion

That’s all for this article. We have covered host card emulation in smart wearables.

You can check the project in GitHub: https://github.com/Explore-In-HMOS-Wearable/how-to-use-hce

See you all in new adventures. :)

~ Fortuna Favet Fortibus

References

[embed]GitHub - Explore-In-HMOS-Wearable/how-to-use-hce Contribute to Explore-In-HMOS-Wearable/how-to-use-hce development by creating an account on GitHub.github.com

[embed]EMV - Wikipedia EMV is a payment method based on a technical standard for smart payment cards and for payment terminals and automated…en.wikipedia.org

[embed]HarmonyOS-NFC-HCE-Guide developer.huawei.com


메타데이터
post_id
60481c64c439
slug
how-to-implement-hce-payment-in-smart-wearables-60481c64c439
url
https://medium.com/huawei-developers/how-to-implement-hce-payment-in-smart-wearables-60481c64c439
canonical_url
https://medium.com/huawei-developers/how-to-implement-hce-payment-in-smart-wearables-60481c64c439
author_url
https://medium.com/@mrkaraaslan
status
ok
fetched_at
2026-06-14 11:28:49