Tapaya
Chấp nhận thanh toán tại cửa hàngHướng dẫn tích hợpTag NFCBeta

Tag NFC

AndroidBeta

Đọc và ghi tag NFC từ ứng dụng Android của bạn, bằng NFC của điện thoại hoặc đầu đọc của máy thanh toán Sunmi dòng P.

Đọc và ghi tag NFC, chẳng hạn thẻ khách hàng thân thiết, thẻ ra vào, nhãn dán và thẻ thông minh, trực tiếp từ ứng dụng Android của bạn. SDK giao tiếp với chính đầu đọc của thiết bị: Android NFC trên điện thoại, hoặc đầu đọc không tiếp xúc của máy thanh toán Sunmi dòng P. Không có plugin nào đứng giữa.

Phiên đọc tag được cung cấp trong các artifact tùy chọn, com.tapaya:accept-nfc và, cho máy Sunmi, com.tapaya:accept-nfc-sunmi. Chỉ thêm các artifact này nếu bạn cần; thanh toán bằng thẻ vẫn hoạt động như nhau dù có hay không có chúng.

Thiết bị được hỗ trợ

Thiết bịĐầu đọcArtifact bạn cần
Điện thoại hoặc máy tính bảng Android có NFCAndroid NFCaccept-nfc
Máy thanh toán Sunmi dòng PĐầu đọc không tiếp xúc của máy, thông qua dịch vụ thanh toán (pay service) của Sunmiaccept-nfc và accept-nfc-sunmi
Mọi thiết bị khácKhông cóKhông được hỗ trợ

Máy thanh toán Sunmi dòng P không có Android NFC; ăng-ten của chúng nằm phía sau mô-đun thanh toán. accept-nfc-sunmi truy cập ăng-ten này thông qua dịch vụ thanh toán của Sunmi, và được dùng thay cho Android NFC bất cứ khi nào dịch vụ đó được cài đặt.

Trên thiết bị không có đầu đọc dùng được, isSupported() trả về false và openSession() thất bại với NOT_SUPPORTED.

Điện thoại có thêm một vài giới hạn

Điện thoại chỉ đọc tag cho activity đang ở foreground, nên phiên sẽ kết thúc khi activity đó rời khỏi foreground. MIFARE Classic chỉ hoạt động trên điện thoại có bộ điều khiển NFC của NXP; trên các điện thoại khác, các thao tác của nó thất bại với NOT_SUPPORTED. Với tag ISO-DEP, atsHex chỉ chứa các byte lịch sử (historical bytes) của ATS, vì Android không cung cấp phần còn lại.

Tag được hỗ trợ

Loại tagVí dụ
NFC Type 2NTAG, MIFARE Ultralight, Ultralight EV1, Ultralight C
MIFARE ClassicMIFARE Classic Mini, 1K, 4K
Thẻ thông minh ISO-DEPMIFARE DESFire

Cách hoạt động

  1. Ứng dụng của bạn gọi Accept.nfc.openSession(activity).
  2. SDK bật đầu đọc của thiết bị và chờ chạm tag.
  3. Khách hàng chạm một tag, và openSession() trả về một TagSession mang UID và họ (family) của tag.
  4. Ứng dụng của bạn đọc và ghi thông qua phiên, mỗi lần một lệnh.
  5. Ứng dụng của bạn gọi close(), thao tác này giải phóng đầu đọc cho thanh toán và các phiên đọc tag khác.

Thanh toán luôn được ưu tiên

Mỗi thiết bị chỉ có một đầu đọc không tiếp xúc, và phiên đọc tag với thanh toán bằng thẻ không thể dùng chung đầu đọc đó. Việc bắt đầu một khoản thanh toán bằng Accept.payments.pay() sẽ kết thúc phiên của bạn, và openSession() thất bại với READER_BUSY cho tới khi khoản thanh toán kết thúc. Hãy sẵn sàng cho việc phiên có thể kết thúc bất cứ lúc nào; xem Kết thúc phiên.

Điều kiện tiên quyết

