Wierzymy, że Internet może być bezpieczniejszym
i lepiej chronionym miejscem. Dla każdego.
Aplikacje 2FAS są kluczowe dla naszej misji zrewolucjonizowania standardów ochrony kont w Internecie.
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
)
}
}
Nasz cel
Bezpieczeństwo w sieci jest kluczowe dla prywatności każdego z nas, zarówno online, jak i offline. Przejęcia kont prowadzą do wycieków danych osobowych, ujawniania cennych i wrażliwych informacji lub podszywania się w mediach społecznościowych, powodując co roku szkody liczone w miliardach dolarów.
Skąd wiesz, które rozwiązanie bezpieczeństwa jest dla Ciebie najlepsze? Czy oceniasz narzędzia wyłącznie na podstawie zaufania lub reputacji marki, czy może w oparciu o wiarygodne źródło informacji, w tym kod źródłowy?
Dlatego stworzyliśmy projekt 2FAS – inicjatywę opartą na wartościach transparentności, prywatności i bezpieczeństwa. Aplikacje 2FAS są zaprojektowane tak, by spełniały potrzeby każdego, pozostając przy tym bezpłatne, dostępne i łatwe w użyciu.
11
lat starannego rozwoju i ciągłych ulepszeń
4.7
średnia ocena w Google Play i App Store
6+
milionów pobrań na całym świecie
Jak zaczynaliśmy i gdzie jesteśmy dziś
Poznaj nasz ZESPÓŁ
Jesteśmy entuzjastami bezpieczeństwa, designu i open source, wiecznie ciekawi świata i gotowi zmienić sposób, w jaki dba się o bezpieczeństwo w sieci.
Główny zespół
Marek
Założyciel, CEO

Grzegorz
CTO

Andrzej
CMO

Paweł
CDO, UX

Rafał
Programista

Zibi
Programista

Mateusz
Programista

Tobiasz
Programista

Krzysztof
Programista

Mateusz
Projektant

Dominik
Projektant

Kasia
Projektant

Olga
Projektant

Irena
Menedżer ds. Wizerunku i Relacji

Rafał
Główny tłumacz, QA

Leo
Lider społeczności

Mateusz
Bezpieczeństwo

Kontrybutorzy
Projekt 2FAS rozwijany jest dzięki wsparciu wspaniałej grupy społeczności, która regularnie ulepsza bazę kodu aplikacji 2FAS, jej zawartość i rozwija naszą społeczność na Discordzie i Reddicie.
Chcesz wesprzeć projekt 2FAS?
Udostępniamy światu aplikacje 2FAS całkowicie bezpłatnie. Jeśli wierzysz w utrzymanie Internetu bezpiecznym, otwartym i chronionym dla każdego, Twoje wsparcie będzie dla nas ogromnie ważne!
Jest kilka sposobów, w jakie możesz przyczynić się do rozwoju projektu 2FAS:
- Sprawdzaj kod źródłowy 2FAS.
- Wprowadzaj zmiany w kodzie.
- Zwiększaj bezpieczeństwo, wyszukując i usuwając luki.
- Tłumacz aplikację 2FAS na nowe języki.
- Promuj aplikację 2FAS w mediach społecznościowych i innych kanałach.
- Pomagaj innym użytkownikom w kwestiach związanych z 2FA i aplikacjami 2FAS na naszym serwerze Discord.
- Wspieraj innych użytkowników, tworząc dokumentację i pomagając im pokonywać trudności.
- Aktualizuj i rozbudowuj bazę ikon usług.
- Przekaż darowiznę na projekt 2FAS.
2FAS pojawił się w
Chcesz się spotkać?
Połączmy się online
lub offline!
Pracujemy w 100% zdalnie, ale jeśli jesteś zainteresowany współpracą, umówmy się na rozmowę online albo na kawę w Las Vegas!
Znajdziesz nas w słonecznej Nevadzie
Two Factor Authentication Service, Inc.
1887 Whitney Mesa Dr #2130
Henderson, Nevada 89014


