红包

概述

基于网易云信的即时通讯(NIM),接入金融魔方 IM 红包 SDK 实现 App 快速便捷地集成发单聊红包、发群聊红包、拆红包并查看交易记录等功能。
【注:示例中的 partnerId 为金融魔方分配给商户的唯一标识的渠道 id,对于云信 SDK 直接使用云信为客户分配的 appKey 作为 partnerId,因此不需要再配置渠道 id。】
进行如下集成之前,务必保证项目中已经导入了金融魔方红包 SDK 如若想集成钱包功能,也请集成钱包 SDK

集成准备工作

IMAGE

#import "JRMFHeader.h"
- (void)application:(UIApplication *)application
            openURL:(NSURL *)url
  sourceApplication:(NSString *)sourceApplication
         annotation:(id)annotation
{
    if ([url.host isEqualToString:@"safepay"]) {
        //跳转支付宝钱包进行支付,处理支付结果
        [[AlipaySDK defaultService] processOrderWithPaymentResult:url standbyCallback:^(NSDictionary *resultDic) {
            if ([[resultDic objectForKey:@"resultStatus"] isEqualToString:@"9000"]) {
                [JrmfPacket doActionAlipayDone];
            }
        }];
    }

    [[SPayClient sharedInstance] application:application openURL:url sourceApplication:sourceApplication annotation:annotation];
}

- (void)application:(UIApplication *)app
            openURL:(NSURL *)url
            options:(NSDictionary<NSString*, id> *)options
{
    if ([url.host isEqualToString:@"safepay"]) {
        //跳转支付宝钱包进行支付,处理支付结果
        [[AlipaySDK defaultService] processOrderWithPaymentResult:url standbyCallback:^(NSDictionary *resultDic) {
            if ([[resultDic objectForKey:@"resultStatus"] isEqualToString:@"9000"]) {
                [JrmfPacket doActionAlipayDone];
            }
        }];
    }

    [[SPayClient sharedInstance]application:app openURL:url options:options];
}

- (void)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [[SPayClient sharedInstance] application:application didFinishLaunchingWithOptions:launchOptions];
}

- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url
{
    return [[SPayClient sharedInstance] application:application handleOpenURL:url];
}

集成流程

1. 获取 token

@implementation NTESRedPacketManager

#pragma mark - NIMLoginManagerDelegate
//云信登录之后的回调方法 NIMLoginManagerDelegate
- (void)onLogin:(NIMLoginStep)step
{
    switch (step)
    {
        case NIMLoginStepSyncOK:
        {
            NIMRedPacketTokenRequest *request = [[NIMRedPacketTokenRequest alloc] init];
            request.type = NIMRedPacketServiceTypeJRMF;
            NSString *envelopeName = @"云信红包";
            BOOL isOnLine = [NTESDemoConfig sharedConfig].redPacketConfig.useOnlineEnv;
            NSString *aliPaySchemeUrl = [NTESDemoConfig sharedConfig].redPacketConfig.aliPaySchemeUrl;
            [[NIMSDK sharedSDK].redPacketManager fetchTokenWithRedPacketRequest:request completion:^(NSError * _Nullable error, NSString * _Nullable token) {
                if (!error)
                {
                    [JRMFSington GetPacketSington].JrmfThirdToken = token;
                    [JrmfPacket instanceJrmfPacketWithPartnerId:[JRMFSington GetPacketSington].JrmfPartnerId EnvelopeName:envelopeName aliPaySchemeUrl:aliPaySchemeUrl weChatSchemeUrl:nil appMothod:isOnLine];
                    [JrmfWalletSDK instanceJrmfWalletSDKWithPartnerId:[JRMFSington GetPacketSington].JrmfPartnerId AppMethod:isOnLine];
                }
                else
                {
                    DDLogError(@"fetch red packet token error : %@",error);
                }
            }];
        }
            break;
        default:
            break;
    }
}

2. 添加红包相关自定义消息类型

在 NTESCustomAttachmentDefines 中自定义消息枚举

typedef NS_ENUM(NSInteger,NTESCustomMessageType) {
    CustomMessageTypeJanKenPon  = 1,   //剪子石头布
    CustomMessageTypeSnapchat   = 2,   //阅后即焚
    CustomMessageTypeChartlet   = 3,   //贴图表情
    CustomMessageTypeWhiteboard = 4,   //白板会话
    CustomMessageTypeRedPacket  = 5,   //红包消息
    CustomMessageTypeRedPacketTip = 6, //红包提示消息
};

3. 自定义消息体和视图