Phiên đọc tag không cần merchant đã đăng nhập, plugin Tapaya Terminal hay quyền truy cập vị trí. Các artifact tự thêm những gì chúng cần vào manifest của bạn:

  • accept-nfc thêm quyền NFC và đánh dấu android.hardware.nfc là không bắt buộc, để Play vẫn cung cấp ứng dụng của bạn trên máy Sunmi, vốn không có Android NFC.
  • accept-nfc-sunmi thêm quyền com.sunmi.perm.CONTACTLESS_CARD và một mục <queries> cho dịch vụ thanh toán của Sunmi. Artifact này nhúng sẵn Sunmi PayLib.

Tích hợp

Cài đặt

build.gradle.kts
dependencies {
    implementation("com.tapaya:accept-nfc:1.20.0")

    // Only if you run on Sunmi P-series terminals
    implementation("com.tapaya:accept-nfc-sunmi:1.20.0")
}

Nếu bạn cũng khai báo com.tapaya:accept, hãy giữ tất cả các artifact ở cùng một phiên bản.

Accept.nfc là một extension property; hãy import trước khi sử dụng:

import com.tapaya.accept.nfc.nfc

Kiểm tra khả năng hỗ trợ

Gọi isSupported() để quyết định có hiển thị tính năng tag hay không:

if (Accept.nfc.isSupported()) {
    showScanTagButton()
}

isSupported() không cần chạm và không động tới đầu đọc, nên có thể gọi an toàn bất cứ lúc nào. Hàm cho bạn biết thiết bị có đầu đọc mà SDK dùng được. Hàm trả về true trên điện thoại đã tắt NFC; khi đó openSession() thất bại với NFC_DISABLED. Hàm ném ra NotInitialized nếu Accept.initialize chưa chạy.

Mở phiên

Truyền vào activity đang ở foreground và ở trạng thái resumed:

val session = try {
    Accept.nfc.openSession(activity)
} catch (e: TagSessionException) {
    when (e.reason) {
        TagErrorReason.TIMEOUT -> showMessage("No tag detected. Try again.")
        TagErrorReason.READER_BUSY -> showMessage("The reader is busy. Try again soon.")
        TagErrorReason.NFC_DISABLED -> promptToEnableNfc()
        else -> showMessage("Can't read tags on this device.")
    }
    return
}

try {
    showTag(session.info.uidHex, session.info.family)
    // read and write...
} finally {
    session.close()
}

openSession() tạm dừng (suspend) cho tới khi một tag được chạm hoặc hết tapWait (mặc định 30 giây). Hãy hiển thị lời nhắc “Chạm thẻ của bạn” trong lúc chờ. Hàm ném ra IllegalStateException nếu activity không ở trạng thái resumed.

Trên điện thoại, activity đóng vai trò quan trọng: Android chỉ đọc tag cho activity đó, và khi activity rời khỏi foreground, việc chờ sẽ dừng hoặc phiên kết thúc với ACTIVITY_PAUSED. Máy Sunmi đọc tag bất kể thứ gì đang ở foreground, nhưng bạn vẫn truyền vào một activity.

Để dừng chờ, chẳng hạn khi khách hàng nhấn Cancel, hãy hủy coroutine đã gọi openSession(). Đầu đọc sẽ ngừng chờ, và bạn có thể mở một phiên mới ngay lập tức.

Đọc thông tin phiên

session.info mô tả tag đã được chạm:

TrườngMô tả
uidHexUID của tag dưới dạng hex viết hoa, ví dụ 04A1B2C3D4E5F6.
familyTagFamily, cho bạn biết những thao tác nào áp dụng được.
sak, atqaHexThông tin nhận dạng cấp thấp được trả về khi phát hiện tag.
atsHexATS cho tag ISO_DEP; nếu không thì là null.
memoryBytesDung lượng bộ nhớ người dùng, khi đầu đọc xác định được; nếu không thì là null.
versionHexPhản hồi GET_VERSION, với các tag hỗ trợ lệnh này; nếu không thì là null.

Đọc và ghi tag

Rẽ nhánh theo session.info.family, sau đó dùng các thao tác dành cho loại tag đó:

when (session.info.family) {
    TagFamily.NTAG, TagFamily.ULTRALIGHT, TagFamily.ULTRALIGHT_EV1,
    TagFamily.ULTRALIGHT_C, TagFamily.TYPE2_OTHER -> readType2Tag(session)
    TagFamily.MIFARE_CLASSIC_MINI, TagFamily.MIFARE_CLASSIC_1K,
    TagFamily.MIFARE_CLASSIC_4K -> readClassicCard(session)
    TagFamily.ISO_DEP -> readSmartCard(session)
    else -> showMessage("This tag isn't supported.")
}

