Skip to content

Setting up the Self SDK

The Self SDK provides powerful tools for digital identity management and secure communication.

This guide will walk you through the process of setting up your Self account and creating an inbox, which are essential steps for using the SDK effectively.

Setting up your Self account

1. Import the necessary packages

1
2
3
4
5
import (
    "github.com/joinself/self-go-sdk/account"
    "github.com/joinself/self-go-sdk/event"
    "github.com/charmbracelet/log"
)
1
2
3
4
5
6
import com.joinself.selfsdk.kmp.account.Account
import com.joinself.selfsdk.kmp.account.LogLevel
import com.joinself.selfsdk.kmp.account.Target
import com.joinself.selfsdk.kmp.error.SelfStatus
import com.joinself.selfsdk.kmp.event.*
import com.joinself.selfsdk.kmp.message.*
import android.content.Context
import com.joinself.common.Environment
import com.joinself.common.Environment
import com.joinself.sdk.models.Account
import com.joinself.sdk.models.ChatMessage
import com.joinself.sdk.models.CredentialRequest
import com.joinself.sdk.models.CredentialResponse
import com.joinself.sdk.models.Receipt
import com.joinself.sdk.models.SigningRequest
import com.joinself.sdk.models.SigningResponse
import com.joinself.sdk.models.VerificationRequest
import com.joinself.sdk.models.VerificationResponse
import com.joinself.sdk.ui.addLivenessCheckRoute
import com.joinself.ui.theme.SelfModifier

2. Configure your account settings

Android and iOS SDK require the integrity check be configured in the Admin app

1
2
3
4
5
6
7
cfg := &account.Config{
    StorageKey: make([]byte, 32), // Replace with a securely generated key
    StoragePath: "./storage", // Local storage path for account state
    Environment: account.TargetSandbox, // Choose between Develop and Sandbox
    LogLevel: account.LogWarn, // Set log level (Error, Warn, Info, Debug, Trace)
    Callbacks: account.Callbacks{},
}
1
2
3
4
5
6
7
8
9
val account = Account()
val status = account.configure(
    storagePath = ":memory:",
    storageKey = ByteArray(32),
    rpcEndpoint = Target.SANDBOX.rpcEndpoint(),
    objectEndpoint = Target.SANDBOX.objectEndpoint(),
    messageEndpoint = Target.SANDBOX.messageEndpoint(),
    logLevel = LogLevel.INFO,
)
SelfSDK.initialize(androidContext, 
    applicationAddress = "<document address from admin app>", // required for non-sandbox environment        
    log = { Log.d("Self", it) }
)

val storagePath = File(androidContext.filesDir.absolutePath + "/account1")
if (!storagePath.exists()) storagePath.mkdirs()

val account = Account.Builder()
    .setContext(androidContext)
    .setEnvironment(Environment)
    .setSandbox(true)
    .setStoragePath(storagePath.absolutePath)
    .build()
1
2
3
4
5
6
let account = Account.Builder()
        .withEnvironment(Environment.preview)
        .withSandbox(false) // if true -> production
        .withGroupId("YOUR_APP_GROUP_ID") // ex: com.example.app.your_app_group
        .withStoragePath("ADD_YOUR_STORAGE_PATH_HERE" + "/account1")
        .build()

3. Set up event callbacks

cfg.Callbacks = account.Callbacks{
    OnConnect: func(selfAccount *account.Account) {
        log.Info("messaging socket connected")
    },

    OnDisconnect: func(selfAccount *account.Account, err error) {
        if err != nil {
            log.Warn("messaging socket disconnected", "error", err)
        } else {
            log.Info("messaging socket disconnected")
        }
    },

    OnWelcome: func(selfAccount *account.Account, wlc *event.Welcome) {
        groupAddress, err := selfAccount.ConnectionAccept(
            wlc.ToAddress(),
            wlc,
        )
        if err != nil {
            log.Warn("failed to accept connection to encrypted group", "error", err)
            return
        }

        log.Info(
            "accepted connection encrypted group",
            "from", wlc.FromAddress().String(),
            "group", groupAddress.String(),
        )
    },

    OnMessage: func(selfAccount *account.Account, msg *event.Message) {
        switch event.ContentType(msg) {
        case event.TypeChat:
            // Handle chat messages
        case event.TypeDiscoveryResponse:
            // Handle discovery responses
        }
    },
}
account.configure(
    // ... other config options ...
    onConnect = {
        println("KMP connected")
    },
    onDisconnect = { reason: SelfStatus? ->
        println("KMP disconnected")
    },
    onWelcome = { welcome: Welcome ->
        account.connectionAccept(asAddress = welcome.toAddress(), welcome = welcome) { status: SelfStatus, groupAddress: PublicKey ->
            println("accepted connection encrypted group status:${status.code()} - from:${welcome.fromAddress().encodeHex()} - group:${groupAddress.encodeHex()}")
        }
    },
    onMessage = { message: Message ->
        val content = message.content()
        val contentType = content.contentType()
        when (contentType) {
            ContentType.CHAT -> {
                // Handle chat messages
            }
            ContentType.DISCOVERY_RESPONSE -> {
                // Handle discovery responses
            }
        }
    }
)
account.setOnInfoRequest { key ->
    println("info request $key")
}
account.setOnInfoResponse { fromAddress, data ->
}