@interface NTESRedPacketAttachment : NSObject<NIMCustomAttachment,NTESCustomAttachmentInfo>

//红包 ID
@property (nonatomic, copy) NSString *redPacketId;
//红包标题
@property (nonatomic, copy) NSString *title;
//红包内容
@property (nonatomic, copy) NSString *content;

@end
@implementation NTESRedPacketAttachment

// 该方法必须实现,用于自定义消息的解码
- (NSString *)encodeAttachment {
    NSDictionary *dictContent = @{
                                  CMRedPacketTitle   :  self.title,
                                  CMRedPacketContent :  self.content,
                                  CMRedPacketId      :  self.redPacketId
                                 };
    NSDictionary *dict = @{CMType: @(CustomMessageTypeRedPacket), CMData: dictContent};
    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dict
                                                       options:0
                                                         error:nil];
    return [[NSString alloc] initWithData:jsonData
                                 encoding:NSUTF8StringEncoding];
}

//该方法用于返回红包消息视图的大小,开发者可自行定义
- (CGSize)contentSize:(NIMMessage *)message cellWidth:(CGFloat)width {
    return CGSizeMake(249, 96);
}

//该方法返回消息内容和消息视图的边界距离
- (UIEdgeInsets)contentViewInsets:(NIMMessage *)message {
    CGFloat bubblePaddingForImage    = 3.f;
    CGFloat bubbleArrowWidthForImage = 5.f;
    if (message.isOutgoingMsg) {
        return  UIEdgeInsetsMake(bubblePaddingForImage,bubblePaddingForImage,bubblePaddingForImage,bubblePaddingForImage + bubbleArrowWidthForImage);
    }else{
        return  UIEdgeInsetsMake(bubblePaddingForImage,bubblePaddingForImage + bubbleArrowWidthForImage, bubblePaddingForImage,bubblePaddingForImage);
    }
}

//该方法返回与之相关联的显示消息的视图,即 NTESSessionRedPacketContentView
- (NSString *)cellContent:(NIMMessage *)message{
   return @"NTESSessionRedPacketContentView";
}

@end
@interface NTESRedPacketTipAttachment : NSObject<NIMCustomAttachment,NTESCustomAttachmentInfo>

//红包发送者的 ID
@property (nonatomic, strong) NSString * sendPacketId;

//拆红包者的 ID
@property (nonatomic, strong) NSString * openPacketId;

//红包 ID
@property (nonatomic, strong) NSString * packetId;

//是否为最后一个红包
@property (nonatomic, strong) NSString * isGetDone;

@end

【注:NTESRedPacketTipAttachment 的实现和 NTESRedPacketAttachment 相似,具体实现可以参考 demo 代码】

NSString *const NIMDemoEventNameOpenRedPacket = @"NIMDemoEventNameOpenRedPacket";

@interface NTESSessionRedPacketContentView()

@property (nonatomic, strong) UILabel *titleLabel;

@property (nonatomic, strong) UILabel *subTitleLabel;

@property (nonatomic, strong) UILabel *descLabel;

@property (nonatomic, strong) UITapGestureRecognizer *tap;

@end

@implementation NTESSessionRedPacketContentView

- (instancetype)initSessionMessageContentView{
    self = [super initSessionMessageContentView];
    if (self) {
        // 内容布局
    }
    return self;
}

//点击红包的手势处理
- (void)onTouchUpInside:(id)sender
{
    if ([self.delegate respondsToSelector:@selector(onCatchEvent:)]) {
        NIMKitEvent *event = [[NIMKitEvent alloc] init];
        event.eventName = NIMDemoEventNameOpenRedPacket;
        event.messageModel = self.model;
        event.data = self;
        [self.delegate onCatchEvent:event];
    }
}

- (void)layoutSubviews
{
    [super layoutSubviews];
    BOOL outgoing = self.model.message.isOutgoingMsg;
    if (outgoing)
    {
       //靠界面左排版
    }
    else
    {
        //靠界面右排版
    }
}

- (UIImage *)chatBubbleImageForState:(UIControlState)state outgoing:(BOOL)outgoing
{
    NSString *stateString = state == UIControlStateNormal? @"normal" : @"pressed";
    NSString *imageName = @"icon_redpacket_";
    if (outgoing)
    {
        //当消息头像位于右边的时候,cell 显示的内容以及返回的背景图片
    }
    else
    {
        //当消息头像位于左边的时候,cell 显示的内容以及返回的背景图片
    }
    return [UIImage imageNamed:imageName];
}
static NSString *const NTESShowRedPacketDetailEvent = @"NTESShowRedPacketDetailEvent";