Một lệnh không phù hợp với tag đã chạm sẽ thất bại với NOT_SUPPORTED, và phiên vẫn mở.

NFC Type 2

NTAG, MIFARE Ultralight, Ultralight EV1, Ultralight C và các tag Type 2 khác (TYPE2_OTHER).

Thao tácChức năng
readPages(startPage)Trả về 16 byte: bốn trang 4 byte bắt đầu từ startPage.
writePage(page, data)Ghi một trang 4 byte.
passwordAuth(password)Chỉ dành cho NTAG và Ultralight EV1. Mở khóa các trang được bảo vệ bằng mật khẩu 4 byte và trả về PACK 2 byte, bạn có thể dùng PACK để kiểm tra tag có phải là tag chính hãng hay không.
ultralightCAuth(key)Chỉ dành cho Ultralight C. Xác thực bằng khóa 3DES 16 byte.
transceiveType2(frame)Gửi một frame Type 2 thô, cho các lệnh không có phương thức riêng, và trả về phản hồi của tag.
// Protected NTAG and Ultralight EV1 tags only
session.passwordAuth(password = byteArrayOf(0x12, 0x34, 0x56, 0x78))

val pages = session.readPages(startPage = 4)  // pages 4 to 7
session.writePage(page = 4, data = byteArrayOf(0x01, 0x02, 0x03, 0x04))

// Raw GET_VERSION command
val version = session.transceiveType2(byteArrayOf(0x60))

MIFARE Classic

MIFARE Classic Mini, 1K và 4K. Hãy xác thực một sector trước khi đọc hoặc ghi các block của nó.

Thao tácChức năng
authenticateClassic(block, keyType, key)Xác thực sector chứa block bằng một khóa 6 byte.
readBlock(block)Trả về 16 byte của block.
writeBlock(block, data)Ghi 16 byte vào block.
increment, decrement, restoreĐưa kết quả của thao tác trên value block vào bộ đệm truyền (transfer buffer) của tag.
transfer(block)Ghi bộ đệm truyền vào một block.
session.authenticateClassic(block = 4, keyType = MifareKeyType.A, key = sectorKey)
val block = session.readBlock(4)
session.writeBlock(4, newContent)

// Value blocks: change the value, then write it back
session.decrement(block = 5, value = 1)
session.transfer(block = 5)

Khóa sai sẽ thất bại với AUTH_FAILED, và SDK tự chọn lại (re-select) tag cho bạn, nên bạn có thể thử khóa tiếp theo ngay lập tức:

val key = candidateKeys.firstOrNull { key ->
    try {
        session.authenticateClassic(block = 4, keyType = MifareKeyType.A, key = key)
        true
    } catch (e: TagSessionException) {
        if (e.reason != TagErrorReason.AUTH_FAILED) throw e
        false
    }
} ?: error("no key for sector 1")

Thẻ thông minh ISO-DEP

MIFARE DESFire và các thẻ ISO 14443-4 khác.

Thao tácChức năng
transmitApdu(apdu)Gửi một APDU lệnh và trả về phản hồi: dữ liệu theo sau là SW1 SW2.
val select = byteArrayOf(0x00, 0xA4.toByte(), 0x04, 0x00, 0x07,
    0xA0.toByte(), 0x00, 0x00, 0x00, 0x03, 0x10, 0x10, 0x00)
val response = session.transmitApdu(select)
val sw = response.takeLast(2)   // [0x90, 0x00] on success

Kết thúc phiên

Gọi close() ngay khi bạn xong việc, tốt nhất là từ một khối finally. Cho tới khi bạn làm vậy, không khoản thanh toán nào có thể dùng đầu đọc. Có thể gọi close() an toàn nhiều lần, và hàm không bao giờ ném ngoại lệ với một phiên đã kết thúc.

Phiên cũng có thể tự kết thúc. session.ended là một StateFlow giữ giá trị null trong khi phiên còn hoạt động, sau đó phát ra lý do phiên kết thúc:

