We believe in making
the Internet a safer
and more secure place. For everyone.
Open-source and Source-available 2FAS apps are essential to our mission of revolutionizing the security standards of protecting online accounts.
import UIKit
import Common
final class TokensViewController: UIViewController {
var presenter: TokensPresenter!
var addButton: UIBarButtonItem? {
navigationItem.rightBarButtonItem
}
private(set) var gridView: GridView!
private(set) var gridLayout: UICollectionViewFlowLayout!
private(set) var dataSource: UICollectionViewDiffableDataSource<GridSection, GridCell>!
let headerHeight: CGFloat = 50
let emptySearchScreenView = GridViewEmptySearchScreen()
let emptyListScreenView = GridViewEmptyListScreen()
private var configuredWidth: CGFloat = 0
var searchBarAdded = false
let searchController = CommonSearchController()
override func loadView() {
gridLayout = UICollectionViewFlowLayout()
gridView = GridView(frame: .zero, collectionViewLayout: gridLayout)
self.view = gridView
gridView.configure()
}
override func viewDidLoad() {
super.viewDidLoad()
setupView()
setupEmptyScreensLayout()
setupEmptyScreensEvents()
setupDelegates()
setupDataSource()
setupDragAndDrop()
setupNotificationsListeners()
}
// MARK: - App events
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
configureLayout()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
presenter.viewWillAppear()
startSafeAreaKeyboardAdjustment()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
stopSafeAreaKeyboardAdjustment()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
private extension TokensViewController {
func setupView() {
extendedLayoutIncludesOpaqueBars = true
view.backgroundColor = Theme.Colors.Fill.background
title = T.Commons.tokens
accessibilityTraits = .header
}
func setupDelegates() {
searchController.searchBarDelegate = self
gridView.delegate = self
}
func setupDataSource() {
dataSource = UICollectionViewDiffableDataSource(
collectionView: gridView,
cellProvider: { collectionView, indexPath, item in
if item.cellType == .serviceTOTP {
if collectionView.isEditing {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: GridViewEditItemCell.reuseIdentifier,
for: indexPath
) as? GridViewEditItemCell
cell?.update(
name: item.name,
additionalInfo: item.additionalInfo,
serviceTypeName: item.serviceTypeName,
iconType: item.iconType,
category: item.category,
canBeDragged: item.canBeDragged
)
return cell
} else {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: GridViewItemCell.reuseIdentifier,
for: indexPath
) as? GridViewItemCell
cell?.update(
name: item.name,
secret: item.secret,
serviceTypeName: item.serviceTypeName,
additionalInfo: item.additionalInfo,
iconType: item.iconType,
category: item.category,
useNextToken: item.useNextToken
)
return cell
}
} else if item.cellType == .serviceHOTP {
if collectionView.isEditing {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: GridViewEditItemCell.reuseIdentifier,
for: indexPath
) as? GridViewEditItemCell
cell?.update(
name: item.name,
additionalInfo: item.additionalInfo,
serviceTypeName: item.serviceTypeName,
iconType: item.iconType,
category: item.category,
canBeDragged: item.canBeDragged
)
return cell
} else {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: GridViewCounterItemCell.reuseIdentifier,
for: indexPath
) as? GridViewCounterItemCell
cell?.update(
name: item.name,
secret: item.secret,
serviceTypeName: item.serviceTypeName,
additionalInfo: item.additionalInfo,
iconType: item.iconType,
category: item.category
)
return cell
}
}
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: GridEmptyCollectionViewCell.reuseIdentifier,
for: indexPath
) as? GridEmptyCollectionViewCell
return cell
})
dataSource.supplementaryViewProvider = { [weak self] collectionView, kind, indexPath
-> UICollectionReusableView? in
let header = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: GridSectionHeader.reuseIdentifier,
for: indexPath
) as? GridSectionHeader
header?.setIsEditing(collectionView.isEditing)
header?.dataSource = self
if let data = self?.dataSource.snapshot().sectionIdentifiers[indexPath.section] {
header?.setConfiguration(data)
}
return header
}
}
func setupDragAndDrop() {
gridView.dragDelegate = self
gridView.dropDelegate = self
gridView.dragInteractionEnabled = presenter.enableDragAndDropOnStart
}
func setupEmptyScreensLayout() {
view.addSubview(emptySearchScreenView, with: [
emptySearchScreenView.leadingAnchor.constraint(equalTo: gridView.frameLayoutGuide.leadingAnchor),
emptySearchScreenView.trailingAnchor.constraint(equalTo: gridView.frameLayoutGuide.trailingAnchor),
emptySearchScreenView.topAnchor.constraint(equalTo: gridView.frameLayoutGuide.topAnchor),
emptySearchScreenView.bottomAnchor.constraint(equalTo: gridView.frameLayoutGuide.bottomAnchor)
])
emptySearchScreenView.isHidden = true
emptySearchScreenView.alpha = 0
view.addSubview(emptyListScreenView, with: [
emptyListScreenView.leadingAnchor.constraint(equalTo: gridView.frameLayoutGuide.leadingAnchor),
emptyListScreenView.trailingAnchor.constraint(equalTo: gridView.frameLayoutGuide.trailingAnchor),
emptyListScreenView.topAnchor.constraint(equalTo: gridView.safeTopAnchor),
emptyListScreenView.bottomAnchor.constraint(equalTo: gridView.safeBottomAnchor)
])
emptyListScreenView.isHidden = true
emptyListScreenView.alpha = 0
}
func configureLayout() {
guard let screenWidth = UIApplication.keyWindow?.bounds.size.width,
configuredWidth != screenWidth else { return }
configuredWidth = screenWidth
let cellHeight = Theme.Metrics.servicesCellHeight
let minimumCellWidth: CGFloat = Theme.Metrics.pageWidth
let itemsInRow = Int(screenWidth / minimumCellWidth)
let margin: CGFloat = 0
let marginsWidth = margin * CGFloat(itemsInRow - 1)
let screenWidthWithoutMargins = screenWidth - marginsWidth
let elementWidth = floor(screenWidthWithoutMargins / CGFloat(itemsInRow))
gridLayout.itemSize = CGSize(width: elementWidth, height: cellHeight)
gridLayout.minimumInteritemSpacing = margin
gridLayout.headerReferenceSize = CGSize(width: 100, height: headerHeight)
gridLayout.minimumLineSpacing = 0
}
func setupEmptyScreensEvents() {
emptyListScreenView.pairNewService = { [weak self] in self?.presenter.handleShowCamera() }
emptyListScreenView.import2FAS = { [weak self] in
AnalyticsLog(.onboardingBackupFile)
self?.presenter.handleImport2FAS()
}
emptyListScreenView.importGA = { [weak self] in
AnalyticsLog(.onboardingGA)
self?.presenter.handleImportGA()
}
emptyListScreenView.help = { [weak self] in self?.presenter.handleShowHelp() }
}
func setupNotificationsListeners() {
let center = NotificationCenter.default
center.addObserver(
self, selector: #selector(notificationServicesWereUpdated), name: .servicesWereUpdated, object: nil
)
center.addObserver(
self, selector: #selector(notificationSectionsWereUpdated), name: .sectionsWereUpdated, object: nil
)
center.addObserver(
self,
selector: #selector(notificationAppDidBecomeActive),
name: UIApplication.didBecomeActiveNotification,
object: nil
)
center.addObserver(
self,
selector: #selector(notificationAppDidBecomeInactive),
name: UIApplication.willResignActiveNotification,
object: nil
)
center.addObserver(
self,
selector: #selector(notificationAppDidBecomeInactive),
name: UIApplication.didEnterBackgroundNotification,
object: nil
)
}
}
import UIKit
import Common
final class TokensViewController: UIViewController {
var presenter: TokensPresenter!
var addButton: UIBarButtonItem? {
navigationItem.rightBarButtonItem
}
private(set) var gridView: GridView!
private(set) var gridLayout: UICollectionViewFlowLayout!
private(set) var dataSource: UICollectionViewDiffableDataSource<GridSection, GridCell>!
let headerHeight: CGFloat = 50
let emptySearchScreenView = GridViewEmptySearchScreen()
let emptyListScreenView = GridViewEmptyListScreen()
private var configuredWidth: CGFloat = 0
var searchBarAdded = false
let searchController = CommonSearchController()
override func loadView() {
gridLayout = UICollectionViewFlowLayout()
gridView = GridView(frame: .zero, collectionViewLayout: gridLayout)
self.view = gridView
gridView.configure()
}
override func viewDidLoad() {
super.viewDidLoad()
setupView()
setupEmptyScreensLayout()
setupEmptyScreensEvents()
setupDelegates()
setupDataSource()
setupDragAndDrop()
setupNotificationsListeners()
}
// MARK: - App events
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
configureLayout()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
presenter.viewWillAppear()
startSafeAreaKeyboardAdjustment()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
stopSafeAreaKeyboardAdjustment()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
private extension TokensViewController {
func setupView() {
extendedLayoutIncludesOpaqueBars = true
view.backgroundColor = Theme.Colors.Fill.background
title = T.Commons.tokens
accessibilityTraits = .header
}
func setupDelegates() {
searchController.searchBarDelegate = self
gridView.delegate = self
}
func setupDataSource() {
dataSource = UICollectionViewDiffableDataSource(
collectionView: gridView,
cellProvider: { collectionView, indexPath, item in
if item.cellType == .serviceTOTP {
if collectionView.isEditing {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: GridViewEditItemCell.reuseIdentifier,
for: indexPath
) as? GridViewEditItemCell
cell?.update(
name: item.name,
additionalInfo: item.additionalInfo,
serviceTypeName: item.serviceTypeName,
iconType: item.iconType,
category: item.category,
canBeDragged: item.canBeDragged
)
return cell
} else {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: GridViewItemCell.reuseIdentifier,
for: indexPath
) as? GridViewItemCell
cell?.update(
name: item.name,
secret: item.secret,
serviceTypeName: item.serviceTypeName,
additionalInfo: item.additionalInfo,
iconType: item.iconType,
category: item.category,
useNextToken: item.useNextToken
)
return cell
}
} else if item.cellType == .serviceHOTP {
if collectionView.isEditing {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: GridViewEditItemCell.reuseIdentifier,
for: indexPath
) as? GridViewEditItemCell
cell?.update(
name: item.name,
additionalInfo: item.additionalInfo,
serviceTypeName: item.serviceTypeName,
iconType: item.iconType,
category: item.category,
canBeDragged: item.canBeDragged
)
return cell
} else {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: GridViewCounterItemCell.reuseIdentifier,
for: indexPath
) as? GridViewCounterItemCell
cell?.update(
name: item.name,
secret: item.secret,
serviceTypeName: item.serviceTypeName,
additionalInfo: item.additionalInfo,
iconType: item.iconType,
category: item.category
)
return cell
}
}
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: GridEmptyCollectionViewCell.reuseIdentifier,
for: indexPath
) as? GridEmptyCollectionViewCell
return cell
})
dataSource.supplementaryViewProvider = { [weak self] collectionView, kind, indexPath
-> UICollectionReusableView? in
let header = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: GridSectionHeader.reuseIdentifier,
for: indexPath
) as? GridSectionHeader
header?.setIsEditing(collectionView.isEditing)
header?.dataSource = self
if let data = self?.dataSource.snapshot().sectionIdentifiers[indexPath.section] {
header?.setConfiguration(data)
}
return header
}
}
func setupDragAndDrop() {
gridView.dragDelegate = self
gridView.dropDelegate = self
gridView.dragInteractionEnabled = presenter.enableDragAndDropOnStart
}
func setupEmptyScreensLayout() {
view.addSubview(emptySearchScreenView, with: [
emptySearchScreenView.leadingAnchor.constraint(equalTo: gridView.frameLayoutGuide.leadingAnchor),
emptySearchScreenView.trailingAnchor.constraint(equalTo: gridView.frameLayoutGuide.trailingAnchor),
emptySearchScreenView.topAnchor.constraint(equalTo: gridView.frameLayoutGuide.topAnchor),
emptySearchScreenView.bottomAnchor.constraint(equalTo: gridView.frameLayoutGuide.bottomAnchor)
])
emptySearchScreenView.isHidden = true
emptySearchScreenView.alpha = 0
view.addSubview(emptyListScreenView, with: [
emptyListScreenView.leadingAnchor.constraint(equalTo: gridView.frameLayoutGuide.leadingAnchor),
emptyListScreenView.trailingAnchor.constraint(equalTo: gridView.frameLayoutGuide.trailingAnchor),
emptyListScreenView.topAnchor.constraint(equalTo: gridView.safeTopAnchor),
emptyListScreenView.bottomAnchor.constraint(equalTo: gridView.safeBottomAnchor)
])
emptyListScreenView.isHidden = true
emptyListScreenView.alpha = 0
}
func configureLayout() {
guard let screenWidth = UIApplication.keyWindow?.bounds.size.width,
configuredWidth != screenWidth else { return }
configuredWidth = screenWidth
let cellHeight = Theme.Metrics.servicesCellHeight
let minimumCellWidth: CGFloat = Theme.Metrics.pageWidth
let itemsInRow = Int(screenWidth / minimumCellWidth)
let margin: CGFloat = 0
let marginsWidth = margin * CGFloat(itemsInRow - 1)
let screenWidthWithoutMargins = screenWidth - marginsWidth
let elementWidth = floor(screenWidthWithoutMargins / CGFloat(itemsInRow))
gridLayout.itemSize = CGSize(width: elementWidth, height: cellHeight)
gridLayout.minimumInteritemSpacing = margin
gridLayout.headerReferenceSize = CGSize(width: 100, height: headerHeight)
gridLayout.minimumLineSpacing = 0
}
func setupEmptyScreensEvents() {
emptyListScreenView.pairNewService = { [weak self] in self?.presenter.handleShowCamera() }
emptyListScreenView.import2FAS = { [weak self] in
AnalyticsLog(.onboardingBackupFile)
self?.presenter.handleImport2FAS()
}
emptyListScreenView.importGA = { [weak self] in
AnalyticsLog(.onboardingGA)
self?.presenter.handleImportGA()
}
emptyListScreenView.help = { [weak self] in self?.presenter.handleShowHelp() }
}
func setupNotificationsListeners() {
let center = NotificationCenter.default
center.addObserver(
self, selector: #selector(notificationServicesWereUpdated), name: .servicesWereUpdated, object: nil
)
center.addObserver(
self, selector: #selector(notificationSectionsWereUpdated), name: .sectionsWereUpdated, object: nil
)
center.addObserver(
self,
selector: #selector(notificationAppDidBecomeActive),
name: UIApplication.didBecomeActiveNotification,
object: nil
)
center.addObserver(
self,
selector: #selector(notificationAppDidBecomeInactive),
name: UIApplication.willResignActiveNotification,
object: nil
)
center.addObserver(
self,
selector: #selector(notificationAppDidBecomeInactive),
name: UIApplication.didEnterBackgroundNotification,
object: nil
)
}
}
Mission-driven
Online security is critical to everyone's privacy, online and offline. Account hijacking leads to breaches of personal data, leaks of valuable and sensitive information, social media impersonation, and results in billions of dollars in damage each year.
How do you know which security solution is best for you? Should you evaluate tools based on trust or brand reputation alone — or perhaps on a reliable source of information, including source code?
That is why we created the 2FAS project — an initiative that adheres to the values of transparency, privacy and security. The 2FAS apps are designed to meet everyone's needs while remaining free, accessible and easy to use.
11
years of careful development and continuous improvements
4.7
average rating score in Google Play and AppStore
6+
million downloads worldwide
How we started and where we are today
Meet the TEAM
We're security, design and Open-source enthusiasts, forever curious and ready to transform the way online security is done.
Core squad
Mark
Founder, CEO

