iOS (Swift)

Leveraging Particle Connect within iOS applications.

Particle Connect for iOS

The utilization of Particle Connect within iOS applications enables simultaneous Web2-like onboarding and Web3-native onboarding, acting as a sort of "SSO for Web3," aggregating multiple onboarding paths within a single custom, in-house connection modal. These paths include Web3 accounts (social logins), Web3 wallets (through WalletConnect and Solana's wallet-adapter), and private key imports. Thus, Particle Connect on iOS aims to facilitate a universally accessible entry into mobile dApps.

Leveraging Particle Connect within your iOS application is relatively straightforward, as outlined below.

Repository

Before diving into the configuration and utilization process, if you'd like to look at our demo repository for an initial understanding of the underlying architecture and implementation flow, the official Particle Connect iOS SDK repository can be found here.

🚧

It is strongly discouraged to use a private key or the mnemonic import/generate function. If you use it, you need to secure the data yourself, as Particle keeps no relationship with the imported/generated mnemonic or private key.

Getting Started

Prerequisites

The setup process on iOS can be somewhat lengthy, so to ensure you don't run into any compatibility issues and can get started as soon as possible, you'll need to confirm that your project meets several different prerequisites:

  • Xcode 14.1 or higher
  • CocoaPods 1.11.0 or higher
  • iOS 14 or higher

Setting up the Particle dashboard

Additionally, before diving in, you'll need to retrieve three universally required values from the Particle dashboard: your projectId, clientKey, and appId. These directly link your project (instance of Particle Connect) with the Particle dashboard to enable customization, analytics, tracking, etc. To find these values, follow the below process:

  1. Sign up/log in to the Particle dashboard.

  1. Create a new project or enter an existing project.

  1. Create a new iOS application, or skip this step if you already have one.

  1. Retrieve the project ID (projectId), the client key (clientKey), and the application ID (appId).

‎‎

‎‎

‎‎

‎‎

Dependencies

Particle Connect requires numerous dependencies you must declare within your Podfile. To do this, you'll need to start by ensuring that you have a Podfile made. This is where you'll determine the pods (packages) to be used within your application.

If you don't have a Podfile made, head over to the root of your project directly and run the following:

pod init

Then, within your Podfile, you'll need to throw in a few different dependencies, the specifics of which depend on the functions of Particle Connect that you'd like to use.

  1. Regardless of specific functions, you'll need ConnectCommon and ParticleConnect, alongside ParticleAuthAdapter. Optionally, if you'd like to use Particle Auth Core instead, you can use AuthCoreAdapter.
  2. If you'd like to support importing (or generating fresh) wallets on EVM chains, you'll need ConnectEVMAdapter.
    1. To do the same on Solana, you can use ConnectSolanaAdapter.
  3. To support connection with Phantom (on Solana), you'll need to use ConnectPhantomAdapter
  4. For general WalletConnect-based connection, you can use ConnectWalletConnectAdapter:
pod 'ConnectCommon'
pod 'ParticleConnect'
pod 'ConnectWalletConnectAdapter'
pod 'ConnectEVMAdapter' 
pod 'ConnectSolanaAdapter'
pod 'ConnectPhantomAdapter'
pod 'ParticleAuthAdapter' 

With this set, you can simply run the following command to ensure changes are saved:

pod install --repo-update
open your-project.xcworkspace

Before continuing, paste the following code into your Podfile, if not already present; this is required for utilizing Particle Connect. You'll encounter issues otherwise.

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
    config.build_settings['BUILD_LIBRARY_FOR_DISTRIBUTION'] = 'YES'
      end
    end
end

Configuration

Now that you've retrieved your projectId, clientKey, and appId from the Particle dashboard, and also set up your Podfile, it's time to move onto the core configuration of your project before diving into examples of utilization. To begin this process, you'll need to configure a ParticleNetwork-Info.plist file.

Since it's ikely not already present, head to the root of your project and make a blank file, ParticleNetwork-Info.plist

Within this file, you must enter the authentication credentials retrieved from the Particle dashboard. You can do this through the structure below:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>PROJECT_UUID</key>
	<string>YOUR_PROJECT_UUID</string>
	<key>PROJECT_CLIENT_KEY</key>
	<string>YOUR_PROJECT_CLIENT_KEY</string>
	<key>PROJECT_APP_UUID</key>
	<string>YOUR_PROJECT_APP_UUID</string>
</dict>
</plist>

Additionally, head over to your UIApplicationDelegate. Here, you'll need to import the various libraries previously declared within your Podfile at the top of the file. An example of this can be seen below:

import ConnectCommon
import ConnectEVMAdapter
import ConnectPhantomAdapter
import ConnectSolanaAdapter
import ConnectWalletConnectAdapter
import ParticleNetworkBase
import ParticleConnect
import ParticleAuthAdapter
// Alternatively..
// import AuthCoreAdapter

Initialization

Now that these are set, you'll need to initialize Particle Connect itself (within UIApplicationDelegate). Particle Connect won't function until initialization is complete (and valid). Essentially, initialization involves the declaration of specific imported adapters, alongside the chain to be used and application metadata.

Specifically, ParticleConnect.initialize takes the following parameters:

  • env, the environment (production, debug, etc.) in which Particle Connect will be initialized (in this example, it's set to .debug).
  • chainInfo, the primary chain to be used within your application. This should be dictated by an overall blockchain, followed by a specific network (mainnet, testnet). Within this example, it's .ethereum(.mainnet)(alternatively it could be .polygon(.mainnet), and so on.)
  • dAppData, metadata outlining the description of your application. This should be an instance of DAppMetadata with the following parameters:
    • name, the name of your project.
    • icon, a URL linking to the icon of your project; this should be 512x512, ideally.
    • url, the URL of your project's website.
  • Finally, within the body of initialize, you'll need to pass in a list of adapters to be offered within the connection modal. This can be defined as a simple array containing various imported adapters (such as ParticleAuthAdapter, WalletConnectAdapter, etc.)

An example of initialization can be found below.

var adapters: [
       ParticleAuthAdapter(),
       WalletConnectAdapter(),
       MetaMaskConnectAdapter(),
       PhantomConnectAdapter(),
       EVMConnectAdapter(),
       SolanaConnectAdapter(),
       ImtokenConnectAdapter(),
       BitkeepConnectAdapter(),
       RainbowConnectAdapter(),
       TrustConnectAdapter(),
       // Alternatively to ParticleAuthAdapter..
       // AuthCoreAdaper()
       ]
         
let moreAdapterClasses: [WalletConnectAdapter.Type] =
     [ZerionConnectAdapter.self,
      MathConnectAdapter.self,
      OmniConnectAdapter.self,
      Inch1ConnectAdapter.self,
      ZengoConnectAdapter.self,
      AlphaConnectAdapter.self,
      OKXConnectAdapter.self]

adapters.append(contentsOf: moreAdapterClasses.map {
     $0.init()
})
 
ParticleConnect.initialize(env: .debug,  
 chainInfo: .ethereum(.mainnet),  
 dAppData: DAppMetaData(name: "Particle Connect",  
 icon: URL(string: "https://connect.particle.network/icons/512.png")!,  
 url: URL(string: "https://connect.particle.network")!)) {
      adapters
}

Scheme Configuration

The final step in setting up your iOS project is the configuration of a scheme URL and LSAApplicationQueriesSchemes within your application. Beginning with the scheme URL, add the following to your dApp's application(\_:open:options:) method:

return ParticleConnect.handleUrl(url)

From there, you'll need to configure the scheme URL itself; select your application target within the "Info" section, then add an additional URL type, and finally include the scheme. This scheme should be pn added to your appId.

For example, if your appId is 63bfa427-cf5f-4742-9ff1-e8f5a1b9828f, then the scheme URL would be pn63bfa427-cf5f-4742-9ff1-e8f5a1b9828f --simply adding pn to the beginning of the appId.

With the scheme URL configured, you'll also need to head into info.plist and add LSApplicationQueriesSchemes according to your specific utilization of Web3 wallets (not Particle Auth, but independent wallet adapters). An example of this can be seen below (each of these is optional and dependent upon your given implementation)

<key>LSApplicationQueriesSchemes</key>
<array>
	<string>metamask</string>
	<string>phantom</string>
	<string>bitkeep</string>
	<string>imtokenv2</string>
	<string>rainbow</string>
	<string>trust</string>
	
	<string>zerion</string>
        <string>mathwallet</string>
        <string>1inch</string>
        <string>awallet</string>
        <string>okex</string>
</array>

Examples of Utilization

Utilization of WalletConnect V2 *

If you're planning on using WalletConnect V2 as your primary method of connection for traditional Web3 wallets, you'll need to use the setWalletConnectV2ProjectId method on ParticleConnect, passing in your WalletConnect project ID retrieved from the WalletConnect dashboard. Additionally, you can set specific chains to be used within WalletConnect (chainInfo structures) with setWalletConnectV2SupportChainInfos. An example of this can be found below.

ParticleConnect.setWalletConnectV2ProjectId("WalletConnect V2 Project ID")

ParticleConnect.setWalletConnectV2SupportChainInfos([.ethereum(.mainnet), .ethereum(.goerli)])

Switch Chain

Oftentimes, the primary chain initially set through initialize won't be the only chain used within your application; thus, to account for this and change the primary chain post-initialization, you can call ParticleConnect.setChain, passing in a chainInfo object, as shown in the example below.

ParticleConnect.setChain(chainInfo: .ethereum(.mainnet))

Get all Wallet Adapter *

To retrieve all wallet adapters either in total, by chainType (evm or solana), or by address, you'll need to call one of the following methods:

  • ParticleConnect.getAllAdapters, returns all adapters (for every address and chainType).
  • ParticleConnect.getAdapters, retrieves adapters by chainType (evm or solana).
  • ParticleConnect.getAdapterByAddress, returns all adapters associated with a specific (user) public address.
let adapter = ParticleConnect.getAllAdapters()
            .filter { $0.walletType == .particle }.first

let adapters = ParticleConnect.getAdapters(chainType: .evm)

let adapter = ParticleConnect.getAdapterByAddress(publicAddress: address)

Get Adapter Accounts *

adapter.getAccounts returns a list of accounts associated/connected with a specific adapter. adapter is an instance of a given wallet adapter, such as ParticleAuthAdapter. Each adapter has a getAccounts method by default. Alternatively, to retrieve smart accounts associated with a given adapter, you'll need to call adapter.getSmartAccounts. E.g.:

let accounts = adapter.getAccounts()

// Alternatively
let accounts = adapter.getSmartAccounts()

Connect Wallet

Likely one of the most important methods, specific adapters can initiate connection (through an initialization in a variable such as adapter) with the connect method. Depending on the specific adapter, this method will throw a modal facilitating connection.

For EVMConnectAdapter and SolanaConnectAdapter, this will generate a fresh account (public and private key pair). Otherwise, the specific connection flow will be dependent upon the adapter in which connect is called. E.g.:

adapter.connect().subscribe { [weak self] result in
  guard let self = self else { return }
  switch result {
      case .failure(let error):
      print(error)
      case .success(let account):
      print(account)
  }.disposed(by: bag)

Disconnect Wallet

Alternatively, if a user has already connected their wallet, you'll be able to use disconnect as the primary method to disconnect a user within an active session. This will be relatively universal across adapters. E.g.:

adapter.disconnect(address).subscribe { [weak self] result in
  guard let self = self else { return }
  switch result {
      case .failure(let error):
      print(error)
      case .success(let success):
      print(success)
  }.disposed(by: bag)

Is Connected

Another quite important method is isConnected, returning a Boolean based on whether or not a given session (user) is currently connected to a wallet (through the specific adapter instance). adapter.isConnected takes a given public address and returns true or false depending on it's connection status. E.g.:

let result = adapter.isConnected(publicAddress: address)

Reconnect If Needed

Some adapters may require utilization of reconnectIfNeeded if they open external applications or redirects to initiate connection and login, good examples of this include WalletConnectAdapter, MetaMaskConnectAdapter, RainConnectAdapter, etc. adapter.reconnectIfNeeded takes one parameter, publicAddress, of the account in question that needs to be reconnected upon re-entry. E.g.:


adapter.reconnectIfNeeded(publicAddress: address) 

Import Wallet

If you're using EVMConnectAdapter or SolanaConnectAdapter, you can import wallets through either a seed phrase or private key. These methods will associate an account instance derived from these keys, allowing utilization within your application. These can be achieved through either importWalletFromPrivateKey for importing a private key, or importWalletFromMnemonic for importing a mnemonic (seed phrase).

Additionally, you can export one of these wallets with adapter.exportWalletPrivateKey, passing in the address (of the EVMConnectAdapter or SolanaConnectAdapter imported/generated wallet) that you'd like to export.

📘

This is not the import/export of wallets within Particle Auth; Particle Network's MPC-TSS accounts do not support importing or exporting private keys.

This is specific to custom account connection with Particle Connect.

adapter.importWalletFromPrivateKey(privateKey).subscribe { [weak self] result in
  guard let self = self else { return }
  switch result {
      case .failure(let error):
      print(error)
      case .success(let account):
      print(account)
  }
}.disposed(by: bag)

adapter.importWalletFromMnemonic(mnemonic).subscribe { [weak self] result in
  guard let self = self else { return }
  switch result {
      case .failure(let error):
      print(error)
      case .success(let account):
      print(account)
  }
}.disposed(by: bag)

// Exportation
adapter.exportWalletPrivateKey(publicAddress: address).subscribe { [weak self] result in
  guard let self = self else { return }
  switch result {
      case .failure(let error):
      print(error)
      case .success(let privateKey):
      print(privateKey)
  }
}.disposed(by: bag)

Sign and Send Transaction

To initiate a transaction for signature and ultimately settlement on-chain, you'll need to use adapter.signAndSendTransaction (for both EVM & Solana)- this acts as the primary method of sending transactions through Particle Connect. Calling this method will return a popup to the adapter in question, prompting a signature from the end user. Once confirmation is given, the transaction will be immediately sent to the network.

adapter.signAndSendTransaction takes the following parameters:

  • address, the user address to target for transaction initiation.
  • transaction, a stringified transaction object (containing fields like 'to', 'from', 'value', 'data', etc.)

E.g.:

adapter.signAndSendTransaction(publicAddress: address, transaction: transaction).subscribe { [weak self] result in
  guard let self = self else { return }
  switch result {
      case .failure(let error):
      print(error)
      case .success(let signature):
      print(signature)
  }
}.disposed(by: bag)

Sign Transaction

adapter.signTransaction is a Solana-specific method for signing a transaction without pushing it to the network. Specifically, this method requires the following parameters:

  • address, the user address to target for signature initiation.
  • transaction, a base58 string representing a transaction object.

This will return a popup requesting confirmation to either SolanaConnectAdapter or PhantomConnectAdapter depending on which you've defined as adapter (or, alternatively, a custom adapter you've set up). E.g.:

adapter.signTransaction(publicAddress: address, transaction: transaction).subscribe { [weak self] result in
  guard let self = self else { return }
  switch result {
      case .failure(let error):
      print(error)
      case .success(let signed):
      print(signed)
  }
}.disposed(by: bag)

// Alternatively, the plural form of this method, takes a list of transactions
adapter.signAllTransactions(publicAddress: address, transactions: transactions).subscribe { [weak self] result in
  guard let self = self else { return }
  switch result {
      case .failure(let error):
      print(error)
      case .success(let signed):
      print(signed)
  }
}.disposed(by: bag)

Sign Message

To sign a basic UTF-8 message (string), you can use adapter.signMessage to prompt the user for a signature, alongside the message in question. adapter.signMessage takes the following parameters:

  • address, the user address to target for signature initiation.
  • message, the message you'd like the user to sign; for EVM, this should be a hex-encoded string (prefixed by '0x'), while for Solana, you can pass in a standard string (no encoding needed).

E.g.:

adapter.signMessage(publicAddress: address, message: message).subscribe { [weak self] result in
  guard let self = self else { return }
  switch result {
      case .failure(let error):
      print(error)
      case .success(let signed):
      print(signed)
  }
}.disposed(by: bag)

Sign Typed Data

As an alternative to signMessage, for EVM chains, you can request that a user signs typed (structured) data rather than purely a raw string. To do this, you can use adapter.signTypedData (equivalent to eth_signTypedData V4). This takes the following parameters:

  • address, the user address to target for signature initiation.
  • data, the typed data to be signed; see the Web (JavaScript/TypeScript) page for additional guidance.

E.g.:

connectAdapter.signTypedData(publicAddress: address, data: data).subscribe { [weak self] result in
  guard let self = self else { return }
  switch result {
      case .failure(let error):
      print(error)
      case .success(let signed):
      print(signed)
  }
}.disposed(by: bag)

Custom WalletConnect Adapter

There may be cases where the existing selection of adapters doesn't include a wallet you'd like to be featured within Particle Connect. In this situation, creating a custom adapter (using WalletConnectAdapter as the parent class) is quite simple. Doing so will require a structure similar to the example below, in which a custom Coin98 adapter is made by overriding walletType with a custom AdapterInfo instance.

public class Coin98WalletConnectAdapter: WalletConnectAdapter {
  public override var walletType: WalletType {
      return WalletType.custom(info: AdapterInfo.init(name: "Coin98",
                   url: "https://coin98.com/",
                   icon: "https://registry.walletconnect.com/v2/logo/md/dee547be-936a-4c92-9e3f-7a2350a62e00",
                   redirectUrlHost: "coin98",
                   supportChains: [.evm],
                   deepLink: "coin98://", // Requires either universal link or scheme
                   scheme: "coin98://"))
  }
}