session.ended.filterNotNull().first().let { reason ->
    when (reason) {
        TagSessionEndReason.CARD_REMOVED -> showMessage("Card removed")
        TagSessionEndReason.PAYMENT -> showMessage("A payment took over the reader")
        TagSessionEndReason.ACTIVITY_PAUSED -> showMessage("Keep the app in the foreground")
        else -> Unit
    }
}
TagSessionEndReasonKhi nào xảy ra
CLOSEDỨng dụng của bạn đã gọi close().
CARD_REMOVEDTag đã rời khỏi vùng đọc và không quay lại trong thời gian chờ reselect.
IDLE_TIMEOUTKhông có lệnh nào đến trong thời gian chờ idle (mặc định 10 giây).
PAYMENTMột khoản thanh toán đã bắt đầu qua Accept.payments.pay(). Thanh toán luôn được ưu tiên.
ACTIVITY_PAUSEDChỉ trên điện thoại. Activity mà bạn dùng để mở phiên đã rời khỏi foreground, và Android ngừng đọc tag cho activity đó.
READER_LOSTChỉ trên máy Sunmi. Kết nối tới dịch vụ thanh toán của Sunmi bị ngắt.

Sau khi phiên đã kết thúc, mọi lệnh tiếp theo đều thất bại với SESSION_ENDED. Hãy gọi lại openSession() cho một lần chạm mới.

Xử lý lỗi

Lỗi liên quan tới tag sẽ ném ra TagSessionException, với reason cho bạn biết cần làm gì tiếp theo. Gọi API trước khi SDK được thiết lập sẽ ném ra NotInitialized. TagSessionException không phải là một AcceptException, vì vậy hãy bắt riêng hai loại này:

try {
    val session = Accept.nfc.openSession(activity)
    // ...
} catch (e: TagSessionException) {
    // reason-specific handling, see the tables below
} catch (e: NotInitialized) {
    // Accept.initialize hasn't been called
}

Xác thực lại khi authLost được đặt

Với bất kỳ lý do nào, cờ authLost của ngoại lệ đều có thể được đặt. Khi cờ này được đặt, hãy xác thực lại trước khi gửi lệnh tiếp theo tới vùng nhớ được bảo vệ; với DESFire, hãy chọn lại cả ứng dụng (application).

Đối số khóa, block hoặc trang có độ dài hoặc dấu không đúng, hoặc frame hay APDU rỗng, sẽ ném ra IllegalArgumentException trước khi bất cứ thứ gì đến được tag.

Mở phiên

reasonKhi nào xảy raCần làm gì
TIMEOUTKhông có lần chạm nào trong tapWait.Nhắc lại, hoặc dừng.
READER_BUSYMột khoản thanh toán hoặc phiên khác đang dùng đầu đọc.Thử lại sau; không có hàng đợi.
SESSION_ENDEDMột khoản thanh toán đã bắt đầu, hoặc activity đã rời khỏi foreground, trong lúc chờ chạm.Mở một phiên mới khi khoản thanh toán đã kết thúc hoặc ứng dụng đã quay lại foreground.
NFC_DISABLEDChỉ trên điện thoại. NFC bị tắt trong cài đặt hệ thống.Yêu cầu người dùng bật NFC.
VENDOR_ERRORMáy Sunmi: dịch vụ thanh toán không khả dụng. vendorCode chứa mã lỗi của dịch vụ, nếu có.Thử lại. Nếu lỗi vẫn tiếp diễn, hãy báo cho bộ phận hỗ trợ nhà phát triển kèm theo vendorCode.
NOT_SUPPORTEDThiết bị không có đầu đọc mà SDK dùng được.Ẩn tính năng khi isSupported() trả về false.

Gửi lệnh

reasonKhi nào xảy raCần làm gì
TIMEOUTTag không phản hồi kịp thời; tag đã được chọn lại.Thử lại. Nếu authLost được đặt, hãy xác thực trước.
NO_RESPONSETag không phản hồi.Thử lại nếu tag vẫn còn trong vùng đọc.
AUTH_FAILEDSai khóa hoặc mật khẩu.Thử khóa tiếp theo; tag đã được chọn lại.
NAKTag từ chối lệnh: trang nằm ngoài phạm vi, vùng nhớ được bảo vệ hoặc lệnh không được hỗ trợ.Sửa lệnh. Phiên vẫn tiếp tục.
NOT_SUPPORTEDLệnh không phù hợp với họ tag này, hoặc với bộ điều khiển NFC của điện thoại này (MIFARE Classic cần NXP).Kiểm tra session.info.family. Phiên vẫn tiếp tục.
VENDOR_ERRORSDK riêng của đầu đọc báo lỗi; vendorCode chứa mã lỗi.Thử lại, sau đó báo cho bộ phận hỗ trợ nhà phát triển kèm theo vendorCode.