Greg
CTO

Andrew
CMO

Paul
CDO, UX

Raphael
Development

Zibi
Development

Matt
Development

Tobias
Development

Chris
Development

Matt
Design

Dominic
Design

Kate
Design

Olga
Design

Irena
Brand Relations Manager

Rafael
Lead translator, QA

Leo
Community Lead

Matt
Security Officer

Contributors
The project is developed with the support of an awesome group of contributors that regularly improve the 2FAS app codebase, content of our application and build the 2FAS community.
Do you want to contribute to the 2FAS project?
We are giving the world the 2FAS apps completely free. If you believe in keeping the Internet safe, open and secure for everyone, we would greatly value your contribution!
There are several ways for you to contribute to the 2FAS project:
- Check the 2FAS source code.
- Commit code changes.
- Increase security by finding and solving vulnerabilities.
- Translate the 2FAS app to new languages.
- Popularize the 2FAS app (Social Media / other channels).
- Help other users with 2FA and the 2FAS apps-related problems on our Discord server.
- Support other users by creating documentation and helping to overcome difficulties.
- Update and expand the service’s icons database.
- Donate to the 2FAS project.
2FAS has been featured in
Want to meet?
Let's connect online
or offline!
We are working 100% remotely, but if you're interested in cooperation, let's have an online call or meet for coffee in Las Vegas!
Find us in sunny Nevada
Two Factor Authentication Service, Inc.
1887 Whitney Mesa Dr #2130
Henderson, Nevada 89014