@interface NTESSessionRedPacketTipContentView : NIMSessionMessageContentView

@property (nonatomic,strong) M80AttributedLabel *label;

@end
//实现 NTESSessionRedPacketContentView 中相同的布局方法
//...

- (UIImage *)chatBubbleImageForState:(UIControlState)state outgoing:(BOOL)outgoing
{
    NSString *name = [[[NIMKit sharedKit] resourceBundleName] stringByAppendingPathComponent:@"icon_session_time_bg"];
    UIEdgeInsets insets = UIEdgeInsetsFromString(@"{8,20,8,20}");
    return [[UIImage imageNamed:name] resizableImageWithCapInsets:insets resizingMode:UIImageResizingModeStretch];
}
@implementation NTESCellLayoutConfig

- (instancetype)init
{
    if (self = [super init])
    {
        _types =  @[
                   //...
                   @"NTESRedPacketAttachment",
                   @"NTESRedPacketTipAttachment"
                   ];
        _sessionCustomconfig = [[NTESSessionCustomContentConfig alloc] init];
        _chatroomTextConfig  = [[NTESChatroomTextContentConfig alloc] init];
        _chatroomRobotConfig = [[NTESChatroomRobotContentConfig alloc] init];
    }
    return self;
}

        2)当显示的消息是领取红包的提示信息的时候不显示头像和昵称

    - (BOOL)shouldShowAvatar:(NIMMessageModel *)model
{
    //...
    if ([self isRedpacketTip:model.message]) {
        return NO;
    }
    return [super shouldShowAvatar:model];
}

- (BOOL)shouldShowNickName:(NIMMessageModel *)model
{
    //...
    if ([self isRedpacketTip:model.message]) {
        return NO;
    }
    return [super shouldShowNickName:model];
}

//不显示红包领取的头像和昵称
- (BOOL)isRedpacketTip:(NIMMessage *)message
{
    if (message.messageType == NIMMessageTypeCustom) {
        NIMCustomObject *object = message.messageObject;
        if ([object.attachment isKindOfClass:[NTESRedPacketTipAttachment class]]) {
            return YES;
        }
    }
    return NO;
}
@implementation NTESAppDelegate
    //...
    //注入 NIMKit 自定义排版配置
    [[NIMKit sharedKit] registerLayoutConfig:[NTESCellLayoutConfig new]];
    //...
@end
@implementation NTESSessionConfig

- (NSArray *)mediaItems
{
    //...
    NIMMediaItem *redPacket  = [NIMMediaItem item:@"onTapMediaItemRedPacket:"
                                      normalImage:[UIImage imageNamed:@"icon_redpacket_normal"]
                                    selectedImage:[UIImage imageNamed:@"icon_redpacket_pressed"]
                                            title:@"红包"];

    //...
    //将 redPacket 按钮添加到数组 items 中,并将 items 添加 defaultMediaItems 中返回
    return [defaultMediaItems arrayByAddingObjectsFromArray:items];
}
@implementation NTESSessionMsgConverter

//发送红包
+ (NIMMessage *)msgWithRedPacket:(NTESRedPacketAttachment *)attachment
{
    NIMMessage *message               = [[NIMMessage alloc] init];
    NIMCustomObject *customObject     = [[NIMCustomObject alloc] init];
    customObject.attachment           = attachment;
    message.messageObject             = customObject;
    message.apnsContent               = @"发来了一个红包";
    NIMMessageSetting *setting        = [[NIMMessageSetting alloc] init];
    setting.historyEnabled            = NO;
    message.setting                   = setting;
    return message;
}

//领红包后的提示
+ (NIMMessage *)msgWithRedPacketTip:(NTESRedPacketTipAttachment *)attachment
{
    NIMMessage *message               = [[NIMMessage alloc] init];
    NIMCustomObject *customObject     = [[NIMCustomObject alloc] init];
    customObject.attachment           = attachment;
    message.messageObject             = customObject;

    NIMMessageSetting *setting = [[NIMMessageSetting alloc] init];
    setting.apnsEnabled        = NO;
    setting.shouldBeCounted    = NO;
    setting.historyEnabled     = NO;
    message.setting            = setting;
    return message;
}

4. 消息发送及消息事件处理

@implementation NTESSessionViewController
//附加按钮面板
- (void)onTapMediaItemRedPacket:(NIMMediaItem *)item
{
    //调用红包发送接口,这里统一封装相关接口到 NTESRedPacketManager 类中
    [[NTESRedPacketManager sharedManager] sendRedPacket:self.session];
}