Phiên đã kết thúc

reasonKhi nào xảy raCần làm gì
CARD_REMOVEDTag đã rời khỏi vùng đọc trong lúc thực hiện lệnh.Yêu cầu chạm lại và mở một phiên mới.
SESSION_ENDEDMột lệnh được gửi sau khi phiên đã kết thúc.Mở một phiên mới.

Thời gian chờ

Truyền TagTimeouts vào openSession() để thay đổi giá trị mặc định:

Accept.nfc.openSession(activity, TagTimeouts(tapWait = 15.seconds, idle = 30.seconds))
Thời gian chờMặc địnhTối đaÁp dụng cho
tapWait30 s60 sChờ chạm trong openSession()
operation1 s5 sMỗi lệnh trên tag
reselect1 s5 sChọn lại tag sau một lệnh thất bại
idle10 s60 sKhoảng thời gian giữa các lệnh trước khi phiên kết thúc

Mọi giá trị đều phải lớn hơn 0. Tạo TagTimeouts với giá trị nằm ngoài các khoảng này sẽ ném ra IllegalArgumentException.

Cần lưu ý

  • Một đầu đọc. Thiết bị chỉ có một đầu đọc không tiếp xúc, nên mỗi lúc chỉ có một phiên đọc tag hoặc một lượt chờ chạm. Lệnh gọi openSession() thứ hai sẽ thất bại với READER_BUSY. Khoản thanh toán bắt đầu bằng Accept.payments.pay() sẽ kết thúc phiên trước; khoản thanh toán bắt đầu bên ngoài SDK thì không thể điều phối, vì vậy hãy giữ phiên ngắn gọn.
  • Hủy một lệnh không hoàn tác lệnh đó. Vào lúc bạn hủy coroutine, lệnh đã tới tag và vẫn được thực thi; chỉ kết quả của nó bị bỏ đi. Một writeBlock đã bị hủy vẫn có thể thay đổi tag.
  • Các lệnh chạy theo thứ tự. Có thể gọi các phương thức của phiên từ bất kỳ coroutine nào; chúng tới tag lần lượt từng lệnh một, theo đúng thứ tự bạn gọi.
  • Dữ liệu tag không xuất hiện trong log. SDK không bao giờ ghi log khóa, mật khẩu hay dữ liệu tag, và che UID (ví dụ 04A1…E5F6). Việc ứng dụng của bạn lưu trữ và hiển thị dữ liệu tag như thế nào là do bạn quyết định.

Kiểm thử tích hợp

Trên điện thoại và, nếu bạn hỗ trợ, trên máy thanh toán Sunmi dòng P:

  1. Kiểm tra rằng isSupported() trả về true trên cả hai, và false trên thiết bị không có NFC.
  2. Chạm một tag NTAG hoặc Ultralight và xác minh rằng phiên báo đúng UID và readPages(0) trả về 16 byte. Sau đó thực hiện writePage và đọc lại trang đó.
  3. Trên MIFARE Classic, xác thực bằng một khóa sai và xác nhận nhận được AUTH_FAILED, sau đó xác thực bằng khóa đúng.
  4. Hủy openSession() trong lúc chờ chạm và mở ngay một phiên mới.
  5. Bắt đầu một khoản thanh toán trong khi phiên đang mở: phiên sẽ kết thúc với PAYMENT và khoản thanh toán được thực hiện thành công. Lấy tag ra trong một phiên khác: phiên sẽ kết thúc với CARD_REMOVED.
  6. Trên điện thoại, tắt NFC và xác nhận nhận được NFC_DISABLED. Trong khi phiên đang mở, đưa ứng dụng xuống chạy nền và xác nhận phiên kết thúc với ACTIVITY_PAUSED.
Trang này còn thiếu điều gì?Đề xuất tính năng. Chúng tôi sẽ đính kèm trang này để đội ngũ nắm được bối cảnh.