Wir glauben daran,
das Internet sicherer
und geschützter zu machen. Für alle.
Die Open-Source- und Source-available-Apps von 2FAS sind ein zentraler Teil unserer Mission, die Sicherheitsstandards zum Schutz von Online-Konten zu revolutionieren.
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
)
}
}
Missionsgetrieben
Online-Sicherheit ist entscheidend für die Privatsphäre jedes Menschen – online wie offline. Kontoübernahmen führen zu Lecks persönlicher Daten, zum Verlust wertvoller und sensibler Informationen, zu Identitätsmissbrauch in sozialen Medien und verursachen jedes Jahr Schäden in Milliardenhöhe.
Woher weißt du, welche Sicherheitslösung die beste für dich ist? Solltest du Tools allein nach Vertrauen oder Markenruf bewerten – oder anhand verlässlicher Informationen, einschließlich des Quellcodes?
Deshalb haben wir das 2FAS-Projekt ins Leben gerufen – eine Initiative, die für Transparenz, Privatsphäre und Sicherheit steht. Die 2FAS-Apps sind für die Bedürfnisse aller entwickelt und bleiben dabei kostenlos, zugänglich und einfach zu bedienen.
Jahre sorgfältige Entwicklung und kontinuierliche Verbesserungen
durchschnittliche Rezensionen bei Google Play und im App Store
Millionen Downloads weltweit
Wie alles begann und wo wir heute stehen
Unser TEAM
Wir begeistern uns für Sicherheit, Design und Open Source, sind immer neugierig und bereit, Online-Sicherheit neu zu denken.
Kernteam
Mark
Gründer, CEO

Greg
CTO

Andrew
CMO

Paul
CDO, UX

Raphael
Entwicklung

Zibi
Entwicklung

Matt
Entwicklung

Tobias
Entwicklung

Chris
Entwicklung

Matt
Design

Dominic
Design

Kate
Design

Olga
Design

Irena
Brand Relations Manager

Rafael
Leitender-Übersetzer, QA

Leo
Community Leiter

Matt
Sicherheitsbeauftragter

Mitwirkende
Das Projekt wächst mit der Unterstützung einer großartigen Community, die regelmäßig den Code und die Inhalte der 2FAS-Apps verbessert und die 2FAS-Community weiter ausbaut.
Möchtest du zum 2FAS-Projekt beitragen?
Wir stellen die 2FAS-Apps der ganzen Welt völlig kostenlos zur Verfügung. Wenn du daran glaubst, das Internet für alle sicher, offen und geschützt zu halten, schätzen wir deinen Beitrag sehr!
Es gibt mehrere Möglichkeiten, wie du zum 2FAS-Projekt beitragen kannst:
- Prüfe den 2FAS-Quellcode.
- Trage Code-Änderungen bei.
- Erhöhe die Sicherheit, indem du Schwachstellen findest und behebst.
- Übersetze die 2FAS-App in neue Sprachen.
- Mach die 2FAS-App bekannt (Social Media / andere Kanäle).
- Hilf anderen Nutzern bei Problemen mit 2FA und den 2FAS-Apps auf unserem Discord-Server.
- Unterstütze andere Nutzer mit Dokumentation und hilf ihnen, Schwierigkeiten zu lösen.
- Aktualisiere und erweitere die Datenbank der Service-Icons.
- Spende für das 2FAS-Projekt.
2FAS wurde vorgestellt in
Lust auf ein Treffen?
Lass uns online
oder offline vernetzen!
Wir arbeiten zu 100 % remote. Wenn du an einer Zusammenarbeit interessiert bist, lass uns online sprechen oder auf einen Kaffee in Las Vegas treffen!
Du findest uns im sonnigen Nevada
Two Factor Authentication Service, Inc.
1887 Whitney Mesa Dr #2130
Henderson, Nevada 89014


