Wallet improvements

This commit is contained in:
Peter 2019-09-25 00:57:46 +04:00
parent ee9815cff8
commit c64c16b8ee
39 changed files with 4202 additions and 3733 deletions

View File

@ -255,6 +255,12 @@ build_verbose: check_env
//:IntentsExtension#dwarf-and-dsym,iphoneos-arm64 \
--verbose 8 ${BUCK_OPTIONS} ${BUCK_THREADS_OPTIONS} ${BUCK_DEBUG_OPTIONS}
deps: check_env
$(BUCK) query "deps(//:AppPackage)" --dot \
${BUCK_OPTIONS} ${BUCK_DEBUG_OPTIONS}
build_openssl: check_env
$(BUCK) build \
//submodules/openssl:openssl#iphoneos-arm64 \

View File

@ -236,11 +236,11 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
|> take(1)
|> mapToSignal { matchedContacts in
return account
|> introduceError(IntentContactsError.self)
|> castError(IntentContactsError.self)
|> mapToSignal { account -> Signal<[(String, TelegramUser)], IntentContactsError> in
if let account = account {
return matchingCloudContacts(postbox: account.postbox, contacts: matchedContacts)
|> introduceError(IntentContactsError.self)
|> castError(IntentContactsError.self)
} else {
return .fail(.generic)
}
@ -278,11 +278,11 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
let account = self.accountPromise.get()
let signal = account
|> introduceError(IntentHandlingError.self)
|> castError(IntentHandlingError.self)
|> mapToSignal { account -> Signal<INPerson?, IntentHandlingError> in
if let account = account {
return matchingCloudContact(postbox: account.postbox, peerId: PeerId(peerId))
|> introduceError(IntentHandlingError.self)
|> castError(IntentHandlingError.self)
|> map { user -> INPerson? in
if let user = user {
return personWithUser(stableId: "tg\(peerId)", user: user)
@ -394,7 +394,7 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
func handle(intent: INSearchForMessagesIntent, completion: @escaping (INSearchForMessagesIntentResponse) -> Void) {
self.actionDisposable.set((self.accountPromise.get()
|> take(1)
|> introduceError(IntentHandlingError.self)
|> castError(IntentHandlingError.self)
|> mapToSignal { account -> Signal<[INMessage], IntentHandlingError> in
guard let account = account else {
return .fail(.generic)
@ -409,7 +409,7 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
}
return (completion |> timeout(4.0, queue: Queue.mainQueue(), alternate: .single(Void())))
|> introduceError(IntentHandlingError.self)
|> castError(IntentHandlingError.self)
|> take(1)
|> mapToSignal { _ -> Signal<[INMessage], IntentHandlingError> in
let messages: Signal<[INMessage], NoError>
@ -419,7 +419,7 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
messages = unreadMessages(account: account)
}
return messages
|> introduceError(IntentHandlingError.self)
|> castError(IntentHandlingError.self)
|> afterDisposed {
account.shouldBeServiceTaskMaster.set(.single(.never))
}
@ -484,7 +484,7 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
for (_, messageId) in maxMessageIdsToApply {
signals.append(applyMaxReadIndexInteractively(postbox: account.postbox, stateManager: account.stateManager, index: MessageIndex(id: messageId, timestamp: 0))
|> introduceError(IntentHandlingError.self))
|> castError(IntentHandlingError.self))
}
if signals.isEmpty {
@ -576,7 +576,7 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
func handle(intent: INSearchCallHistoryIntent, completion: @escaping (INSearchCallHistoryIntentResponse) -> Void) {
self.actionDisposable.set((self.accountPromise.get()
|> take(1)
|> introduceError(IntentHandlingError.self)
|> castError(IntentHandlingError.self)
|> mapToSignal { account -> Signal<[CallRecord], IntentHandlingError> in
guard let account = account else {
return .fail(.generic)
@ -584,7 +584,7 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
account.shouldBeServiceTaskMaster.set(.single(.now))
return missedCalls(account: account)
|> introduceError(IntentHandlingError.self)
|> castError(IntentHandlingError.self)
|> afterDisposed {
account.shouldBeServiceTaskMaster.set(.single(.never))
}

View File

@ -4754,3 +4754,21 @@ Any member of this group will be able to see messages in the channel.";
"Channel.AdminLog.CanDeleteMessagesOfOthers" = "Delete Messages of Others";
"ChatSearch.ResultsTooltip" = "Tap to view as a list.";
"Updated.JustNow" = "updated just now";
"Updated.MinutesAgo_0" = "updated %@ minutes ago"; //three to ten
"Updated.MinutesAgo_1" = "updated 1 minute ago"; //one
"Updated.MinutesAgo_2" = "updated 2 minutes ago"; //two
"Updated.MinutesAgo_3_10" = "updated %@ minutes ago"; //three to ten
"Updated.MinutesAgo_many" = "updated %@ minutes ago"; // more than ten
"Updated.MinutesAgo_any" = "updated %@ minutes ago"; // more than ten
"Updated.HoursAgo_0" = "updated %@ hours ago";
"Updated.HoursAgo_1" = "updated 1 hour ago";
"Updated.HoursAgo_2" = "updated 2 hours ago";
"Updated.HoursAgo_3_10" = "updated %@ hours ago";
"Updated.HoursAgo_any" = "updated %@ hours ago";
"Updated.HoursAgo_many" = "updated %@ hours ago";
"Updated.HoursAgo_0" = "updated %@ hours ago";
"Updated.YesterdayAt" = "updated yesterday at %@";
"Updated.AtDate" = "updated %@";
"Updated.TodayAt" = "updated today at %@";

View File

@ -22,7 +22,8 @@
+ (DeviceSpecificEncryptionParameters * _Nonnull)deviceSpecificEncryptionParameters:(NSString * _Nonnull)rootPath baseAppBundleId:(NSString * _Nonnull)baseAppBundleId;
- (NSData * _Nullable)bundleDataWithAppToken:(NSData * _Nullable)appToken;
+ (void)encryptApplicationSecret:(NSData * _Nonnull)secret baseAppBundleId:(NSString * _Nonnull)baseAppBundleId completion:(void (^)(NSData * _Nullable))completion;
+ (void)decryptApplicationSecret:(NSData * _Nonnull)secret baseAppBundleId:(NSString * _Nonnull)baseAppBundleId completion:(void (^)(NSData * _Nullable))completion;
+ (void)getHardwareEncryptionAvailableWithBaseAppBundleId:(NSString * _Nonnull)baseAppBundleId completion:(void (^)(NSData * _Nullable))completion;
+ (void)encryptApplicationSecret:(NSData * _Nonnull)secret baseAppBundleId:(NSString * _Nonnull)baseAppBundleId completion:(void (^)(NSData * _Nullable, NSData * _Nullable))completion;
+ (void)decryptApplicationSecret:(NSData * _Nonnull)secret publicKey:(NSData * _Nonnull)publicKey baseAppBundleId:(NSString * _Nonnull)baseAppBundleId completion:(void (^)(NSData * _Nullable))completion;
@end

View File

@ -16,6 +16,8 @@
#import <MtProtoKitDynamic/MtProtoKitDynamic.h>
#endif
static NSString *telegramApplicationSecretKey = @"telegramApplicationSecretKey_v3";
static uint32_t funcSwap32(uint32_t input)
{
return OSSwapBigToHostInt32(input);
@ -238,7 +240,7 @@ API_AVAILABLE(ios(10))
}
- (NSData * _Nullable)encrypt:(NSData * _Nonnull)data;
- (NSData * _Nullable)decrypt:(NSData * _Nonnull)data;
- (NSData * _Nullable)decrypt:(NSData * _Nonnull)data cancelled:(bool *)cancelled;
@end
@ -258,6 +260,11 @@ API_AVAILABLE(ios(10))
CFRelease(_publicKey);
}
- (NSData * _Nullable)getPublicKey {
NSData *result = CFBridgingRelease(SecKeyCopyExternalRepresentation(_publicKey, nil));
return result;
}
- (NSData * _Nullable)encrypt:(NSData * _Nonnull)data {
if (data.length % 16 != 0) {
return nil;
@ -274,12 +281,17 @@ API_AVAILABLE(ios(10))
return cipherText;
}
- (NSData * _Nullable)decrypt:(NSData * _Nonnull)data {
- (NSData * _Nullable)decrypt:(NSData * _Nonnull)data cancelled:(bool *)cancelled {
CFErrorRef error = NULL;
NSData *plainText = (NSData *)CFBridgingRelease(SecKeyCreateDecryptedData(_privateKey, kSecKeyAlgorithmECIESEncryptionCofactorX963SHA256AESGCM, (__bridge CFDataRef)data, &error));
if (!plainText) {
__unused NSError *err = CFBridgingRelease(error);
if (err.code == -2) {
if (cancelled) {
*cancelled = true;
}
}
return nil;
}
@ -431,22 +443,29 @@ API_AVAILABLE(ios(10))
return bundleSeedID;
}
+ (LocalPrivateKey * _Nullable)getLocalPrivateKey:(NSString * _Nonnull)baseAppBundleId API_AVAILABLE(ios(10)) {
+ (NSString * _Nonnull)applicationSecretTag:(bool)isCheckKey {
if (isCheckKey) {
return [[telegramApplicationSecretKey stringByAppendingString:@"_check"] dataUsingEncoding:NSUTF8StringEncoding];
} else {
return [telegramApplicationSecretKey dataUsingEncoding:NSUTF8StringEncoding];
}
}
+ (LocalPrivateKey * _Nullable)getApplicationSecretKey:(NSString * _Nonnull)baseAppBundleId isCheckKey:(bool)isCheckKey API_AVAILABLE(ios(10)) {
NSString *bundleSeedId = [self bundleSeedId];
if (bundleSeedId == nil) {
return nil;
}
NSData *applicationTag = [self applicationSecretTag:isCheckKey];
NSString *accessGroup = [bundleSeedId stringByAppendingFormat:@".%@", baseAppBundleId];
NSData *applicationTag = [@"telegramLocalKey" dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *query = @{
(id)kSecClass: (id)kSecClassKey,
(id)kSecAttrApplicationTag: applicationTag,
(id)kSecAttrKeyType: (id)kSecAttrKeyTypeECSECPrimeRandom,
(id)kSecAttrAccessGroup: (id)accessGroup,
(id)kSecReturnRef: @YES,
(id)kSecReturnRef: @YES
};
SecKeyRef privateKey = NULL;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&privateKey);
@ -474,13 +493,13 @@ API_AVAILABLE(ios(10))
return result;
}
+ (bool)removeLocalPrivateKey:(NSString * _Nonnull)baseAppBundleId API_AVAILABLE(ios(10)) {
+ (bool)removeApplicationSecretKey:(NSString * _Nonnull)baseAppBundleId isCheckKey:(bool)isCheckKey API_AVAILABLE(ios(10)) {
NSString *bundleSeedId = [self bundleSeedId];
if (bundleSeedId == nil) {
return nil;
}
NSData *applicationTag = [@"telegramLocalKey" dataUsingEncoding:NSUTF8StringEncoding];
NSData *applicationTag = [self applicationSecretTag:isCheckKey];
NSString *accessGroup = [bundleSeedId stringByAppendingFormat:@".%@", baseAppBundleId];
NSDictionary *query = @{
@ -496,142 +515,21 @@ API_AVAILABLE(ios(10))
return true;
}
+ (LocalPrivateKey * _Nullable)addLocalPrivateKey:(NSString * _Nonnull)baseAppBundleId API_AVAILABLE(ios(10)) {
+ (LocalPrivateKey * _Nullable)addApplicationSecretKey:(NSString * _Nonnull)baseAppBundleId isCheckKey:(bool)isCheckKey API_AVAILABLE(ios(10)) {
NSString *bundleSeedId = [self bundleSeedId];
if (bundleSeedId == nil) {
return nil;
}
NSData *applicationTag = [@"telegramLocalKey" dataUsingEncoding:NSUTF8StringEncoding];
NSData *applicationTag = [self applicationSecretTag:isCheckKey];
NSString *accessGroup = [bundleSeedId stringByAppendingFormat:@".%@", baseAppBundleId];
SecAccessControlRef access = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleAlwaysThisDeviceOnly, kSecAccessControlPrivateKeyUsage, NULL);
NSDictionary *attributes = @{
(id)kSecAttrKeyType: (id)kSecAttrKeyTypeECSECPrimeRandom,
(id)kSecAttrKeySizeInBits: @256,
(id)kSecAttrTokenID: (id)kSecAttrTokenIDSecureEnclave,
(id)kSecPrivateKeyAttrs: @{
(id)kSecAttrIsPermanent: @YES,
(id)kSecAttrApplicationTag: applicationTag,
(id)kSecAttrAccessControl: (__bridge id)access,
(id)kSecAttrAccessGroup: (id)accessGroup,
},
};
CFErrorRef error = NULL;
SecKeyRef privateKey = SecKeyCreateRandomKey((__bridge CFDictionaryRef)attributes, &error);
if (!privateKey) {
if (access) {
CFRelease(access);
}
__unused NSError *err = CFBridgingRelease(error);
return nil;
SecAccessControlRef access;
if (isCheckKey) {
access = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, kSecAccessControlPrivateKeyUsage, NULL);
} else {
access = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, kSecAccessControlUserPresence | kSecAccessControlPrivateKeyUsage, NULL);
}
SecKeyRef publicKey = SecKeyCopyPublicKey(privateKey);
if (!publicKey) {
if (privateKey) {
CFRelease(privateKey);
}
if (access) {
CFRelease(access);
}
__unused NSError *err = CFBridgingRelease(error);
return nil;
}
LocalPrivateKey *result = [[LocalPrivateKey alloc] initWithPrivateKey:privateKey publicKey:publicKey];
if (publicKey) {
CFRelease(publicKey);
}
if (privateKey) {
CFRelease(privateKey);
}
if (access) {
CFRelease(access);
}
return result;
}
+ (LocalPrivateKey * _Nullable)getApplicationSecretKey:(NSString * _Nonnull)baseAppBundleId API_AVAILABLE(ios(10)) {
NSString *bundleSeedId = [self bundleSeedId];
if (bundleSeedId == nil) {
return nil;
}
NSString *accessGroup = [bundleSeedId stringByAppendingFormat:@".%@", baseAppBundleId];
NSData *applicationTag = [@"telegramApplicationSecretKey" dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *query = @{
(id)kSecClass: (id)kSecClassKey,
(id)kSecAttrApplicationTag: applicationTag,
(id)kSecAttrKeyType: (id)kSecAttrKeyTypeECSECPrimeRandom,
(id)kSecAttrAccessGroup: (id)accessGroup,
(id)kSecReturnRef: @YES,
};
SecKeyRef privateKey = NULL;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&privateKey);
if (status != errSecSuccess) {
return nil;
}
SecKeyRef publicKey = SecKeyCopyPublicKey(privateKey);
if (!publicKey) {
if (privateKey) {
CFRelease(privateKey);
}
return nil;
}
LocalPrivateKey *result = [[LocalPrivateKey alloc] initWithPrivateKey:privateKey publicKey:publicKey];
if (publicKey) {
CFRelease(publicKey);
}
if (privateKey) {
CFRelease(privateKey);
}
return result;
}
+ (bool)removeApplicationSecretKey:(NSString * _Nonnull)baseAppBundleId API_AVAILABLE(ios(10)) {
NSString *bundleSeedId = [self bundleSeedId];
if (bundleSeedId == nil) {
return nil;
}
NSData *applicationTag = [@"telegramApplicationSecretKey" dataUsingEncoding:NSUTF8StringEncoding];
NSString *accessGroup = [bundleSeedId stringByAppendingFormat:@".%@", baseAppBundleId];
NSDictionary *query = @{
(id)kSecClass: (id)kSecClassKey,
(id)kSecAttrApplicationTag: applicationTag,
(id)kSecAttrKeyType: (id)kSecAttrKeyTypeECSECPrimeRandom,
(id)kSecAttrAccessGroup: (id)accessGroup
};
OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
if (status != errSecSuccess) {
return false;
}
return true;
}
+ (LocalPrivateKey * _Nullable)addApplicationSecretKey:(NSString * _Nonnull)baseAppBundleId API_AVAILABLE(ios(10)) {
NSString *bundleSeedId = [self bundleSeedId];
if (bundleSeedId == nil) {
return nil;
}
NSData *applicationTag = [@"telegramApplicationSecretKey" dataUsingEncoding:NSUTF8StringEncoding];
NSString *accessGroup = [bundleSeedId stringByAppendingFormat:@".%@", baseAppBundleId];
SecAccessControlRef access = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleAlwaysThisDeviceOnly, kSecAccessControlUserPresence | kSecAccessControlPrivateKeyUsage, NULL);
NSDictionary *attributes = @{
(id)kSecAttrKeyType: (id)kSecAttrKeyTypeECSECPrimeRandom,
(id)kSecAttrKeySizeInBits: @256,
@ -738,24 +636,67 @@ API_AVAILABLE(ios(10))
return [[DeviceSpecificEncryptionParameters alloc] initWithKey:key salt:salt];
}
+ (void)encryptApplicationSecret:(NSData * _Nonnull)secret baseAppBundleId:(NSString * _Nonnull)baseAppBundleId completion:(void (^)(NSData * _Nullable))completion {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
LocalPrivateKey *privateKey = [self getApplicationSecretKey:baseAppBundleId];
if (privateKey == nil) {
privateKey = [self addApplicationSecretKey:baseAppBundleId];
+ (dispatch_queue_t)encryptionQueue {
static dispatch_queue_t instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = dispatch_queue_create("encryptionQueue", 0);
});
return instance;
}
+ (void)getHardwareEncryptionAvailableWithBaseAppBundleId:(NSString * _Nonnull)baseAppBundleId completion:(void (^)(NSData * _Nullable))completion {
dispatch_async([self encryptionQueue], ^{
LocalPrivateKey *checkKey = [self getApplicationSecretKey:baseAppBundleId isCheckKey:true];
if (checkKey != nil) {
NSData *sampleData = [checkKey encrypt:[NSData data]];
if (sampleData == nil) {
[self removeApplicationSecretKey:baseAppBundleId isCheckKey:false];
[self removeApplicationSecretKey:baseAppBundleId isCheckKey:true];
} else {
NSData *decryptedData = [checkKey decrypt:sampleData cancelled: nil];
if (decryptedData == nil) {
[self removeApplicationSecretKey:baseAppBundleId isCheckKey:false];
[self removeApplicationSecretKey:baseAppBundleId isCheckKey:true];
}
}
} else {
[self removeApplicationSecretKey:baseAppBundleId isCheckKey:false];
[self removeApplicationSecretKey:baseAppBundleId isCheckKey:true];
}
LocalPrivateKey *privateKey = [self getApplicationSecretKey:baseAppBundleId isCheckKey:false];
if (privateKey == nil) {
completion(nil);
return;
[self removeApplicationSecretKey:baseAppBundleId isCheckKey:false];
[self removeApplicationSecretKey:baseAppBundleId isCheckKey:true];
privateKey = [self addApplicationSecretKey:baseAppBundleId isCheckKey:false];
privateKey = [self addApplicationSecretKey:baseAppBundleId isCheckKey:true];
}
NSData *result = [privateKey encrypt:secret];
completion(result);
completion([privateKey getPublicKey]);
});
}
+ (void)decryptApplicationSecret:(NSData * _Nonnull)secret baseAppBundleId:(NSString * _Nonnull)baseAppBundleId completion:(void (^)(NSData * _Nullable))completion {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
LocalPrivateKey *privateKey = [self getApplicationSecretKey:baseAppBundleId];
+ (void)encryptApplicationSecret:(NSData * _Nonnull)secret baseAppBundleId:(NSString * _Nonnull)baseAppBundleId completion:(void (^)(NSData * _Nullable, NSData * _Nullable))completion {
dispatch_async([self encryptionQueue], ^{
LocalPrivateKey *privateKey = [self getApplicationSecretKey:baseAppBundleId isCheckKey:false];
if (privateKey == nil) {
[self removeApplicationSecretKey:baseAppBundleId isCheckKey:false];
[self removeApplicationSecretKey:baseAppBundleId isCheckKey:true];
privateKey = [self addApplicationSecretKey:baseAppBundleId isCheckKey:false];
privateKey = [self addApplicationSecretKey:baseAppBundleId isCheckKey:true];
}
if (privateKey == nil) {
completion(nil, nil);
return;
}
NSData *result = [privateKey encrypt:secret];
completion(result, [privateKey getPublicKey]);
});
}
+ (void)decryptApplicationSecret:(NSData * _Nonnull)secret publicKey:(NSData * _Nonnull)publicKey baseAppBundleId:(NSString * _Nonnull)baseAppBundleId completion:(void (^)(NSData * _Nullable))completion {
dispatch_async([self encryptionQueue], ^{
LocalPrivateKey *privateKey = [self getApplicationSecretKey:baseAppBundleId isCheckKey:false];
if (privateKey == nil) {
completion(nil);
return;
@ -764,7 +705,16 @@ API_AVAILABLE(ios(10))
completion(nil);
return;
}
NSData *result = [privateKey decrypt:secret];
NSData *currentPublicKey = [privateKey getPublicKey];
if (currentPublicKey == nil) {
completion(nil);
return;
}
if (![publicKey isEqualToData:currentPublicKey]) {
completion(nil);
return;
}
NSData *result = [privateKey decrypt:secret cancelled:nil];
completion(result);
});
}

View File

@ -242,6 +242,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
public final var visibleContentOffsetChanged: (ListViewVisibleContentOffset) -> Void = { _ in }
public final var visibleBottomContentOffsetChanged: (ListViewVisibleContentOffset) -> Void = { _ in }
public final var beganInteractiveDragging: () -> Void = { }
public final var endedInteractiveDragging: () -> Void = { }
public final var didEndScrolling: (() -> Void)?
private var currentGeneralScrollDirection: GeneralScrollDirection?
@ -599,6 +600,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
self.lastContentOffsetTimestamp = 0.0
self.didEndScrolling?()
}
self.endedInteractiveDragging()
}
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {

View File

@ -86,7 +86,7 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
}
var statusBarStyle: StatusBarStyle = .Ignore
var statusBarStyleUpdated: (() -> Void)?
var statusBarStyleUpdated: ((ContainedViewLayoutTransition) -> Void)?
init(controllerRemoved: @escaping (ViewController) -> Void) {
self.controllerRemoved = controllerRemoved
@ -260,7 +260,7 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
} else {
transitionType = .pop
}
self.state.pending = PendingChild(value: self.makeChild(layout: layout, value: last), transitionType: transitionType, transition: transition, update: { [weak self] pendingChild in
self.state.pending = PendingChild(value: self.makeChild(layout: layout.withUpdatedInputHeight(nil), value: last), transitionType: transitionType, transition: transition, update: { [weak self] pendingChild in
self?.pendingChildIsReady(pendingChild)
})
}
@ -268,12 +268,16 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
}
}
var statusBarTransition = transition
if let pending = self.state.pending {
if pending.isReady {
self.state.pending = nil
let previous = self.state.top
previous?.value.view.endEditing(true)
self.state.top = pending.value
self.topTransition(from: previous, to: pending.value, transitionType: pending.transitionType, layout: layout, transition: pending.transition)
self.topTransition(from: previous, to: pending.value, transitionType: pending.transitionType, layout: layout.withUpdatedInputHeight(nil), transition: pending.transition)
statusBarTransition = pending.transition
if !self.isReady {
self.isReady = true
self.isReadyUpdated?()
@ -287,11 +291,16 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
self.topTransition(from: previous, to: nil, transitionType: .pop, layout: layout, transition: .immediate)
}
var updatedStatusBarStyle = self.statusBarStyle
if let top = self.state.top {
self.applyLayout(layout: layout, to: top, transition: transition)
self.statusBarStyle = top.value.statusBar.statusBarStyle
updatedStatusBarStyle = top.value.statusBar.statusBarStyle
} else {
self.statusBarStyle = .Ignore
updatedStatusBarStyle = .Ignore
}
if self.statusBarStyle != updatedStatusBarStyle {
self.statusBarStyle = updatedStatusBarStyle
self.statusBarStyleUpdated?(statusBarTransition)
}
if self.state.transition == nil {

View File

@ -368,6 +368,12 @@ open class NavigationController: UINavigationController, ContainableController,
let flatContainer = NavigationContainer(controllerRemoved: { [weak self] controller in
self?.controllerRemoved(controller)
})
flatContainer.statusBarStyleUpdated = { [weak self] transition in
guard let strongSelf = self, let layout = strongSelf.validLayout else {
return
}
strongSelf.updateContainers(layout: layout, transition: transition)
}
self.displayNode.insertSubnode(flatContainer, at: 0)
self.rootContainer = .flat(flatContainer)
flatContainer.frame = CGRect(origin: CGPoint(), size: layout.size)
@ -378,6 +384,12 @@ open class NavigationController: UINavigationController, ContainableController,
let flatContainer = NavigationContainer(controllerRemoved: { [weak self] controller in
self?.controllerRemoved(controller)
})
flatContainer.statusBarStyleUpdated = { [weak self] transition in
guard let strongSelf = self, let layout = strongSelf.validLayout else {
return
}
strongSelf.updateContainers(layout: layout, transition: transition)
}
self.displayNode.insertSubnode(flatContainer, at: 0)
self.rootContainer = .flat(flatContainer)
flatContainer.frame = CGRect(origin: CGPoint(), size: layout.size)
@ -394,6 +406,7 @@ open class NavigationController: UINavigationController, ContainableController,
self.rootContainer = .split(splitContainer)
splitContainer.frame = CGRect(origin: CGPoint(), size: layout.size)
splitContainer.update(layout: layout, masterControllers: masterControllers, detailControllers: detailControllers, transition: .immediate)
flatContainer.statusBarStyleUpdated = nil
flatContainer.removeFromSupernode()
case let .split(splitContainer):
transition.updateFrame(node: splitContainer, frame: CGRect(origin: CGPoint(), size: layout.size))

View File

@ -34,7 +34,7 @@ private func importedAccountData(basePath: String, documentsPath: String, accoun
}
}
|> ignoreValues
|> introduceError(AccountImportError.self)
|> castError(AccountImportError.self)
let importData = importPreferencesData(documentsPath: documentsPath, masterDatacenterId: Int32(masterDatacenterId), account: account, database: database)
|> mapToSignal { accountUserId -> Signal<(AccountImportProgressType, Float), AccountImportError> in
@ -65,7 +65,7 @@ private func importPreferencesData(documentsPath: String, masterDatacenterId: In
transaction.setState(AuthorizedAccountState(isTestingEnvironment: false, masterDatacenterId: masterDatacenterId, peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: parsedAccountUserId), state: nil))
return parsedAccountUserId
}
|> introduceError(AccountImportError.self)
|> castError(AccountImportError.self)
} else {
return .fail(.generic)
}
@ -81,17 +81,17 @@ private func importDatabaseData(accountManager: AccountManager, account: Tempora
transaction.updatePeerPresencesInternal(presences: [user.id: presence], merge: { _, updated in return updated })
}
|> ignoreValues
|> introduceError(AccountImportError.self)
|> castError(AccountImportError.self)
}
let importedSecretChats = loadLegacySecretChats(account: account, basePath: basePath, accountPeerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: accountUserId), database: database)
|> introduceError(AccountImportError.self)
|> castError(AccountImportError.self)
/*let importedFiles = loadLegacyFiles(account: account, basePath: basePath, accountPeerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: accountUserId), database: database)
|> introduceError(AccountImportError.self)*/
|> castError(AccountImportError.self)*/
let importedLegacyPreferences = importLegacyPreferences(accountManager: accountManager, account: account, documentsPath: basePath + "/Documents", database: database)
|> introduceError(AccountImportError.self)
|> castError(AccountImportError.self)
return importedAccountUser
|> map { _ -> (AccountImportProgressType, Float) in return (.generic, 0.0) }
@ -224,7 +224,7 @@ public func importedLegacyAccount(basePath: String, accountManager: AccountManag
}
return temporaryAccount(manager: accountManager, rootPath: rootPathForBasePath(basePath), encryptionParameters: encryptionParameters)
|> introduceError(AccountImportError.self)
|> castError(AccountImportError.self)
|> mapToSignal { account -> Signal<ImportedLegacyAccountEvent, AccountImportError> in
let actions = importedAccountData(basePath: basePath, documentsPath: documentsPath, accountManager: accountManager, account: account, database: database)
var result = actions

View File

@ -209,7 +209,7 @@ public final class SecureIdAuthController: ViewController {
switch self.mode {
case let .form(peerId, scope, publicKey, callbackUrl, _, _):
self.formDisposable = (combineLatest(requestSecureIdForm(postbox: context.account.postbox, network: context.account.network, peerId: peerId, scope: scope, publicKey: publicKey), secureIdConfiguration(postbox: context.account.postbox, network: context.account.network) |> introduceError(RequestSecureIdFormError.self))
self.formDisposable = (combineLatest(requestSecureIdForm(postbox: context.account.postbox, network: context.account.network, peerId: peerId, scope: scope, publicKey: publicKey), secureIdConfiguration(postbox: context.account.postbox, network: context.account.network) |> castError(RequestSecureIdFormError.self))
|> mapToSignal { form, configuration -> Signal<SecureIdEncryptedFormData, RequestSecureIdFormError> in
return context.account.postbox.transaction { transaction -> Signal<SecureIdEncryptedFormData, RequestSecureIdFormError> in
guard let accountPeer = transaction.getPeer(context.account.peerId), let servicePeer = transaction.getPeer(form.peerId) else {
@ -240,7 +240,7 @@ public final class SecureIdAuthController: ViewController {
handleError(error, callbackUrl, peerId)
})
case .list:
self.formDisposable = (combineLatest(getAllSecureIdValues(network: self.context.account.network), secureIdConfiguration(postbox: context.account.postbox, network: context.account.network) |> introduceError(GetAllSecureIdValuesError.self), context.account.postbox.transaction { transaction -> Signal<Peer, GetAllSecureIdValuesError> in
self.formDisposable = (combineLatest(getAllSecureIdValues(network: self.context.account.network), secureIdConfiguration(postbox: context.account.postbox, network: context.account.network) |> castError(GetAllSecureIdValuesError.self), context.account.postbox.transaction { transaction -> Signal<Peer, GetAllSecureIdValuesError> in
guard let accountPeer = transaction.getPeer(context.account.peerId) else {
return .fail(.generic)
}

View File

@ -337,7 +337,7 @@ public func channelDiscussionGroupSetupController(context: AccountContext, peerI
return updateGroupDiscussionForChannel(network: context.account.network, postbox: context.account.postbox, channelId: peerId, groupId: resultPeerId)
}
|> introduceError(ChannelDiscussionGroupError.self)
|> castError(ChannelDiscussionGroupError.self)
|> switchToLatest
}
} else {

View File

@ -355,7 +355,7 @@ public func channelMembersController(context: AccountContext, peerId: PeerId) ->
addMembersDisposable.set((contactsController.result
|> deliverOnMainQueue
|> introduceError(AddChannelMemberError.self)
|> castError(AddChannelMemberError.self)
|> mapToSignal { [weak contactsController] contacts -> Signal<Never, AddChannelMemberError> in
contactsController?.displayProgress = true

View File

@ -1105,7 +1105,7 @@ public func deviceContactInfoController(context: AccountContext, subject: Device
}
let _ = (contactDataManager.createContactWithData(composedContactData)
|> introduceError(AddContactError.self)
|> castError(AddContactError.self)
|> mapToSignal { contactIdAndData -> Signal<(DeviceContactStableId, DeviceContactExtendedData, Peer?)?, AddContactError> in
guard let (id, data) = contactIdAndData else {
return .single(nil)
@ -1122,7 +1122,7 @@ public func deviceContactInfoController(context: AccountContext, subject: Device
context.account.postbox.transaction { transaction -> (DeviceContactStableId, DeviceContactExtendedData, Peer?)? in
return (id, data, transaction.getPeer(peer.id))
}
|> introduceError(AddContactError.self)
|> castError(AddContactError.self)
)
}
default:
@ -1130,13 +1130,13 @@ public func deviceContactInfoController(context: AccountContext, subject: Device
}
return importContact(account: context.account, firstName: composedContactData.basicData.firstName, lastName: composedContactData.basicData.lastName, phoneNumber: filteredPhoneNumbers[0].value)
|> introduceError(AddContactError.self)
|> castError(AddContactError.self)
|> mapToSignal { peerId -> Signal<(DeviceContactStableId, DeviceContactExtendedData, Peer?)?, AddContactError> in
if let peerId = peerId {
return context.account.postbox.transaction { transaction -> (DeviceContactStableId, DeviceContactExtendedData, Peer?)? in
return (id, data, transaction.getPeer(peerId))
}
|> introduceError(AddContactError.self)
|> castError(AddContactError.self)
} else {
return .single((id, data, nil))
}

View File

@ -1760,7 +1760,7 @@ public func chatMessagePhotoInteractiveFetched(context: AccountContext, photoRef
|> mapToSignal { type -> Signal<FetchResourceSourceType, FetchResourceError> in
if case .remote = type, let peerType = storeToDownloadsPeerType {
return storeDownloadedMedia(storeManager: context.downloadedMediaStoreManager, media: photoReference.abstract, peerType: peerType)
|> introduceError(FetchResourceError.self)
|> castError(FetchResourceError.self)
|> mapToSignal { _ -> Signal<FetchResourceSourceType, FetchResourceError> in
return .complete()
}

View File

@ -54,6 +54,8 @@
BOOL _isOpenGLLoaded;
}
@property (nonatomic) CGRect defaultFrame;
- (instancetype)initWithBackgroundColor:(UIColor *)backgroundColor primaryColor:(UIColor *)primaryColor buttonColor:(UIColor *)buttonColor accentColor:(UIColor *)accentColor regularDotColor:(UIColor *)regularDotColor highlightedDotColor:(UIColor *)highlightedDotColor suggestedLocalizationSignal:(SSignal *)suggestedLocalizationSignal;
@property (nonatomic, copy) void (^startMessaging)(void);

View File

@ -71,6 +71,23 @@ static void TGDispatchOnMainThread(dispatch_block_t block) {
}
@end
@interface RMIntroView : UIView
@property (nonatomic, copy) void (^onLayout)();
@end
@implementation RMIntroView
- (void)layoutSubviews {
[super layoutSubviews];
if (_onLayout) {
_onLayout();
}
}
@end
@interface RMIntroViewController () <UIGestureRecognizerDelegate>
{
@ -92,6 +109,8 @@ static void TGDispatchOnMainThread(dispatch_block_t block) {
SVariable *_alternativeLocalization;
NSDictionary<NSString *, NSString *> *_englishStrings;
bool _loadedView;
}
@end
@ -215,12 +234,6 @@ static void TGDispatchOnMainThread(dispatch_block_t block) {
}
}
- (void)loadView
{
[super loadView];
}
- (void)loadGL
{
if (/*[[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground*/true && !_isOpenGLLoaded)
@ -273,10 +286,28 @@ static void TGDispatchOnMainThread(dispatch_block_t block) {
_isOpenGLLoaded = false;
}
- (void)loadView {
self.view = [[RMIntroView alloc] initWithFrame:self.defaultFrame];
__weak RMIntroViewController *weakSelf = self;
((RMIntroView *)self.view).onLayout = ^{
__strong RMIntroViewController *strongSelf = weakSelf;
if (strongSelf != nil) {
[strongSelf updateLayout];
}
};
[self viewDidLoad];
}
- (void)viewDidLoad
{
[super viewDidLoad];
if (_loadedView) {
return;
}
_loadedView = true;
self.view.backgroundColor = _backgroundColor;
[self loadGL];
@ -411,7 +442,7 @@ static void TGDispatchOnMainThread(dispatch_block_t block) {
return deviceScreen;
}
- (void)viewWillLayoutSubviews
- (void)updateLayout
{
UIInterfaceOrientation isVertical = (self.view.bounds.size.height / self.view.bounds.size.width > 1.0f);

View File

@ -485,7 +485,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
if !wallets.wallets.isEmpty {
let _ = (testGiverWalletAddress(tonInstance: tonContext.instance)
|> deliverOnMainQueue).start(next: { address in
arguments.pushController(WalletInfoScreen(context: context, tonContext: tonContext, walletInfo: wallets.wallets[0], address: address))
arguments.pushController(WalletInfoScreen(context: context, tonContext: tonContext, walletInfo: wallets.wallets[0].info, address: address))
})
}
}
@ -500,7 +500,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|> deliverOnMainQueue).start(next: { wallets in
if let tonContext = context.tonContext {
if !wallets.wallets.isEmpty {
let _ = (walletAddress(publicKey: wallets.wallets[0].publicKey, tonInstance: tonContext.instance)
let _ = (walletAddress(publicKey: wallets.wallets[0].info.publicKey, tonInstance: tonContext.instance)
|> deliverOnMainQueue).start(next: { address in
let _ = (getGramsFromTestGiver(address: address, amount: 1500000000, tonInstance: tonContext.instance)
|> deliverOnMainQueue).start(completed: {

View File

@ -1609,16 +1609,33 @@ private func accountContextMenuItems(context: AccountContext, logout: @escaping
}
func openWallet(context: AccountContext, push: @escaping (ViewController) -> Void) {
let _ = (availableWallets(postbox: context.account.postbox)
|> deliverOnMainQueue).start(next: { wallets in
if let tonContext = context.tonContext {
if wallets.wallets.isEmpty {
guard let tonContext = context.tonContext else {
return
}
let _ = (combineLatest(queue: .mainQueue(),
availableWallets(postbox: context.account.postbox),
tonContext.keychain.encryptionPublicKey()
)
|> deliverOnMainQueue).start(next: { wallets, currentPublicKey in
if wallets.wallets.isEmpty {
if let _ = currentPublicKey {
push(WalletSplashScreen(context: context, tonContext: tonContext, mode: .intro))
} else {
let _ = (walletAddress(publicKey: wallets.wallets[0].publicKey, tonInstance: tonContext.instance)
|> deliverOnMainQueue).start(next: { address in
push(WalletInfoScreen(context: context, tonContext: tonContext, walletInfo: wallets.wallets[0], address: address))
})
push(WalletSplashScreen(context: context, tonContext: tonContext, mode: .secureStorageNotAvailable))
}
} else {
let walletInfo = wallets.wallets[0].info
if let currentPublicKey = currentPublicKey {
if currentPublicKey == walletInfo.encryptedSecret.publicKey {
let _ = (walletAddress(publicKey: walletInfo.publicKey, tonInstance: tonContext.instance)
|> deliverOnMainQueue).start(next: { address in
push(WalletInfoScreen(context: context, tonContext: tonContext, walletInfo: walletInfo, address: address))
})
} else {
push(WalletSplashScreen(context: context, tonContext: tonContext, mode: .secureStorageReset(.changed)))
}
} else {
push(WalletSplashScreen(context: context, tonContext: tonContext, mode: .secureStorageReset(.notAvailable)))
}
}
})

View File

@ -272,7 +272,7 @@ public func preparedShareItems(account: Account, to peerId: PeerId, dataItems: [
dataSignals = dataSignals
|> then(
wrappedSignal
|> introduceError(Void.self)
|> castError(Void.self)
|> take(1)
)
}
@ -339,11 +339,11 @@ public func sentShareItems(account: Account, to peerIds: [PeerId], items: [Prepa
}
return enqueueMessagesToMultiplePeers(account: account, peerIds: peerIds, messages: messages)
|> introduceError(Void.self)
|> castError(Void.self)
|> mapToSignal { messageIds -> Signal<Float, Void> in
let key: PostboxViewKey = .messages(Set(messageIds))
return account.postbox.combinedView(keys: [key])
|> introduceError(Void.self)
|> castError(Void.self)
|> mapToSignal { view -> Signal<Float, Void> in
if let messagesView = view.views[key] as? MessagesView {
for (_, message) in messagesView.messages {

View File

@ -11,11 +11,32 @@ import MtProtoKit
import TelegramApi
#endif
public struct TonKeychain {
public let encrypt: (Data) -> Signal<Data?, NoError>
public let decrypt: (Data) -> Signal<Data?, NoError>
public struct TonKeychainEncryptedData: Codable, Equatable {
public let publicKey: Data
public let data: Data
public init(encrypt: @escaping (Data) -> Signal<Data?, NoError>, decrypt: @escaping (Data) -> Signal<Data?, NoError>) {
public init(publicKey: Data, data: Data) {
self.publicKey = publicKey
self.data = data
}
}
public enum TonKeychainEncryptDataError {
case generic
}
public enum TonKeychainDecryptDataError {
case generic
case publicKeyMismatch
}
public struct TonKeychain {
public let encryptionPublicKey: () -> Signal<Data?, NoError>
public let encrypt: (Data) -> Signal<TonKeychainEncryptedData, TonKeychainEncryptDataError>
public let decrypt: (TonKeychainEncryptedData) -> Signal<Data, TonKeychainDecryptDataError>
public init(encryptionPublicKey: @escaping () -> Signal<Data?, NoError>, encrypt: @escaping (Data) -> Signal<TonKeychainEncryptedData, TonKeychainEncryptDataError>, decrypt: @escaping (TonKeychainEncryptedData) -> Signal<Data, TonKeychainDecryptDataError>) {
self.encryptionPublicKey = encryptionPublicKey
self.encrypt = encrypt
self.decrypt = decrypt
}
@ -93,7 +114,7 @@ public final class TonInstance {
}
}
fileprivate func createWallet(keychain: TonKeychain, serverSalt: Data) -> Signal<(WalletInfo, [String]), NoError> {
fileprivate func createWallet(keychain: TonKeychain, serverSalt: Data) -> Signal<(WalletInfo, [String]), CreateWalletError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
@ -104,17 +125,14 @@ public final class TonInstance {
return
}
let cancel = keychain.encrypt(key.secret).start(next: { encryptedSecretData in
guard let encryptedSecretData = encryptedSecretData else {
assertionFailure()
return
}
let _ = self.exportKey(key: key, serverSalt: serverSalt).start(next: { wordList in
subscriber.putNext((WalletInfo(publicKey: WalletPublicKey(rawValue: key.publicKey), encryptedSecret: EncryptedWalletSecret(rawValue: encryptedSecretData)), wordList))
subscriber.putNext((WalletInfo(publicKey: WalletPublicKey(rawValue: key.publicKey), encryptedSecret: encryptedSecretData), wordList))
subscriber.putCompletion()
}, error: { _ in
preconditionFailure()
}, error: { error in
subscriber.putError(.generic)
})
}, error: { _ in
subscriber.putError(.generic)
}, completed: {
})
}, error: { _ in
@ -142,30 +160,14 @@ public final class TonInstance {
return
}
let cancel = keychain.encrypt(key.secret).start(next: { encryptedSecretData in
guard let encryptedSecretData = encryptedSecretData else {
subscriber.putError(.generic)
return
}
subscriber.putNext(WalletInfo(publicKey: WalletPublicKey(rawValue: key.publicKey), encryptedSecret: EncryptedWalletSecret(rawValue: encryptedSecretData)))
subscriber.putNext(WalletInfo(publicKey: WalletPublicKey(rawValue: key.publicKey), encryptedSecret: encryptedSecretData))
subscriber.putCompletion()
}, error: { _ in
subscriber.putError(.generic)
}, completed: {
})
}, error: { error in
if let error = error as? TONError {
#warning("Use error kind APIs")
let filePrefix = "File \""
let fileSuffix = "\" can't be created for writing"
let text = error.text
if text.hasPrefix(filePrefix) && text.hasSuffix(fileSuffix) {
let filePath = String(error.text[text.index(text.startIndex, offsetBy: filePrefix.count) ..< text.index(text.endIndex, offsetBy: -fileSuffix.count)])
subscriber.putError(.fileExists(filePath))
} else {
subscriber.putError(.generic)
}
} else {
subscriber.putError(.generic)
}
}, error: { _ in
subscriber.putError(.generic)
}, completed: {
})
disposable.set(ActionDisposable {
@ -253,10 +255,10 @@ public final class TonInstance {
}
}
fileprivate func getWalletState(address: String) -> Signal<WalletState, NoError> {
fileprivate func getWalletState(address: String) -> Signal<(WalletState, Int64), NoError> {
return self.getWalletStateRaw(address: address)
|> map { state in
return WalletState(balance: state.balance, lastTransactionId: state.lastTransactionId.flatMap(WalletTransactionId.init(tonTransactionId:)))
return (WalletState(balance: state.balance, lastTransactionId: state.lastTransactionId.flatMap(WalletTransactionId.init(tonTransactionId:))), state.syncUtime)
}
}
@ -424,12 +426,11 @@ public final class TonInstance {
}
fileprivate func sendGramsFromWallet(keychain: TonKeychain, serverSalt: Data, walletInfo: WalletInfo, fromAddress: String, toAddress: String, amount: Int64, textMessage: String) -> Signal<Never, SendGramsFromWalletError> {
return keychain.decrypt(walletInfo.encryptedSecret.rawValue)
|> castError(SendGramsFromWalletError.self)
return keychain.decrypt(walletInfo.encryptedSecret)
|> mapError { _ -> SendGramsFromWalletError in
return .secretDecryptionFailed
}
|> mapToSignal { decryptedSecret -> Signal<Never, SendGramsFromWalletError> in
guard let decryptedSecret = decryptedSecret else {
return .fail(.secretDecryptionFailed)
}
let key = TONKey(publicKey: walletInfo.publicKey.rawValue, secret: decryptedSecret)
return self.ensureWalletInitialized(address: fromAddress, key: key, serverSalt: serverSalt)
@ -462,12 +463,11 @@ public final class TonInstance {
}
fileprivate func walletRestoreWords(walletInfo: WalletInfo, keychain: TonKeychain, serverSalt: Data) -> Signal<[String], WalletRestoreWordsError> {
return keychain.decrypt(walletInfo.encryptedSecret.rawValue)
|> castError(WalletRestoreWordsError.self)
return keychain.decrypt(walletInfo.encryptedSecret)
|> mapError { _ -> WalletRestoreWordsError in
return .secretDecryptionFailed
}
|> mapToSignal { decryptedSecret -> Signal<[String], WalletRestoreWordsError> in
guard let decryptedSecret = decryptedSecret else {
return .fail(.secretDecryptionFailed)
}
return Signal { subscriber in
let disposable = MetaDisposable()
@ -504,31 +504,28 @@ public struct WalletPublicKey: Codable, Hashable {
}
}
public struct EncryptedWalletSecret: Codable, Equatable {
public var rawValue: Data
public init(rawValue: Data) {
self.rawValue = rawValue
}
}
public struct WalletInfo: PostboxCoding, Codable, Equatable {
public let publicKey: WalletPublicKey
public let encryptedSecret: EncryptedWalletSecret
public let encryptedSecret: TonKeychainEncryptedData
public init(publicKey: WalletPublicKey, encryptedSecret: EncryptedWalletSecret) {
public init(publicKey: WalletPublicKey, encryptedSecret: TonKeychainEncryptedData) {
self.publicKey = publicKey
self.encryptedSecret = encryptedSecret
}
public init(decoder: PostboxDecoder) {
self.publicKey = WalletPublicKey(rawValue: decoder.decodeStringForKey("publicKey", orElse: ""))
self.encryptedSecret = EncryptedWalletSecret(rawValue: decoder.decodeDataForKey("encryptedSecret")!)
if let publicKey = decoder.decodeDataForKey("encryptedSecretPublicKey"), let secret = decoder.decodeDataForKey("encryptedSecretData") {
self.encryptedSecret = TonKeychainEncryptedData(publicKey: publicKey, data: secret)
} else {
self.encryptedSecret = TonKeychainEncryptedData(publicKey: Data(), data: Data())
}
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeString(self.publicKey.rawValue, forKey: "publicKey")
encoder.encodeData(self.encryptedSecret.rawValue, forKey: "encryptedSecret")
encoder.encodeData(self.encryptedSecret.publicKey, forKey: "encryptedSecretPublicKey")
encoder.encodeData(self.encryptedSecret.data, forKey: "encryptedSecretData")
}
}
@ -550,7 +547,7 @@ public struct WalletStateRecord: PostboxCoding, Equatable {
public init(decoder: PostboxDecoder) {
self.info = decoder.decodeDataForKey("info").flatMap { data in
return try? JSONDecoder().decode(WalletInfo.self, from: data)
} ?? WalletInfo(publicKey: WalletPublicKey(rawValue: ""), encryptedSecret: EncryptedWalletSecret(rawValue: Data()))
} ?? WalletInfo(publicKey: WalletPublicKey(rawValue: ""), encryptedSecret: TonKeychainEncryptedData(publicKey: Data(), data: Data()))
self.state = decoder.decodeDataForKey("state").flatMap { data in
return try? JSONDecoder().decode(CombinedWalletState.self, from: data)
}
@ -617,7 +614,6 @@ public func createWallet(postbox: Postbox, network: Network, tonInstance: TonIns
}
|> mapToSignal { serverSalt -> Signal<(WalletInfo, [String]), CreateWalletError> in
return tonInstance.createWallet(keychain: keychain, serverSalt: serverSalt)
|> castError(CreateWalletError.self)
|> mapToSignal { walletInfo, wordList -> Signal<(WalletInfo, [String]), CreateWalletError> in
return postbox.transaction { transaction -> (WalletInfo, [String]) in
transaction.updatePreferencesEntry(key: PreferencesKeys.walletCollection, { current in
@ -634,7 +630,6 @@ public func createWallet(postbox: Postbox, network: Network, tonInstance: TonIns
private enum ImportWalletInternalError {
case generic
case fileExists(String)
}
public enum ImportWalletError {
@ -652,12 +647,6 @@ public func importWallet(postbox: Postbox, network: Network, tonInstance: TonIns
switch error {
case .generic:
return .fail(.generic)
case let .fileExists(path):
let _ = try? FileManager.default.removeItem(atPath: path)
return tonInstance.importWallet(keychain: keychain, wordList: wordList, serverSalt: serverSalt)
|> mapError { _ -> ImportWalletError in
return .generic
}
}
}
|> mapToSignal { walletInfo -> Signal<WalletInfo, ImportWalletError> in
@ -718,7 +707,7 @@ public func testGiverWalletAddress(tonInstance: TonInstance) -> Signal<String, N
return tonInstance.testGiverWalletAddress()
}
public func getWalletState(address: String, tonInstance: TonInstance) -> Signal<WalletState, NoError> {
private func getWalletState(address: String, tonInstance: TonInstance) -> Signal<(WalletState, Int64), NoError> {
return tonInstance.getWalletState(address: address)
}
@ -750,7 +739,7 @@ public func getCombinedWalletState(postbox: Postbox, walletInfo: WalletInfo, ton
|> mapToSignal { address -> Signal<CombinedWalletStateResult, GetCombinedWalletStateError> in
return getWalletState(address: address, tonInstance: tonInstance)
|> castError(GetCombinedWalletStateError.self)
|> mapToSignal { walletState -> Signal<CombinedWalletStateResult, GetCombinedWalletStateError> in
|> mapToSignal { walletState, syncUtime -> Signal<CombinedWalletStateResult, GetCombinedWalletStateError> in
let topTransactions: Signal<[WalletTransaction], GetCombinedWalletStateError>
if walletState.lastTransactionId == cachedState?.walletState.lastTransactionId {
topTransactions = .single(cachedState?.topTransactions ?? [])
@ -762,7 +751,7 @@ public func getCombinedWalletState(postbox: Postbox, walletInfo: WalletInfo, ton
}
return topTransactions
|> mapToSignal { topTransactions -> Signal<CombinedWalletStateResult, GetCombinedWalletStateError> in
let combinedState = CombinedWalletState(walletState: walletState, timestamp: 0, topTransactions: topTransactions)
let combinedState = CombinedWalletState(walletState: walletState, timestamp: syncUtime, topTransactions: topTransactions)
return postbox.transaction { transaction -> CombinedWalletStateResult in
transaction.updatePreferencesEntry(key: PreferencesKeys.walletCollection, { current in
var walletCollection = (current as? WalletCollection) ?? WalletCollection(wallets: [])

View File

@ -650,7 +650,9 @@ final class SharedApplicationContext {
let tonKeychain: TonKeychain
#if targetEnvironment(simulator)
tonKeychain = TonKeychain(encrypt: { data in
tonKeychain = TonKeychain(encryptionPublicKey: {
return .single(Data())
}, encrypt: { data in
return Signal { subscriber in
subscriber.putNext(data)
subscriber.putCompletion()
@ -664,18 +666,34 @@ final class SharedApplicationContext {
}
})
#else
tonKeychain = TonKeychain(encrypt: { data in
tonKeychain = TonKeychain(encryptionPublicKey: {
return Signal { subscriber in
BuildConfig.encryptApplicationSecret(data, baseAppBundleId: baseAppBundleId, completion: { result in
subscriber.putNext(result)
BuildConfig.getHardwareEncryptionAvailable(withBaseAppBundleId: baseAppBundleId, completion: { value in
subscriber.putNext(value)
subscriber.putCompletion()
})
return EmptyDisposable
}
}, decrypt: { data in
}, encrypt: { data in
return Signal { subscriber in
BuildConfig.decryptApplicationSecret(data, baseAppBundleId: baseAppBundleId, completion: { result in
subscriber.putNext(result)
BuildConfig.encryptApplicationSecret(data, baseAppBundleId: baseAppBundleId, completion: { result, publicKey in
if let result = result, let publicKey = publicKey {
subscriber.putNext(TonKeychainEncryptedData(publicKey: publicKey, data: result))
subscriber.putCompletion()
} else {
subscriber.putError(.generic)
}
})
return EmptyDisposable
}
}, decrypt: { encryptedData in
return Signal { subscriber in
BuildConfig.decryptApplicationSecret(encryptedData.data, publicKey: encryptedData.publicKey, baseAppBundleId: baseAppBundleId, completion: { result in
if let result = result {
subscriber.putNext(result)
} else {
subscriber.putError(.generic)
}
subscriber.putCompletion()
})
return EmptyDisposable
@ -1319,14 +1337,6 @@ final class SharedApplicationContext {
self.isInForegroundPromise.set(true)
self.isActiveValue = true
self.isActivePromise.set(true)
let configuration = URLSessionConfiguration.background(withIdentifier: "org.telegram.Telegram-iOS.background")
let session = URLSession(configuration: configuration)
if #available(iOS 9.0, *) {
session.getAllTasks(completionHandler: { tasks in
print(tasks)
})
}
}
func applicationWillTerminate(_ application: UIApplication) {

View File

@ -22,6 +22,8 @@ final class AuthorizationSequenceSplashController: ViewController {
private let controller: RMIntroViewController
private var validLayout: ContainerViewLayout?
var nextPressed: ((PresentationStrings?) -> Void)?
private let suggestedLocalization = Promise<SuggestedLocalizationInfo?>()
@ -102,7 +104,9 @@ final class AuthorizationSequenceSplashController: ViewController {
private func addControllerIfNeeded() {
if !controller.isViewLoaded || controller.view.superview == nil {
self.displayNode.view.addSubview(controller.view)
controller.view.frame = self.displayNode.bounds;
if let layout = self.validLayout {
controller.view.frame = CGRect(origin: CGPoint(), size: layout.size)
}
controller.viewDidAppear(false)
}
}
@ -134,14 +138,18 @@ final class AuthorizationSequenceSplashController: ViewController {
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.validLayout = layout
let controllerFrame = CGRect(origin: CGPoint(), size: layout.size)
self.controller.defaultFrame = controllerFrame
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: 0.0, transition: transition)
self.addControllerIfNeeded()
if case .immediate = transition {
self.controller.view.frame = CGRect(origin: CGPoint(), size: layout.size)
self.controller.view.frame = controllerFrame
} else {
UIView.animate(withDuration: 0.3, animations: {
self.controller.view.frame = CGRect(origin: CGPoint(), size: layout.size)
self.controller.view.frame = controllerFrame
})
}
}

View File

@ -8,7 +8,7 @@ import AccountContext
func preloadedChatHistoryViewForLocation(_ location: ChatHistoryLocationInput, account: Account, chatLocation: ChatLocation, fixedCombinedReadStates: MessageHistoryViewReadState?, tagMask: MessageTags?, additionalData: [AdditionalMessageHistoryViewData], orderStatistics: MessageHistoryViewOrderStatistics = []) -> Signal<ChatHistoryViewUpdate, NoError> {
return chatHistoryViewForLocation(location, account: account, chatLocation: chatLocation, scheduled: false, fixedCombinedReadStates: fixedCombinedReadStates, tagMask: tagMask, additionalData: additionalData, orderStatistics: orderStatistics)
|> introduceError(Bool.self)
|> castError(Bool.self)
|> mapToSignal { update -> Signal<ChatHistoryViewUpdate, Bool> in
switch update {
case let .Loading(value):

View File

@ -79,7 +79,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee
let stickerSettings: StickerSettings = (transaction.getSharedData(ApplicationSpecificSharedDataKeys.stickerSettings) as? StickerSettings) ?? .defaultSettings
return stickerSettings
}
|> introduceError(ChatContextQueryError.self)
|> castError(ChatContextQueryError.self)
|> mapToSignal { stickerSettings -> Signal<[FoundStickerItem], ChatContextQueryError> in
let scope: SearchStickersScope
switch stickerSettings.emojiStickerSuggestionMode {
@ -91,7 +91,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee
scope = [.installed]
}
return searchStickers(account: context.account, query: query.basicEmoji.0, scope: scope)
|> introduceError(ChatContextQueryError.self)
|> castError(ChatContextQueryError.self)
}
|> map { stickers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
return { _ in
@ -123,7 +123,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee
}
return { _ in return .hashtags(result) }
}
|> introduceError(ChatContextQueryError.self)
|> castError(ChatContextQueryError.self)
return signal |> then(hashtags)
case let .mention(query, types):
@ -178,7 +178,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee
}
return { _ in return .mentions(sortedPeers) }
}
|> introduceError(ChatContextQueryError.self)
|> castError(ChatContextQueryError.self)
return signal |> then(participants)
case let .command(query):
@ -207,7 +207,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee
let sortedCommands = filteredCommands
return { _ in return .commands(sortedCommands) }
}
|> introduceError(ChatContextQueryError.self)
|> castError(ChatContextQueryError.self)
return signal |> then(commands)
case let .contextRequest(addressName, query):
var delayRequest = true
@ -239,7 +239,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee
return .single(nil)
}
}
|> introduceError(ChatContextQueryError.self)
|> castError(ChatContextQueryError.self)
|> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> in
if let user = peer as? TelegramUser, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder {
let contextResults = requestChatContextResults(account: context.account, botId: user.id, peerId: chatPeer.id, query: query, offset: "")
@ -307,7 +307,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee
|> map { result -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
return { _ in return .emojis(result, range) }
}
|> introduceError(ChatContextQueryError.self)
|> castError(ChatContextQueryError.self)
}
}

View File

@ -378,7 +378,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
}
createSignal = addressPromise.get()
|> introduceError(CreateGroupError.self)
|> castError(CreateGroupError.self)
|> mapToSignal { address -> Signal<PeerId?, CreateGroupError> in
guard let address = address else {
return .complete()

View File

@ -291,7 +291,7 @@ func fetchEmojiSpriteResource(postbox: Postbox, network: Network, resource: Emoj
let packName = "P\(resource.packId)_by_AEStickerBot"
return loadedStickerPack(postbox: postbox, network: network, reference: .name(packName), forceActualized: false)
|> introduceError(MediaResourceDataFetchError.self)
|> castError(MediaResourceDataFetchError.self)
|> mapToSignal { result -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> in
switch result {
case let .result(_, items, _):

View File

@ -218,7 +218,7 @@ private final class FetchManagerCategoryContext {
|> mapToSignal { type -> Signal<FetchResourceSourceType, FetchResourceError> in
if let storeManager = storeManager, let mediaReference = entry.mediaReference, case .remote = type, let peerType = entry.storeToDownloadsPeerType {
return storeDownloadedMedia(storeManager: storeManager, media: mediaReference, peerType: peerType)
|> introduceError(FetchResourceError.self)
|> castError(FetchResourceError.self)
|> mapToSignal { _ -> Signal<FetchResourceSourceType, FetchResourceError> in
return .complete()
}
@ -347,7 +347,7 @@ private final class FetchManagerCategoryContext {
activeContext.disposable?.dispose()
if isVideoPreload {
activeContext.disposable = (preloadVideoResource(postbox: self.postbox, resourceReference: entry.resourceReference, duration: 4.0)
|> introduceError(FetchResourceError.self)
|> castError(FetchResourceError.self)
|> map { _ -> FetchResourceSourceType in return .local }
|> then(.single(.local))
|> deliverOnMainQueue).start(next: { _ in
@ -359,7 +359,7 @@ private final class FetchManagerCategoryContext {
|> mapToSignal { type -> Signal<FetchResourceSourceType, FetchResourceError> in
if let storeManager = storeManager, let mediaReference = entry.mediaReference, case .remote = type, let peerType = entry.storeToDownloadsPeerType {
return storeDownloadedMedia(storeManager: storeManager, media: mediaReference, peerType: peerType)
|> introduceError(FetchResourceError.self)
|> castError(FetchResourceError.self)
|> mapToSignal { _ -> Signal<FetchResourceSourceType, FetchResourceError> in
return .complete()
}

View File

@ -224,7 +224,7 @@ public func fetchVideoLibraryMediaResource(postbox: Postbox, resource: VideoLibr
|> map { view in
return view.values[PreferencesKeys.appConfiguration] as? AppConfiguration ?? .defaultValue
}
|> introduceError(MediaResourceDataFetchError.self)
|> castError(MediaResourceDataFetchError.self)
|> mapToSignal { appConfiguration -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> in
let config = VideoConversionConfiguration.with(appConfiguration: appConfiguration)
let signal = Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> { subscriber in
@ -343,7 +343,7 @@ func fetchLocalFileVideoMediaResource(postbox: Postbox, resource: LocalFileVideo
|> map { view in
return view.values[PreferencesKeys.appConfiguration] as? AppConfiguration ?? .defaultValue
}
|> introduceError(MediaResourceDataFetchError.self)
|> castError(MediaResourceDataFetchError.self)
|> mapToSignal { appConfiguration -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> in
let config = VideoConversionConfiguration.with(appConfiguration: appConfiguration)
let signal = Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> { subscriber in

View File

@ -179,7 +179,7 @@ public class ShareRootControllerImpl {
let account: Signal<(SharedAccountContextImpl, Account, [AccountWithInfo]), ShareAuthorizationError> = internalContext.sharedContext.accountManager.transaction { transaction -> (SharedAccountContextImpl, LoggingSettings) in
return (internalContext.sharedContext, transaction.getSharedData(SharedDataKeys.loggingSettings) as? LoggingSettings ?? LoggingSettings.defaultSettings)
}
|> introduceError(ShareAuthorizationError.self)
|> castError(ShareAuthorizationError.self)
|> mapToSignal { sharedContext, loggingSettings -> Signal<(SharedAccountContextImpl, Account, [AccountWithInfo]), ShareAuthorizationError> in
Logger.shared.logToFile = loggingSettings.logToFile
Logger.shared.logToConsole = loggingSettings.logToConsole
@ -187,7 +187,7 @@ public class ShareRootControllerImpl {
Logger.shared.redactSensitiveData = loggingSettings.redactSensitiveData
return sharedContext.activeAccountsWithInfo
|> introduceError(ShareAuthorizationError.self)
|> castError(ShareAuthorizationError.self)
|> take(1)
|> mapToSignal { primary, accounts -> Signal<(SharedAccountContextImpl, Account, [AccountWithInfo]), ShareAuthorizationError> in
guard let primary = primary else {
@ -209,7 +209,7 @@ public class ShareRootControllerImpl {
return combineLatest(sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationPasscodeSettings]), limitsConfiguration, sharedContext.accountManager.accessChallengeData())
|> take(1)
|> deliverOnMainQueue
|> introduceError(ShareAuthorizationError.self)
|> castError(ShareAuthorizationError.self)
|> map { sharedData, limitsConfiguration, data -> (AccountContext, PostboxAccessChallengeData, [AccountWithInfo]) in
updateLegacyLocalization(strings: sharedContext.currentPresentationData.with({ $0 }).strings)
let context = AccountContextImpl(sharedContext: sharedContext, account: account, tonContext: nil, limitsConfiguration: limitsConfiguration)

View File

@ -24,7 +24,7 @@ func preloadVideoResource(postbox: Postbox, resourceReference: MediaResourceRefe
if let videoBuffer = result?.buffers.videoBuffer, let impl = source.syncWith({ $0 }) {
return impl.ensureHasFrames(until: min(duration, videoBuffer.duration.seconds))
|> ignoreValues
|> introduceError(MediaFrameSourceSeekError.self)
|> castError(MediaFrameSourceSeekError.self)
} else {
return .complete()
}

View File

@ -119,7 +119,9 @@ final class WalletInfoEmptyItemNode: ListViewItemNode {
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: text, font: Font.regular(16.0), textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - sideInset * 2.0, height: .greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.1, cutout: nil, insets: UIEdgeInsets()))
let (addressLayout, addressApply) = makeAddressLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.address, font: Font.monospace(16.0), textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - sideInset * 2.0, height: .greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.1, cutout: nil, insets: UIEdgeInsets()))
var addressString = item.address
addressString.insert("\n", at: addressString.index(addressString.startIndex, offsetBy: addressString.count / 2))
let (addressLayout, addressApply) = makeAddressLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: addressString, font: Font.monospace(16.0), textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - sideInset * 2.0, height: .greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.1, cutout: nil, insets: UIEdgeInsets()))
let contentVerticalOrigin: CGFloat = 32.0

View File

@ -11,6 +11,59 @@ import SolidRoundedButtonNode
import AnimationUI
import SwiftSignalKit
import MergeLists
import TelegramStringFormatting
private func stringForRelativeUpdateTime(strings: PresentationStrings, day: RelativeTimestampFormatDay, dateTimeFormat: PresentationDateTimeFormat, hours: Int32, minutes: Int32) -> String {
let dayString: String
switch day {
case .today:
dayString = strings.Updated_TodayAt(stringForShortTimestamp(hours: hours, minutes: minutes, dateTimeFormat: dateTimeFormat)).0
case .yesterday:
dayString = strings.Updated_YesterdayAt(stringForShortTimestamp(hours: hours, minutes: minutes, dateTimeFormat: dateTimeFormat)).0
}
return dayString
}
private func lastUpdateTimestampString(strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, statusTimestamp: Int32, relativeTo timestamp: Int32) -> String {
let difference = timestamp - statusTimestamp
let expanded = true
if difference < 60 {
return strings.Updated_JustNow
} else if difference < 60 * 60 && !expanded {
let minutes = difference / 60
return strings.Updated_MinutesAgo(minutes)
} else {
var t: time_t = time_t(statusTimestamp)
var timeinfo: tm = tm()
localtime_r(&t, &timeinfo)
var now: time_t = time_t(timestamp)
var timeinfoNow: tm = tm()
localtime_r(&now, &timeinfoNow)
if timeinfo.tm_year != timeinfoNow.tm_year {
return strings.Updated_AtDate(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year, dateTimeFormat: dateTimeFormat)).0
}
let dayDifference = timeinfo.tm_yday - timeinfoNow.tm_yday
if dayDifference == 0 || dayDifference == -1 {
let day: RelativeTimestampFormatDay
if dayDifference == 0 {
if expanded {
day = .today
} else {
let minutes = difference / (60 * 60)
return strings.Updated_HoursAgo(minutes)
}
} else {
day = .yesterday
}
return stringForRelativeUpdateTime(strings: strings, day: day, dateTimeFormat: dateTimeFormat, hours: timeinfo.tm_hour, minutes: timeinfo.tm_min)
} else {
return strings.Updated_AtDate(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year, dateTimeFormat: dateTimeFormat)).0
}
}
}
public final class WalletInfoScreen: ViewController {
private let context: AccountContext
@ -20,6 +73,11 @@ public final class WalletInfoScreen: ViewController {
private var presentationData: PresentationData
private let _ready = Promise<Bool>()
override public var ready: Promise<Bool> {
return self._ready
}
public init(context: AccountContext, tonContext: TonContext, walletInfo: WalletInfo, address: String) {
self.context = context
self.tonContext = tonContext
@ -77,6 +135,8 @@ public final class WalletInfoScreen: ViewController {
})
self.displayNodeDidLoad()
self._ready.set((self.displayNode as! WalletInfoScreenNode).contentReady.get())
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
@ -126,11 +186,7 @@ private final class WalletInfoBalanceNode: ASDisplayNode {
let balanceTextFrame = CGRect(origin: balanceOrigin, size: balanceTextSize)
let balanceIconFrame: CGRect
if self.isLoading {
balanceIconFrame = CGRect(origin: CGPoint(x: floor((width - balanceIconSize.width) / 2.0), y: balanceTextFrame.minY + floor((balanceTextFrame.height - balanceIconSize.height) / 2.0)), size: balanceIconSize)
} else {
balanceIconFrame = CGRect(origin: CGPoint(x: balanceTextFrame.maxX + balanceIconSpacing, y: balanceTextFrame.minY + floor((balanceTextFrame.height - balanceIconSize.height) / 2.0)), size: balanceIconSize)
}
balanceIconFrame = CGRect(origin: CGPoint(x: balanceTextFrame.maxX + balanceIconSpacing, y: balanceTextFrame.minY + floor((balanceTextFrame.height - balanceIconSize.height) / 2.0)), size: balanceIconSize)
transition.updateFrameAdditive(node: self.balanceTextNode, frame: balanceTextFrame)
transition.updateFrame(node: self.balanceIconNode, frame: balanceIconFrame)
@ -140,20 +196,33 @@ private final class WalletInfoBalanceNode: ASDisplayNode {
private final class WalletInfoHeaderNode: ASDisplayNode {
var balance: Int64?
var isRefreshing: Bool = false
var timestampString: String = "" {
didSet {
self.balanceTimestampNode.attributedText = NSAttributedString(string: self.timestampString, font: Font.regular(13), textColor: UIColor(white: 1.0, alpha: 0.6))
}
}
let balanceNode: WalletInfoBalanceNode
private let refreshNode: AnimatedStickerNode
private let balanceSubtitleNode: ImmediateTextNode
private let balanceTimestampNode: ImmediateTextNode
private let receiveButtonNode: SolidRoundedButtonNode
private let sendButtonNode: SolidRoundedButtonNode
private let headerBackgroundNode: ASImageNode
init(theme: PresentationTheme, sendAction: @escaping () -> Void, receiveAction: @escaping () -> Void) {
init(account: Account, theme: PresentationTheme, sendAction: @escaping () -> Void, receiveAction: @escaping () -> Void) {
self.balanceNode = WalletInfoBalanceNode(theme: theme)
self.balanceSubtitleNode = ImmediateTextNode()
self.balanceSubtitleNode.displaysAsynchronously = false
self.balanceSubtitleNode.attributedText = NSAttributedString(string: "your balance", font: Font.regular(13), textColor: UIColor(white: 1.0, alpha: 0.6))
self.balanceTimestampNode = ImmediateTextNode()
self.balanceTimestampNode.displaysAsynchronously = false
self.balanceTimestampNode.attributedText = NSAttributedString(string: "", font: Font.regular(13), textColor: UIColor(white: 1.0, alpha: 0.6))
self.headerBackgroundNode = ASImageNode()
self.headerBackgroundNode.displaysAsynchronously = false
self.headerBackgroundNode.displayWithoutProcessing = true
@ -168,6 +237,13 @@ private final class WalletInfoHeaderNode: ASDisplayNode {
self.receiveButtonNode = SolidRoundedButtonNode(title: "Receive", icon: UIImage(bundleImageName: "Wallet/ReceiveButtonIcon"), theme: SolidRoundedButtonTheme(backgroundColor: .white, foregroundColor: .black), height: 50.0, cornerRadius: 10.0, gloss: false)
self.sendButtonNode = SolidRoundedButtonNode(title: "Send", icon: UIImage(bundleImageName: "Wallet/SendButtonIcon"), theme: SolidRoundedButtonTheme(backgroundColor: .white, foregroundColor: .black), height: 50.0, cornerRadius: 10.0, gloss: false)
self.refreshNode = AnimatedStickerNode()
self.refreshNode.playToCompletionOnStop = true
self.refreshNode.automaticallyLoadFirstFrame = true
if let path = getAppBundle().path(forResource: "celebrate", ofType: "tgs") {
self.refreshNode.setup(account: account, resource: .localFile(path), width: Int(32.0 * UIScreenScale), height: Int(32.0 * UIScreenScale), mode: .direct)
}
super.init()
self.addSubnode(self.headerBackgroundNode)
@ -175,6 +251,8 @@ private final class WalletInfoHeaderNode: ASDisplayNode {
self.addSubnode(self.sendButtonNode)
self.addSubnode(self.balanceNode)
self.addSubnode(self.balanceSubtitleNode)
self.addSubnode(self.balanceTimestampNode)
self.addSubnode(self.refreshNode)
self.receiveButtonNode.pressed = {
receiveAction()
@ -206,11 +284,13 @@ private final class WalletInfoHeaderNode: ASDisplayNode {
let buttonAlpha = buttonTransition * 1.0
let balanceSubtitleSize = self.balanceSubtitleNode.updateLayout(CGSize(width: size.width - sideInset * 2.0, height: 200.0))
let balanceTimestampSize = self.balanceTimestampNode.updateLayout(CGSize(width: size.width - sideInset * 2.0, height: 200.0))
let balanceHeight = self.balanceNode.update(width: size.width, transition: transition)
let balanceSize = CGSize(width: size.width, height: balanceHeight)
let minHeaderScale: CGFloat = 0.435
let maxHeaderScale: CGFloat = min(1.0, (size.width - 40.0) / balanceSize.width)
let minHeaderScale: CGFloat = min(0.435, (size.width - 80.0 * 2.0) / balanceSize.width)
let minHeaderHeight: CGFloat = balanceSize.height + balanceSubtitleSize.height + balanceSubtitleSpacing
@ -219,13 +299,39 @@ private final class WalletInfoHeaderNode: ASDisplayNode {
let headerScaleTransition: CGFloat = max(0.0, min(1.0, (effectiveOffset - minHeaderOffset) / (maxHeaderOffset - minHeaderOffset)))
let headerPositionTransition: CGFloat = max(0.0, (effectiveOffset - minHeaderOffset) / (maxOffset - minHeaderOffset))
let headerY = headerPositionTransition * maxHeaderY + (1.0 - headerPositionTransition) * minHeaderY
let headerScale = headerScaleTransition * 1.0 + (1.0 - headerScaleTransition) * minHeaderScale
let headerScale = headerScaleTransition * maxHeaderScale + (1.0 - headerScaleTransition) * minHeaderScale
let refreshSize = CGSize(width: 32.0, height: 32.0)
self.refreshNode.updateLayout(size: refreshSize)
transition.updateFrame(node: self.refreshNode, frame: CGRect(origin: CGPoint(x: floor((size.width - refreshSize.width) / 2.0), y: navigationHeight - 44.0 + floor((44.0 - refreshSize.height) / 2.0)), size: refreshSize))
if self.balance == nil {
transition.updateAlpha(node: self.refreshNode, alpha: 0.0)
transition.updateSublayerTransformScale(node: self.refreshNode, scale: 0.1)
self.refreshNode.visibility = false
} else if self.isRefreshing {
transition.updateAlpha(node: self.refreshNode, alpha: 1.0)
transition.updateSublayerTransformScale(node: self.refreshNode, scale: 1.0)
self.refreshNode.visibility = true
} else {
let refreshOffset: CGFloat = 20.0
let refreshScaleTransition: CGFloat = max(0.0, min(1.0, (offset - maxOffset) / refreshOffset))
transition.updateAlpha(node: self.refreshNode, alpha: refreshScaleTransition)
let refreshScale: CGFloat = refreshScaleTransition * 1.0 + (1.0 - refreshScaleTransition) * 0.1
transition.updateSublayerTransformScale(node: self.refreshNode, scale: refreshScale)
self.refreshNode.visibility = false
}
let balanceFrame = CGRect(origin: CGPoint(x: 0.0, y: headerY), size: balanceSize)
transition.updateFrame(node: self.balanceNode, frame: balanceFrame)
transition.updateSublayerTransformScale(node: self.balanceNode, scale: headerScale)
transition.updateFrameAdditive(node: self.balanceSubtitleNode, frame: CGRect(origin: CGPoint(x: floor((size.width - balanceSubtitleSize.width) / 2.0), y: balanceFrame.midY + (balanceFrame.height / 2.0 * headerScale) + balanceSubtitleSpacing), size: balanceSubtitleSize))
let balanceSubtitleFrame = CGRect(origin: CGPoint(x: floor((size.width - balanceSubtitleSize.width) / 2.0), y: balanceFrame.midY + (balanceFrame.height / 2.0 * headerScale) + balanceSubtitleSpacing), size: balanceSubtitleSize)
transition.updateFrameAdditive(node: self.balanceSubtitleNode, frame: balanceSubtitleFrame)
let balanceTimestampFrame = CGRect(origin: CGPoint(x: floor((size.width - balanceTimestampSize.width) / 2.0), y: balanceSubtitleFrame.maxY + 2.0), size: balanceTimestampSize)
transition.updateFrameAdditive(node: self.balanceTimestampNode, frame: balanceTimestampFrame)
transition.updateAlpha(node: self.balanceTimestampNode, alpha: headerScaleTransition)
let headerHeight: CGFloat = 1000.0
transition.updateFrame(node: self.headerBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: effectiveOffset + 10.0 - headerHeight), size: CGSize(width: size.width, height: headerHeight)))
@ -252,11 +358,13 @@ private final class WalletInfoHeaderNode: ASDisplayNode {
}
}
if self.balance == nil {
self.balanceNode.balanceTextNode.isHidden = true
self.balanceNode.isHidden = true
self.balanceSubtitleNode.isHidden = true
self.balanceTimestampNode.isHidden = true
} else {
self.balanceNode.balanceTextNode.isHidden = false
self.balanceNode.isHidden = false
self.balanceSubtitleNode.isHidden = false
self.balanceTimestampNode.isHidden = false
}
transition.updateFrame(node: self.receiveButtonNode, frame: receiveButtonFrame)
transition.updateAlpha(node: self.receiveButtonNode, alpha: buttonAlpha)
@ -276,12 +384,19 @@ private final class WalletInfoHeaderNode: ASDisplayNode {
return nil
}
func animateIn() {
self.sendButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.receiveButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
func becameReady(animated: Bool) {
if animated {
self.sendButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.receiveButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.balanceNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.balanceSubtitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.balanceTimestampNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
self.balanceNode.isLoading = false
self.balanceNode.balanceTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.balanceSubtitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
func animateBeganRefreshing() {
//self.refreshNode.layer.animate(from: 0.5 as NSNumber, to: 0.0 as NSNumber, keyPath: "transform.scale", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.2, delay: 0.0, removeOnCompletion: true, additive: true)
}
}
@ -358,24 +473,34 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
private let address: String
private let openTransaction: (WalletTransaction) -> Void
private let hapticFeedback = HapticFeedback()
private let headerNode: WalletInfoHeaderNode
private let listNode: ListView
private let loadingIndicator: UIActivityIndicatorView
private var enqueuedTransactions: [WalletInfoListTransaction] = []
private var validLayout: (ContainerViewLayout, CGFloat)?
private let balanceDisposable = MetaDisposable()
private let stateDisposable = MetaDisposable()
private let transactionListDisposable = MetaDisposable()
private var listOffset: CGFloat?
private var reloadingState: Bool = false
private var loadingMoreTransactions: Bool = false
private var canLoadMoreTransactions: Bool = true
private var combinedState: CombinedWalletState?
private var currentEntries: [WalletInfoListEntry]?
private var isReady: Bool = false
let contentReady = Promise<Bool>()
private var didSetContentReady = false
private var updateTimestampTimer: SwiftSignalKit.Timer?
init(account: Account, tonContext: TonContext, presentationData: PresentationData, walletInfo: WalletInfo, address: String, sendAction: @escaping () -> Void, receiveAction: @escaping () -> Void, openTransaction: @escaping (WalletTransaction) -> Void) {
self.account = account
self.tonContext = tonContext
@ -384,47 +509,50 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
self.address = address
self.openTransaction = openTransaction
self.headerNode = WalletInfoHeaderNode(theme: presentationData.theme, sendAction: sendAction, receiveAction: receiveAction)
self.headerNode = WalletInfoHeaderNode(account: account, theme: presentationData.theme, sendAction: sendAction, receiveAction: receiveAction)
self.listNode = ListView()
self.listNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor
self.listNode.verticalScrollIndicatorFollowsOverscroll = true
self.listNode.isHidden = true
self.loadingIndicator = UIActivityIndicatorView(style: .whiteLarge)
super.init()
self.backgroundColor = .white
self.balanceDisposable.set((getWalletState(address: address, tonInstance: tonContext.instance)
|> deliverOnMainQueue).start(next: { [weak self] value in
guard let strongSelf = self else {
return
}
strongSelf.headerNode.balanceNode.balance = formatBalanceText(max(0, value.balance))
strongSelf.headerNode.balance = max(0, value.balance)
if let (layout, navigationHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate)
}
let wasReady = strongSelf.isReady
strongSelf.isReady = strongSelf.headerNode.balance != nil && strongSelf.currentEntries != nil
if strongSelf.isReady && !wasReady {
strongSelf.animateReadyIn()
}
}))
self.addSubnode(self.listNode)
self.addSubnode(self.headerNode)
self.view.addSubview(self.loadingIndicator)
var canBeginRefresh = true
var isScrolling = false
self.listNode.beganInteractiveDragging = {
isScrolling = true
}
self.listNode.endedInteractiveDragging = {
isScrolling = false
}
self.listNode.updateFloatingHeaderOffset = { [weak self] offset, listTransition in
guard let strongSelf = self, let (layout, navigationHeight) = strongSelf.validLayout else {
return
}
let headerHeight: CGFloat = navigationHeight + 260.0
strongSelf.listOffset = offset
if strongSelf.isReady {
if !strongSelf.reloadingState && canBeginRefresh && isScrolling {
if offset >= headerHeight + 100.0 {
canBeginRefresh = false
strongSelf.headerNode.isRefreshing = true
strongSelf.headerNode.animateBeganRefreshing()
strongSelf.hapticFeedback.impact()
strongSelf.refreshTransactions()
}
}
strongSelf.headerNode.update(size: strongSelf.headerNode.bounds.size, navigationHeight: navigationHeight, offset: offset, transition: listTransition)
}
}
@ -436,12 +564,14 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
guard case let .known(value) = offset, value < 100.0 else {
return
}
if !strongSelf.loadingMoreTransactions && strongSelf.canLoadMoreTransactions {
if !strongSelf.loadingMoreTransactions && !strongSelf.reloadingState && strongSelf.canLoadMoreTransactions {
strongSelf.loadMoreTransactions()
}
}
self.listNode.didEndScrolling = { [weak self] in
canBeginRefresh = true
guard let strongSelf = self, let (_, navigationHeight) = strongSelf.validLayout else {
return
}
@ -460,6 +590,26 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
}
self.refreshTransactions()
self.updateTimestampTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in
guard let strongSelf = self, let combinedState = strongSelf.combinedState, !strongSelf.reloadingState else {
return
}
let string = lastUpdateTimestampString(strings: strongSelf.presentationData.strings, dateTimeFormat: strongSelf.presentationData.dateTimeFormat, statusTimestamp: Int32(clamping: combinedState.timestamp), relativeTo: Int32(Date().timeIntervalSince1970))
if strongSelf.headerNode.timestampString != string {
strongSelf.headerNode.timestampString = string
if let (layout, navigationHeight) = strongSelf.validLayout {
strongSelf.headerNode.update(size: strongSelf.headerNode.bounds.size, navigationHeight: navigationHeight, offset: strongSelf.listOffset ?? 0.0, transition: .immediate)
}
}
}, queue: .mainQueue())
self.updateTimestampTimer?.start()
}
deinit {
self.stateDisposable.dispose()
self.transactionListDisposable.dispose()
self.updateTimestampTimer?.invalidate()
}
func scrollToHideHeader() {
@ -477,6 +627,9 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
let isFirstLayout = self.validLayout == nil
self.validLayout = (layout, navigationHeight)
let indicatorSize = self.loadingIndicator.bounds.size
self.loadingIndicator.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - indicatorSize.width) / 2.0), y: floor((layout.size.height - indicatorSize.height) / 2.0)), size: indicatorSize)
let headerHeight: CGFloat = navigationHeight + 260.0
let topInset: CGFloat = headerHeight
@ -531,13 +684,73 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
private func refreshTransactions() {
self.transactionListDisposable.set(nil)
self.loadingMoreTransactions = true
self.reloadingState = true
self.transactionListDisposable.set((getWalletTransactions(address: self.address, previousId: nil, tonInstance: self.tonContext.instance)
|> deliverOnMainQueue).start(next: { [weak self] transactions in
self.headerNode.timestampString = "updating"
self.stateDisposable.set((getCombinedWalletState(postbox: self.account.postbox, walletInfo: self.walletInfo, tonInstance: self.tonContext.instance)
|> delay(self.combinedState == nil ? 0.0 : 2.0, queue: .mainQueue())
|> deliverOnMainQueue).start(next: { [weak self] value in
guard let strongSelf = self else {
return
}
strongSelf.transactionsLoaded(isReload: true, transactions: transactions)
let combinedState: CombinedWalletState?
switch value {
case let .cached(state):
if state == nil {
strongSelf.loadingIndicator.startAnimating()
} else {
strongSelf.loadingIndicator.stopAnimating()
strongSelf.loadingIndicator.isHidden = true
}
combinedState = state
case let .updated(state):
strongSelf.loadingIndicator.stopAnimating()
strongSelf.loadingIndicator.isHidden = true
combinedState = state
}
strongSelf.combinedState = combinedState
if let combinedState = combinedState {
strongSelf.headerNode.balanceNode.balance = formatBalanceText(max(0, combinedState.walletState.balance))
strongSelf.headerNode.balance = max(0, combinedState.walletState.balance)
if strongSelf.isReady, let (layout, navigationHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate)
}
strongSelf.reloadingState = false
strongSelf.headerNode.timestampString = lastUpdateTimestampString(strings: strongSelf.presentationData.strings, dateTimeFormat: strongSelf.presentationData.dateTimeFormat, statusTimestamp: Int32(clamping: combinedState.timestamp), relativeTo: Int32(Date().timeIntervalSince1970))
if strongSelf.isReady, let (layout, navigationHeight) = strongSelf.validLayout {
strongSelf.headerNode.update(size: strongSelf.headerNode.bounds.size, navigationHeight: navigationHeight, offset: strongSelf.listOffset ?? 0.0, transition: .immediate)
}
strongSelf.transactionsLoaded(isReload: true, transactions: combinedState.topTransactions)
strongSelf.headerNode.isRefreshing = false
if strongSelf.isReady, let (layout, navigationHeight) = strongSelf.validLayout {
strongSelf.headerNode.update(size: strongSelf.headerNode.bounds.size, navigationHeight: navigationHeight, offset: strongSelf.listOffset ?? 0.0, transition: .animated(duration: 0.2, curve: .easeInOut))
}
let wasReady = strongSelf.isReady
strongSelf.isReady = strongSelf.combinedState != nil
if strongSelf.isReady && !wasReady {
if let (layout, navigationHeight) = strongSelf.validLayout {
strongSelf.headerNode.update(size: strongSelf.headerNode.bounds.size, navigationHeight: navigationHeight, offset: layout.size.height, transition: .immediate)
}
strongSelf.becameReady(animated: strongSelf.didSetContentReady)
}
}
if !strongSelf.didSetContentReady {
strongSelf.didSetContentReady = true
strongSelf.contentReady.set(.single(true))
}
}, error: { [weak self] _ in
guard let strongSelf = self else {
return
@ -576,12 +789,14 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
self.loadingMoreTransactions = false
self.canLoadMoreTransactions = transactions.count > 2
let isFirst = self.currentEntries == nil
var updatedEntries: [WalletInfoListEntry] = []
if isReload {
var existingIds = Set<WalletTransactionId>()
for transaction in transactions {
updatedEntries.append(.transaction(updatedEntries.count, transaction))
if !existingIds.contains(transaction.transactionId) {
existingIds.insert(transaction.transactionId)
updatedEntries.append(.transaction(updatedEntries.count, transaction))
}
}
if updatedEntries.isEmpty {
updatedEntries.append(.empty(self.address))
@ -625,13 +840,6 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
self.enqueuedTransactions.append(transaction)
self.dequeueTransaction()
let wasReady = self.isReady
self.isReady = self.headerNode.balance != nil && self.currentEntries != nil
if self.isReady && !wasReady {
self.animateReadyIn()
}
}
private func dequeueTransaction() {
@ -650,11 +858,13 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
})
}
private func animateReadyIn() {
private func becameReady(animated: Bool) {
self.listNode.isHidden = false
self.headerNode.animateIn()
self.loadingIndicator.stopAnimating()
self.loadingIndicator.isHidden = true
self.headerNode.becameReady(animated: animated)
if let (layout, navigationHeight) = self.validLayout {
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.5, curve: .spring))
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: animated ? .animated(duration: 0.5, curve: .spring) : .immediate)
}
}
}

View File

@ -80,9 +80,16 @@ class WalletInfoTransactionItem: ListViewItem {
private let titleFont = Font.medium(17.0)
private let textFont = Font.monospace(15.0)
private let descriptionFont = Font.regular(15.0)
private let dateFont = Font.regular(14.0)
private let directionFont = Font.regular(15.0)
private func formatAddress(_ address: String) -> String {
var address = address
address.insert("\n", at: address.index(address.startIndex, offsetBy: address.count / 2))
return address
}
class WalletInfoTransactionItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
@ -93,6 +100,7 @@ class WalletInfoTransactionItemNode: ListViewItemNode {
private let directionNode: TextNode
private let iconNode: ASImageNode
private let textNode: TextNode
private let descriptionNode: TextNode
private let dateNode: TextNode
private let activateArea: AccessibilityAreaNode
@ -127,6 +135,11 @@ class WalletInfoTransactionItemNode: ListViewItemNode {
self.textNode.contentMode = .left
self.textNode.contentsScale = UIScreen.main.scale
self.descriptionNode = TextNode()
self.descriptionNode.isUserInteractionEnabled = false
self.descriptionNode.contentMode = .left
self.descriptionNode.contentsScale = UIScreen.main.scale
self.dateNode = TextNode()
self.dateNode.isUserInteractionEnabled = false
self.dateNode.contentMode = .left
@ -143,6 +156,7 @@ class WalletInfoTransactionItemNode: ListViewItemNode {
self.addSubnode(self.iconNode)
self.addSubnode(self.directionNode)
self.addSubnode(self.textNode)
self.addSubnode(self.descriptionNode)
self.addSubnode(self.dateNode)
self.addSubnode(self.activateArea)
@ -152,6 +166,7 @@ class WalletInfoTransactionItemNode: ListViewItemNode {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeDirectionLayout = TextNode.asyncLayout(self.directionNode)
let makeTextLayout = TextNode.asyncLayout(self.textNode)
let makeDescriptionLayout = TextNode.asyncLayout(self.descriptionNode)
let makeDateLayout = TextNode.asyncLayout(self.dateNode)
let currentItem = self.item
@ -172,6 +187,7 @@ class WalletInfoTransactionItemNode: ListViewItemNode {
let titleColor: UIColor
let transferredValue = item.walletTransaction.transferredValue
var text: String = ""
var description: String = ""
if transferredValue <= 0 {
title = "\(formatBalanceText(transferredValue))"
titleColor = item.theme.list.itemPrimaryTextColor
@ -184,7 +200,12 @@ class WalletInfoTransactionItemNode: ListViewItemNode {
if !text.isEmpty {
text.append("\n")
}
text.append(message.destination)
text.append(formatAddress(message.destination))
if !description.isEmpty {
description.append("\n")
}
description.append(message.textMessage)
}
}
} else {
@ -192,7 +213,8 @@ class WalletInfoTransactionItemNode: ListViewItemNode {
titleColor = item.theme.chatList.secretTitleColor
directionText = "from"
if let inMessage = item.walletTransaction.inMessage {
text = inMessage.source
text = formatAddress(inMessage.source)
description = inMessage.textMessage
} else {
text = "<unknown>"
}
@ -208,7 +230,9 @@ class WalletInfoTransactionItemNode: ListViewItemNode {
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: text, font: textFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - leftInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let contentSize: CGSize
let (descriptionLayout, descriptionApply) = makeDescriptionLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: description, font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - leftInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
var contentSize: CGSize
var insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
@ -221,8 +245,12 @@ class WalletInfoTransactionItemNode: ListViewItemNode {
let topInset: CGFloat = 11.0
let bottomInset: CGFloat = 11.0
let titleSpacing: CGFloat = 2.0
let textSpacing: CGFloat = 2.0
contentSize = CGSize(width: params.width, height: topInset + titleLayout.size.height + titleSpacing + textLayout.size.height + bottomInset)
if !descriptionLayout.size.width.isZero {
contentSize.height += descriptionLayout.size.height + textSpacing
}
insets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
var topHighlightInset: CGFloat = 0.0
if dateHeaderAtBottom, let header = item.header {
@ -249,6 +277,7 @@ class WalletInfoTransactionItemNode: ListViewItemNode {
let _ = titleApply()
let _ = textApply()
let _ = descriptionApply()
let _ = dateApply()
let _ = directionApply()
@ -272,7 +301,11 @@ class WalletInfoTransactionItemNode: ListViewItemNode {
let directionFrame = CGRect(origin: CGPoint(x: iconFrame.maxX + 3.0, y: titleFrame.maxY - directionLayout.size.height - 1.0), size: directionLayout.size)
strongSelf.directionNode.frame = directionFrame
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: textLayout.size)
let textFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: textLayout.size)
strongSelf.textNode.frame = textFrame
strongSelf.descriptionNode.frame = CGRect(origin: CGPoint(x: leftInset, y: textFrame.maxY + textSpacing), size: descriptionLayout.size)
strongSelf.dateNode.frame = CGRect(origin: CGPoint(x: params.width - leftInset - dateLayout.size.width, y: topInset), size: dateLayout.size)
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: topHighlightInset + -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel * 2.0 - topHighlightInset))

View File

@ -12,6 +12,12 @@ import AnimationUI
import SwiftSignalKit
import OverlayStatusController
import ItemListUI
import TextFormat
public enum WalletSecureStorageResetReason {
case notAvailable
case changed
}
public enum WalletSplashMode {
case intro
@ -20,6 +26,8 @@ public enum WalletSplashMode {
case restoreFailed
case sending(WalletInfo, String, Int64, String)
case sent(WalletInfo, Int64)
case secureStorageNotAvailable
case secureStorageReset(WalletSecureStorageResetReason)
}
public final class WalletSplashScreen: ViewController {
@ -75,7 +83,7 @@ public final class WalletSplashScreen: ViewController {
})
case .sent:
self.navigationItem.setLeftBarButton(UIBarButtonItem(customDisplayNode: ASDisplayNode())!, animated: false)
case .created, .success, .restoreFailed:
case .created, .success, .restoreFailed, .secureStorageNotAvailable, .secureStorageReset:
break
}
@ -110,6 +118,15 @@ public final class WalletSplashScreen: ViewController {
}
controller.dismiss()
(strongSelf.navigationController as? NavigationController)?.replaceController(strongSelf, with: WalletSplashScreen(context: strongSelf.context, tonContext: strongSelf.tonContext, mode: .created(walletInfo, wordList)), animated: true)
}, error: { _ in
guard let strongSelf = self else {
return
}
controller.dismiss()
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: "An Error Occurred", text: "Sorry. Please try again.", actions: [
TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {
})
], actionLayout: .vertical), in: .window(.root))
})
case let .created(walletInfo, wordList):
strongSelf.push(WalletWordDisplayScreen(context: strongSelf.context, tonContext: strongSelf.tonContext, walletInfo: walletInfo, wordList: wordList))
@ -159,12 +176,43 @@ public final class WalletSplashScreen: ViewController {
}
case .sending:
break
case .secureStorageNotAvailable:
strongSelf.dismiss()
case .secureStorageReset:
strongSelf.push(WalletWordCheckScreen(context: strongSelf.context, tonContext: strongSelf.tonContext, mode: .import))
}
}, secondaryAction: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.push(WalletWordCheckScreen(context: strongSelf.context, tonContext: strongSelf.tonContext, mode: .import))
switch strongSelf.mode {
case .secureStorageNotAvailable, .secureStorageReset:
if let navigationController = strongSelf.navigationController as? NavigationController {
var controllers = navigationController.viewControllers
controllers = controllers.filter { controller in
if controller is WalletSplashScreen {
return false
}
if controller is WalletWordDisplayScreen {
return false
}
if controller is WalletWordCheckScreen {
return false
}
return true
}
controllers.append(WalletSplashScreen(context: strongSelf.context, tonContext: strongSelf.tonContext, mode: .intro))
strongSelf.view.endEditing(true)
navigationController.setViewControllers(controllers, animated: true)
}
default:
strongSelf.push(WalletWordCheckScreen(context: strongSelf.context, tonContext: strongSelf.tonContext, mode: .import))
}
}, openTerms: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: "https://telegram.org", forceExternal: true, presentationData: strongSelf.presentationData, navigationController: strongSelf.navigationController as? NavigationController, dismissInput: {})
})
self.displayNodeDidLoad()
@ -198,7 +246,7 @@ private final class WalletSplashScreenNode: ViewControllerTracingNode {
}
}
init(account: Account, presentationData: PresentationData, mode: WalletSplashMode, action: @escaping () -> Void, secondaryAction: @escaping () -> Void) {
init(account: Account, presentationData: PresentationData, mode: WalletSplashMode, action: @escaping () -> Void, secondaryAction: @escaping () -> Void, openTerms: @escaping () -> Void) {
self.presentationData = presentationData
self.mode = mode
self.secondaryAction = secondaryAction
@ -212,7 +260,7 @@ private final class WalletSplashScreenNode: ViewControllerTracingNode {
let title: String
let text: String
let buttonText: String
let termsText: String
let termsText: NSAttributedString
let secondaryActionText: String
switch mode {
@ -220,21 +268,23 @@ private final class WalletSplashScreenNode: ViewControllerTracingNode {
title = "Gram Wallet"
text = "Gram wallet allows you to make fast and secure blockchain-based payments without intermediaries."
buttonText = "Create My Wallet"
termsText = "By creating the wallet you accept\nTerms of Conditions."
let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor, additionalAttributes: [:])
let link = MarkdownAttributeSet(font: Font.regular(13.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor, additionalAttributes: [NSAttributedString.Key.underlineStyle.rawValue: NSUnderlineStyle.single.rawValue as NSNumber])
termsText = parseMarkdownIntoAttributedString("By creating the wallet you accept\n[Terms of Conditions]().", attributes: MarkdownAttributes(body: body, bold: body, link: link, linkAttribute: { _ in nil }), textAlignment: .center)
self.iconNode.image = UIImage(bundleImageName: "Settings/Wallet/IntroIcon")
secondaryActionText = ""
case .created:
title = "Congratulations"
text = "Your Gram wallet has just been created. Only you control it.\n\nTo be able to always have access to it, please write down secret words and\nset up a secure passcode."
buttonText = "Proceed"
termsText = ""
termsText = NSAttributedString(string: "")
self.iconNode.image = UIImage(bundleImageName: "Settings/Wallet/CreatedIcon")
secondaryActionText = ""
case .success:
title = "Ready to go!"
text = "Youre all set. Now you have a wallet that only you control - directly, without middlemen or bankers. "
buttonText = "View My Wallet"
termsText = ""
termsText = NSAttributedString(string: "")
self.iconNode.image = nil
if let path = getAppBundle().path(forResource: "celebrate", ofType: "tgs") {
self.animationNode.setup(account: account, resource: .localFile(path), width: 280, height: 280, mode: .direct)
@ -245,7 +295,7 @@ private final class WalletSplashScreenNode: ViewControllerTracingNode {
title = "Too Bad"
text = "Without the secret words, you can't'nrestore access to the wallet."
buttonText = "Create a New Wallet"
termsText = ""
termsText = NSAttributedString(string: "")
self.iconNode.image = nil
if let path = getAppBundle().path(forResource: "sad", ofType: "tgs") {
self.animationNode.setup(account: account, resource: .localFile(path), width: 280, height: 280, mode: .direct)
@ -256,20 +306,47 @@ private final class WalletSplashScreenNode: ViewControllerTracingNode {
title = "Sending Grams"
text = "Please wait a few seconds for your transaction to be processed..."
buttonText = ""
termsText = ""
termsText = NSAttributedString(string: "")
self.iconNode.image = UIImage(bundleImageName: "Settings/Wallet/SendingIcon")
secondaryActionText = ""
case let .sent(_, amount):
title = "Done!"
text = "\(amount) Grams have been sent."
buttonText = "View My Wallet"
termsText = ""
termsText = NSAttributedString(string: "")
self.iconNode.image = nil
if let path = getAppBundle().path(forResource: "celebrate", ofType: "tgs") {
self.animationNode.setup(account: account, resource: .localFile(path), width: 280, height: 280, mode: .direct)
self.animationNode.visibility = true
}
secondaryActionText = ""
case .secureStorageNotAvailable:
title = "Too Bad"
text = "Please set up Passcode on your device to enable secure payments with your Gram wallet."
buttonText = "OK"
termsText = NSAttributedString(string: "")
self.iconNode.image = nil
if let path = getAppBundle().path(forResource: "sad", ofType: "tgs") {
self.animationNode.setup(account: account, resource: .localFile(path), width: 280, height: 280, mode: .direct)
self.animationNode.visibility = true
}
secondaryActionText = ""
case let .secureStorageReset(reason):
title = "Too Bad"
switch reason {
case .notAvailable:
text = "Unfortunately, your wallet is no longer available because your system Passcode or Touch ID has been turned off."
case .changed:
text = "Unfortunately, your wallet is no longer available due to the change in your system security settings (Passcode/Touch ID). To restore your wallet, tap \"import existing wallet\"."
}
buttonText = "Import Existing Wallet"
termsText = NSAttributedString(string: "")
self.iconNode.image = nil
if let path = getAppBundle().path(forResource: "sad", ofType: "tgs") {
self.animationNode.setup(account: account, resource: .localFile(path), width: 280, height: 280, mode: .direct)
self.animationNode.visibility = true
}
secondaryActionText = "Create New Wallet"
}
self.titleNode = ImmediateTextNode()
@ -287,7 +364,7 @@ private final class WalletSplashScreenNode: ViewControllerTracingNode {
self.termsNode = ImmediateTextNode()
self.termsNode.displaysAsynchronously = false
self.termsNode.attributedText = NSAttributedString(string: termsText, font: Font.regular(13.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor)
self.termsNode.attributedText = termsText
self.termsNode.maximumNumberOfLines = 0
self.termsNode.textAlignment = .center
@ -331,6 +408,20 @@ private final class WalletSplashScreenNode: ViewControllerTracingNode {
}
self.secondaryActionButtonNode.addTarget(self, action: #selector(self.secondaryActionPressed), forControlEvents: .touchUpInside)
self.termsNode.linkHighlightColor = self.presentationData.theme.list.itemSecondaryTextColor.withAlphaComponent(0.5)
self.termsNode.highlightAttributeAction = { attributes in
if let _ = attributes[NSAttributedString.Key.underlineStyle] {
return NSAttributedString.Key.underlineStyle
} else {
return nil
}
}
self.termsNode.tapAttributeAction = { attributes in
if let _ = attributes[NSAttributedString.Key.underlineStyle] {
openTerms()
}
}
}
@objc private func secondaryActionPressed() {

View File

@ -27,6 +27,7 @@ private final class WalletTransactionInfoControllerArguments {
private enum WalletTransactionInfoSection: Int32 {
case amount
case info
case comment
}
private enum WalletTransactionInfoEntry: ItemListNodeEntry {
@ -35,6 +36,8 @@ private enum WalletTransactionInfoEntry: ItemListNodeEntry {
case infoAddress(PresentationTheme, String)
case infoCopyAddress(PresentationTheme, String)
case infoSendGrams(PresentationTheme, String)
case commentHeader(PresentationTheme, String)
case comment(PresentationTheme, String)
var section: ItemListSectionId {
switch self {
@ -42,6 +45,8 @@ private enum WalletTransactionInfoEntry: ItemListNodeEntry {
return WalletTransactionInfoSection.amount.rawValue
case .infoHeader, .infoAddress, .infoCopyAddress, .infoSendGrams:
return WalletTransactionInfoSection.info.rawValue
case .commentHeader, .comment:
return WalletTransactionInfoSection.comment.rawValue
}
}
@ -57,6 +62,10 @@ private enum WalletTransactionInfoEntry: ItemListNodeEntry {
return 3
case .infoSendGrams:
return 4
case .commentHeader:
return 5
case .comment:
return 6
}
}
@ -80,6 +89,10 @@ private enum WalletTransactionInfoEntry: ItemListNodeEntry {
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.sendGrams()
})
case let .commentHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .comment(theme, text):
return ItemListMultilineTextItem(theme: theme, text: text, enabledEntityTypes: [], sectionId: self.section, style: .blocks)
}
}
}
@ -111,6 +124,30 @@ private func extractAddress(_ walletTransaction: WalletTransaction) -> String {
return text
}
private func extractDescription(_ walletTransaction: WalletTransaction) -> String {
let transferredValue = walletTransaction.transferredValue
var text = ""
if transferredValue <= 0 {
for message in walletTransaction.outMessages {
if !text.isEmpty {
text.append("\n\n")
}
text.append(message.textMessage)
}
} else {
if let inMessage = walletTransaction.inMessage {
text = inMessage.textMessage
}
}
return text
}
private func formatAddress(_ address: String) -> String {
var address = address
address.insert("\n", at: address.index(address.startIndex, offsetBy: address.count / 2))
return address
}
private func walletTransactionInfoControllerEntries(presentationData: PresentationData, walletTransaction: WalletTransaction, state: WalletTransactionInfoControllerState) -> [WalletTransactionInfoEntry] {
var entries: [WalletTransactionInfoEntry] = []
@ -118,16 +155,22 @@ private func walletTransactionInfoControllerEntries(presentationData: Presentati
let transferredValue = walletTransaction.transferredValue
let text = extractAddress(walletTransaction)
let description = extractDescription(walletTransaction)
if transferredValue <= 0 {
entries.append(.infoHeader(presentationData.theme, "RECIPIENT"))
} else {
entries.append(.infoHeader(presentationData.theme, "SENDER"))
}
entries.append(.infoAddress(presentationData.theme, text))
entries.append(.infoAddress(presentationData.theme, formatAddress(text)))
entries.append(.infoCopyAddress(presentationData.theme, "Copy Address"))
entries.append(.infoSendGrams(presentationData.theme, "Send Grams"))
if !description.isEmpty {
entries.append(.commentHeader(presentationData.theme, "COMMENT"))
entries.append(.comment(presentationData.theme, description))
}
return entries
}
@ -295,6 +338,8 @@ private class WalletTransactionHeaderItemNode: ListViewItemNode {
contentSize = CGSize(width: params.width, height: titleLayout.size.height + verticalInset + verticalInset)
let insets = itemListNeighborsGroupedInsets(neighbors)
let titleScale: CGFloat = min(1.0, (params.width - 40.0 - iconSize.width) / titleLayout.size.width)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in
@ -311,9 +356,11 @@ private class WalletTransactionHeaderItemNode: ListViewItemNode {
let contentWidth = titleLayout.size.width + iconSpacing + iconSize.width / 2.0
let titleFrame = CGRect(origin: CGPoint(x: floor((params.width - contentWidth) / 2.0), y: verticalInset), size: titleLayout.size)
let subtitleFrame = CGRect(origin: CGPoint(x: floor((params.width - subtitleLayout.size.width) / 2.0), y: titleFrame.maxY - 5.0), size: subtitleLayout.size)
strongSelf.titleNode.frame = titleFrame
strongSelf.titleNode.position = titleFrame.center
strongSelf.titleNode.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
strongSelf.titleNode.transform = CATransform3DMakeScale(titleScale, titleScale, 1.0)
strongSelf.subtitleNode.frame = subtitleFrame
strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX + iconSpacing, y: titleFrame.minY + floor((titleFrame.height - iconSize.height) / 2.0)), size: iconSize)
strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: floor(titleFrame.midX + titleFrame.width / 2.0 * titleScale + iconSpacing), y: titleFrame.minY + floor((titleFrame.height - iconSize.height) / 2.0) - 2.0), size: iconSize)
}
})
}
@ -327,4 +374,3 @@ private class WalletTransactionHeaderItemNode: ListViewItemNode {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}

View File

@ -11,6 +11,7 @@ import SolidRoundedButtonNode
import UndoUI
import AlertUI
import SwiftSignalKit
import TextFormat
private let possibleWordList: [String] = [
"abandon",
@ -2063,10 +2064,8 @@ private let possibleWordList: [String] = [
"zoo"
]
private let verifyWordIndices: [Int] = [4, 16, 21]
public enum WalletWordCheckMode {
case verify(WalletInfo, [String])
case verify(WalletInfo, [String], [Int])
case `import`
}
@ -2113,11 +2112,11 @@ public final class WalletWordCheckScreen: ViewController {
return
}
switch strongSelf.mode {
case let .verify(walletInfo, wordList):
case let .verify(walletInfo, wordList, indices):
let enteredWords = (strongSelf.displayNode as! WalletWordCheckScreenNode).enteredWords
var isCorrect = true
for i in 0 ..< enteredWords.count {
if enteredWords[i] != wordList[verifyWordIndices[i]] {
if enteredWords[i] != wordList[indices[i]] {
isCorrect = false
break
}
@ -2580,23 +2579,27 @@ private final class WalletWordCheckScreenNode: ViewControllerTracingNode, UIScro
self.iconNode.displaysAsynchronously = false
let title: String
let text: String
let text: NSAttributedString
let buttonText: String
let secondaryActionText: String
let wordIndices: [Int]
switch mode {
case .verify:
wordIndices = verifyWordIndices
case let .verify(_, _, indices):
wordIndices = indices
title = "Test Time!"
text = "Lets check that you wrote them down correctly. Please enter words\n\(wordIndices[0] + 1), \(wordIndices[1] + 1) and \(wordIndices[2] + 1) below:"
let body = MarkdownAttributeSet(font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor, additionalAttributes: [:])
let bold = MarkdownAttributeSet(font: Font.bold(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor, additionalAttributes: [NSAttributedString.Key.underlineStyle.rawValue: NSUnderlineStyle.single.rawValue as NSNumber])
text = parseMarkdownIntoAttributedString("Lets check that you wrote them down correctly. Please enter words\n**\(wordIndices[0] + 1)**, **\(wordIndices[1] + 1)** and **\(wordIndices[2] + 1)** below:", attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in nil }), textAlignment: .center)
buttonText = "Continue"
secondaryActionText = ""
self.iconNode.image = UIImage(bundleImageName: "Settings/Wallet/WordsCheckIcon")
case .import:
title = "24 Secret Words"
text = "Please restore access to your wallet by\nentering the 24 secret words you wrote down when creating the wallet."
text = NSAttributedString(string: "Please restore access to your wallet by\nentering the 24 secret words you wrote down when creating the wallet.", font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
buttonText = "Continue"
secondaryActionText = "I don't have those"
wordIndices = Array(0 ..< 24)
@ -2617,7 +2620,7 @@ private final class WalletWordCheckScreenNode: ViewControllerTracingNode, UIScro
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
self.textNode.attributedText = text
self.textNode.maximumNumberOfLines = 0
self.textNode.lineSpacing = 0.1
self.textNode.textAlignment = .center

View File

@ -72,7 +72,15 @@ public final class WalletWordDisplayScreen: ViewController {
}
})]), in: .window(.root))
} else {
strongSelf.push(WalletWordCheckScreen(context: strongSelf.context, tonContext: strongSelf.tonContext, mode: .verify(strongSelf.walletInfo, strongSelf.wordList)))
var wordIndices: [Int] = []
while wordIndices.count < 3 {
let index = Int(arc4random_uniform(UInt32(strongSelf.wordList.count)))
if !wordIndices.contains(index) {
wordIndices.append(index)
}
}
wordIndices.sort()
strongSelf.push(WalletWordCheckScreen(context: strongSelf.context, tonContext: strongSelf.tonContext, mode: .verify(strongSelf.walletInfo, strongSelf.wordList, wordIndices)))
}
})