account.setOnStatusListener { status ->
    println("onStatus $status")
}
account.setOnRelayConnectListener {
    println("onRelay connectted")
}

account.setOnMessageListener { msg ->
    when (msg) {
        is ChatMessage -> println("chat messages")
        is Receipt -> println("receipt message")
    }
}

account.setOnRequestListener { msg ->
    when (msg) {
        is CredentialRequest -> println("credential request")
        is VerificationRequest -> println("verification request")
        is SigningRequest -> println("signing request")
    }
}
account.setOnResponseListener { msg ->
    when (msg) {
        is CredentialResponse -> println("credential response")
        is VerificationResponse -> println("verification response")
        is SigningResponse -> println("signing response")
    }
}
account.setOnInfoRequest { (key: String) in
    print("setOnInfoRequest: \(key)")
}

account.setOnInfoResponse { (address: String, data: [String: Any]) in
    print("setOnInfoResponse: \(address)/\(data)")
}

account.setOnStatusListener { status in
    print("init account status:\(status)")
}

account.setOnRelayConnectListener {
    print("onRelayConnect connected.")
}

account.setOnMessageListener { message in
    print("Message received: \(message.id())")
    switch message {
    case is ChatMessage:
        let chatMessage = message as! ChatMessage

    case is Receipt:
        let receipt = message as! Receipt

    default:
        print("TODO: Handle For Message: \(message)")
        break
    }
}

account.setOnRequestListener { message in
    print("setOnRequestListener: \(message)")
    switch message {
    case is CredentialRequest:
        let credentialRequest = message as! CredentialRequest

    case is VerificationRequest:
        let verificationRequest = message as! VerificationRequest

    case is SigningRequest:
        let signingRequest = message as! SigningRequest

    default:
        log.debug("TODO: Handle For Request: \(message)")
        break
    }
}

account.setOnResponseListener { message in
    print("setOnResponseListener: \(message)")
    switch message {
    case is CredentialResponse:
        let response = message as! CredentialResponse

    default:
        print("TODO: Handle For Response: \(message)")
        break;
    }
}

4. Initialize the Self account

1
2
3
4
selfAccount, err := account.New(cfg)
if err != nil {
    log.Fatal("failed to initialize account", "error", err)
}
// Account is already initialized in the configuration step for Kotlin
println("status: ${status.code()}")
1
2
3
4
// Account is already initialized in the configuration step
account.setOnStatusListener { status ->
    println("account status $status") // 0 is connected
}
1
2
3
4
// Account is already initialized in the configuration step
account.setOnStatusListener { status in
    print("init account status:\(status) -> \(self.account.generateAddress())")
}

Setting up your inbox

1
2
3
4
5
6
inboxAddress, err := selfAccount.InboxOpen()
if err != nil {
    log.Fatal("failed to open account inbox", "error", err)
}

log.Info("initialized account success")
val inboxAddress = runBlocking {
    suspendCoroutine { continuation ->
        account.inboxOpen { status: SelfStatus, address: PublicKey ->
            println("inbox open status:${status.code()} - address:${address.encodeHex()}")
            if (status.success()) {
                continuation.resumeWith(Result.success(address))
            } else {
                continuation.resumeWith(Result.success(null))
            }
        }
    }
}
You dont need to open the inbox on Android.
Its managed internally by the Self Android SDK.
You don’t need to open the inbox on iOS.
It’s managed internally by the Self iOS SDK.

Note: Creating an inbox is only necessary for the current version of the SDK. This requirement will be removed in future updates to streamline the setup process.

By following these steps, you'll establish a secure foundation for your Self-enabled application. Let's dive into each step in detail.

Register an account in Android and iOS app

To use any functionality provided by the Android or iOS SDK, users are required to register an account beforehand.

The SDK includes built-in Kotlin Compose for Android and SwiftUI for iOS UI components to facilitate this process, making it easy to integrate account registration into your app's user interface.

See details in these examples

Below are code examples in a simple MainActivity on how to integrate the registration flow in the app.