//调用红包发送的回调方法(金融魔方 SDK 提供)
@implementation NTESRedPacketManager
#pragma mark - jrmfManagerDelegate
- (void)dojrmfActionDidSendEnvelopedWithID:(NSString *)envId Name:(NSString *)envName Message:(NSString *)envMsg Stat:(jrmfSendStatus)jrmfStat
{
    switch (jrmfStat) {
        case kjrmfStatUnknow:
            break;
        case kjrmfStatSucess:
        {
            NTESRedPacketAttachment *attachment = [[NTESRedPacketAttachment alloc] init];
            attachment.title = envName;
            attachment.redPacketId = envId;
            attachment.content = envMsg;
            NIMMessage *message = [NTESSessionMsgConverter msgWithRedPacket:attachment];
            [[NIMSDK sharedSDK].chatManager sendMessage:message toSession:_currentSession error:nil];
        }
        case kjrmfStatCancel:
            //取消成功都重置会话
            _currentSession = nil;
        default:
            break;
    }
}
@implementation NTESSessionViewController

- (BOOL)onTapCell:(NIMKitEvent *)event
{
    BOOL handled = [super onTapCell:event];
    NSString *eventName = event.eventName;
    //拆红包 
    if([eventName isEqualToString:NIMDemoEventNameOpenRedPacket])
    {
        NIMCustomObject *object = event.messageModel.message.messageObject;
        NTESRedPacketAttachment *attachment = (NTESRedPacketAttachment *)object.attachment;
        //拆红包相关接口
        [[NTESRedPacketManager sharedManager] openRedPacket:attachment.redPacketId from:event.messageModel.message.from session:self.session];
        handled = YES;
    }
    else if([eventName isEqualToString:NTESShowRedPacketDetailEvent])
    {
        NIMCustomObject *object = event.messageModel.message.messageObject;
        NTESRedPacketTipAttachment *attachment = (NTESRedPacketTipAttachment *)object.attachment;
        //调用展示详情页面的接口
        [[NTESRedPacketManager sharedManager] showRedPacketDetail:attachment.packetId];
        handled = YES;
    }
    if (!handled) {
        NSAssert(0, @"invalid event");
    }
    return handled;
}
@implementation NTESRedPacketManager
#pragma mark - jrmfManagerDelegate

- (void)dojrmfActionOpenPacketSuccessWithGetDone:(BOOL)isDone
{    
    NTESRedPacketTipAttachment *attachment = [[NTESRedPacketTipAttachment alloc] init];
    attachment.isGetDone = @(isDone).description;
    attachment.openPacketId = [[NIMSDK sharedSDK].loginManager currentAccount];
    attachment.packetId = _currentRedpacketId;
    attachment.sendPacketId = _currentRedpacketFrom;

    [[NIMSDK sharedSDK].chatManager sendMessage:[NTESSessionMsgConverter msgWithRedPacketTip:attachment] toSession:_currentSession error:nil];

    _currentSession = nil;
    _currentRedpacketId   = nil;
    _currentRedpacketFrom = nil;
}

5. 消息接收

- (id<NIMCustomAttachment>)decodeAttachment:(NSString *)content
{
    id<NIMCustomAttachment> attachment = nil;

    NSData *data = [content dataUsingEncoding:NSUTF8StringEncoding];
    if (data) {
        NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data
                                                             options:0
                                                               error:nil];
        if ([dict isKindOfClass:[NSDictionary class]])
        {
            NSInteger type     = [dict jsonInteger:CMType];
            NSDictionary *data = [dict jsonDict:CMData];
            switch (type) {
                //在当前方法中添加红包消息的解码信息
                case CustomMessageTypeRedPacket:
                {
                    attachment = [[NTESRedPacketAttachment alloc] init];
                    ((NTESRedPacketAttachment *)attachment).title = [data jsonString:CMRedPacketTitle];
                    ((NTESRedPacketAttachment *)attachment).content = [data jsonString:CMRedPacketContent];
                    ((NTESRedPacketAttachment *)attachment).redPacketId = [data jsonString:CMRedPacketId];
                }
                    break;
                //同理,添加红包接受的解码信息
                case CustomMessageTypeRedPacketTip:
                {
                    attachment = [[NTESRedPacketTipAttachment alloc] init];
                    ((NTESRedPacketTipAttachment *)attachment).sendPacketId = [data jsonString:CMRedPacketSendId];
                    ((NTESRedPacketTipAttachment *)attachment).packetId  = [data jsonString:CMRedPacketId];
                    ((NTESRedPacketTipAttachment *)attachment).isGetDone = [data jsonString:CMRedPacketDone];
                    ((NTESRedPacketTipAttachment *)attachment).openPacketId = [data jsonString:CMRedPacketOpenId];
                }
                    break;
                default:
                    break;
            }
            attachment = [self checkAttachment:attachment] ? attachment : nil;
        }
    }
    return attachment;
}
@implementation NTESAppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    //...
    //注册自定义消息的解析器
    [NIMCustomObject registerCustomDecoder:[NTESCustomAttachmentDecoder new]];
    //...
}
@implementation NTESNotificationCenter
#pragma mark - NIMChatManagerDelegate
- (void)onRecvMessages:(NSArray *)recvMessages
{
    //首先对于消息进行过滤,接下来再显示需要显示的消息
    NSArray *messages = [self filterMessages:recvMessages];
    if (messages.count)
    {
        static BOOL isPlaying = NO;
        if (isPlaying) {
            return;
        }
        isPlaying = YES;
        [self playMessageAudioTip];
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            isPlaying = NO;
        });
        [self checkMessageAt:messages];
    }
}

