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 15.0 or higher
- CocoaPods 1.14.3 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:
- Sign up/log in to the Particle dashboard.
- Create a new project or enter an existing project.
- Create a new iOS application, or skip this step if you already have one.
- 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.
- Regardless of specific functions, you'll need
ConnectCommon
andParticleConnect
, alongsideAuthCoreAdapter
. Optionally, if you'd like to use Particle Auth instead, you can useParticleAuthAdapter
. - If you'd like to support importing (or generating fresh) wallets on EVM chains, you'll need
ConnectEVMAdapter
.- To do the same on Solana, you can use
ConnectSolanaAdapter
.
- To do the same on Solana, you can use
- To support connection with Phantom (on Solana), you'll need to use
ConnectPhantomAdapter
- For general WalletConnect-based connection, you can use
ConnectWalletConnectAdapter
:
pod 'ConnectCommon'
pod 'ParticleConnect'
pod 'ConnectWalletConnectAdapter'
pod 'ConnectEVMAdapter'
pod 'ConnectSolanaAdapter'
pod 'ConnectPhantomAdapter'
pod 'AuthCoreAdapter'
# Alternatively, to use ParticleAuthService (deprecated)
# 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 AuthCoreAdapter
// Alternatively, to use ParticleAuthService (deprecated)
// import ParticleAuthAdapter
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 (testnet). Within this example, it's.ethereum
(alternatively it could be.ethereumGoerli
, and so on.)dAppData
, metadata outlining the description of your application. This should be an instance ofDAppMetadata
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 asParticleAuthAdapter
,WalletConnectAdapter
, etc.)
An example of initialization can be found below.
var adapters: [ConnectAdapter] = [
AuthCoreAdaper(),
WalletConnectAdapter(),
MetaMaskConnectAdapter(),
PhantomConnectAdapter(),
EVMConnectAdapter(),
SolanaConnectAdapter(),
ImtokenConnectAdapter(),
BitkeepConnectAdapter(),
RainbowConnectAdapter(),
TrustConnectAdapter(),
ZerionConnectAdapter(),
MathConnectAdapter(),
Inch1ConnectAdapter(),
ZengoConnectAdapter(),
AlphaConnectAdapter(),
OKXConnectAdapter(),
// Alternativell, to use ParticleAuthService (deprecated)
// ParticleAuthAdapter()
]
ParticleConnect.initialize(env: .debug,
chainInfo: .ethereum,
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, .ethereumGoerli])
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 andchainType
).ParticleConnect.getAdapters
, retrieves adapters bychainType
(evm
orsolana
).ParticleConnect.getAdapterByAddress
, returns all adapters associated with a specific (user) public address.
let adapter = ParticleConnect.getAllAdapters()
.filter { $0.walletType == .authCore }.first
let adapters = ParticleConnect.getAdapters(chainType: .evm)
let adapters = 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 smartAccounts = try await adapter.getSmartAccounts()
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 ParticleNetwork.setChainInfo
, passing in a chainInfo
object, as shown in the example below.
WalletType.authCore
comes from Particle Auth Core and WalletType.particle
comes from Particle Auth Service (deprecated), both of them support evm and solana, if you need switch chain for there two account, and have switch from solana to evm or evm to solana, you can call (adapter as! ParticleAdapter).switchChain
.
ParticleNetwork.setChainInfo(.ethereum)
// switch chain asynchronous from evm to solana or solana to evm.
// make sure the adapter.walletType is authCore or particle.
let result = try await (adapter as! ParticleAdapter).switchChain(.ethereum).value
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)
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://"))
}
}