import com.joinself.sdk.ui.addLivenessCheckRoute
import com.joinself.ui.theme.SelfModifier
import com.joinself.sdk.SelfSDK
import com.joinself.sdk.models.Account

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()

        SelfSDK.initialize(
            applicationContext,                
            log = { Log.d("Self", it) }
        )

        val storagePath = File(applicationContext.filesDir.absolutePath + "/account1")
        if (!storagePath.exists()) storagePath.mkdirs()

        val account = Account.Builder()
            .setContext(applicationContext)
            .setEnvironment(Environment)
            .setSandbox(true)
            .setStoragePath(storagePath.absolutePath)
            .build()        

        account.setOnStatusListener { status ->
            println("onStatus $status")
        }

        setContent {
            val coroutineScope = rememberCoroutineScope()
            val navController = rememberNavController()
            val selfModifier = SelfModifier.sdk()

            var isRegistered by remember { mutableStateOf(account.registered()) }

            NavHost(navController = navController,
                startDestination = "main",
                modifier = Modifier.systemBarsPadding(),
                enterTransition = { EnterTransition.None },
                exitTransition = { ExitTransition.None }
            ) {
                composable("main") {
                    Column(
                        verticalArrangement = Arrangement.spacedBy(10.dp),
                        horizontalAlignment = Alignment.CenterHorizontally,
                        modifier = Modifier.padding(start = 8.dp, end = 8.dp).fillMaxWidth()
                    ) {
                        Text(modifier = Modifier.padding(top = 40.dp), text = "Registered:${isRegistered}")
                        Button(
                            modifier = Modifier.padding(top = 20.dp),
                            onClick = {
                                navController.navigate("livenessRoute")
                            },
                            enabled = !isRegistered
                        ) {
                            Text(text = "Create Account")
                        }
                    }
                }

                addLivenessCheckRoute(navController, route = "livenessRoute", selfModifier = selfModifier,
                    account = {
                        account
                    },
                    withCredential = true,
                    onFinish = { selfie, credentials ->
                        if (!account.registered()) {
                            coroutineScope.launch(Dispatchers.IO) {
                                try {
                                    // use the credentials to register an account
                                    if (selfie.isNotEmpty() && credentials.isNotEmpty()) {
                                        val success = account.register(selfieImage = selfie, credentials = credentials)
                                        if (success) {
                                            isRegistered = true
                                            withContext(Dispatchers.Main) {
                                                Toast.makeText(applicationContext, "Register account successfully", Toast.LENGTH_LONG).show()
                                            }
                                        }
                                    }
                                } catch (_: InvalidCredentialException) { }
                            }
                        }
                        coroutineScope.launch(Dispatchers.Main) {
                            navController.popBackStack("livenessRoute", true)
                        }
                    }
                )
            }
        }
    }
}

We setup an onboarding viewmodel to hold the account and do the liveness check and then register an account. The following codes to do that.

final class OnboardingViewModel: ObservableObject {
    @Published var isOnboardingCompleted: Bool = false

    let account = Account.Builder()
        .withEnvironment(Environment.preview)
        .withSandbox(true) // if true -> production
        .withGroupId("") // ex: com.example.app.your_app_group
        .withStoragePath(FileManager.storagePath)
        .build()

    func lfcFlow() {
        SelfSDK.showLiveness(account: account, showIntroduction: true, autoDismiss: true, onResult: { selfieImageData, credentials, error in
            print("showLivenessCheck credentials: \(credentials)")
            self.register(selfieImageData: selfieImageData, credentials: credentials)
        })
    }

    private func register(selfieImageData: Data, credentials: [Credential], completion: ((Bool) -> Void)? = nil) {
        Task(priority: .background) {
            do {
                let success = try await account.register(selfieImage: selfieImageData, credentials: credentials)
                print("Register account: \(success)")
            } catch let error {
                print("Register Error: \(error)")
            }
        }
    }
}

extension FileManager {
    static var storagePath: String {
        let storagePath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("account1")
        createFolderAtURL(url: storagePath) // create folder if needed
        print("StoragePath: \(storagePath)")
        return storagePath.path()
    }

    static func createFolderAtURL(url: URL) {
        if FileManager.default.fileExists(atPath: url.path()) {
            print("Folder already exists: \(url)")

        } else {
            // Create the folder
            do {
                try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
                print("Folder created successfully: \(url)")
            } catch {
                print("Error creating folder: \(error.localizedDescription)")
            }
        }
    }
}

Then in the main view

import SwiftUI
import self_ios_sdk

struct ContentView: View {

    @ObservedObject var viewModel: OnboardingViewModel = OnboardingViewModel()
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, SelfSDK!")

            Button {
                viewModel.lfcFlow()
            } label: {
                Text("LFC Flow")
            }

        }
        .padding()
    }
}