- (NSArray *)filterMessages:(NSArray *)messages
{
    NSMutableArray *array = [[NSMutableArray alloc] init];
    for (NIMMessage *message in messages)
    {
        if ([self checkRedPacketTip:message] && ![self canSaveMessageRedPacketTip:message])
        {
            [[NIMSDK  sharedSDK].conversationManager deleteMessage:message];
            [self.currentSessionViewController uiDeleteMessage:message];
            continue;
        }
        [array addObject:message];
    }
    return [NSArray arrayWithArray:array];
}

//需要记得添加 delegate
- (instancetype)init {
    self = [super init];
    if(self) {
        //...
        [[NIMSDK sharedSDK].chatManager addDelegate:self];
        //...
    }
    return self;
}

6. 消息列表最近消息展示

在消息列表的对应会话中显示最近消息,云信 demo 中在 NTESSessionListViewController 中进行操作

@implementation NTESSessionListViewController

- (NSAttributedString *)contentForRecentSession:(NIMRecentSession *)recent{
    //根据 obeject.attachment 的类型来确定展示的信息
    if ([object.attachment isKindOfClass:[NTESRedPacketAttachment class]])
    {
      text = @"[红包消息]";
    }
    else if ([object.attachment isKindOfClass:[NTESRedPacketTipAttachment class]])
    {
      NTESRedPacketTipAttachment *attach = (NTESRedPacketTipAttachment *)object.attachment;
      text = attach.formatedMessage;
    }
    return attContent;
}

7. 设置页面展示 "我的钱包"

- (void)buildData {
    //...
    NSArray *data = @[
                        @{
                          HeaderTitle:@"",
                          RowContent :@[
                                  @{
                                      Title         : @"我的钱包",
                                      CellAction    : @"onTouchMyWallet:",
                                      ShowAccessory : @(YES),
                                    },
                                  ],
                          },
                      ];
    //...
}

//点击“我的钱包”事件
- (void)onTouchMyWallet:(id)sender
{
    JrmfWalletSDK * jrmf = [[JrmfWalletSDK alloc] init];
    NSString *userId = [[NIMSDK sharedSDK].loginManager currentAccount];
    NIMKitInfo *userInfo = [[NIMKit sharedKit] infoByUser:userId option:nil];
    //展示我的钱包页面
    [jrmf doPresentJrmfWalletPageWithBaseViewController:self userId:userId userName:userInfo.showName userHeadLink:userInfo.avatarUrlString thirdToken:[JRMFSington GetPacketSington].JrmfThirdToken];
}

8. 用户基本信息更新

在云信 Demo 中更新用户信息,并不会实时更新红包中的用户基本信息,需要手动调用更新接口

- (void)updateUserInfo
{
    NSString *me = [[NIMSDK sharedSDK].loginManager currentAccount];
    NSString *nickName = [NTESSessionUtil showNick:me inSession:nil];
    NSString *headUrl = [[NIMKit sharedKit] infoByUser:me option:nil].avatarUrlString;
    //需要手动调用金融魔方提供的该接口
    [JrmfPacket updateUserMsgWithUserId:me userName:nickName userHead:headUrl thirdToken:[JRMFSington GetPacketSington].JrmfThirdToken completion:^(NSError *error, NSDictionary *resultDic) {
        DDLogInfo(@"red packet update user info complete, error : %@",error);
    }];
}